├── .gitignore ├── LICENSE ├── README.md ├── addons └── smart_graphics_settings │ ├── LICENSE │ ├── README.md │ ├── adaptive_graphics.gd │ ├── adaptive_graphics.gd.uid │ ├── adaptive_graphics_ui.gd │ ├── adaptive_graphics_ui.gd.uid │ ├── adaptive_graphics_ui.tscn │ ├── demo │ ├── README.md │ ├── demo_scene.gd │ ├── demo_scene.gd.uid │ └── demo_scene.tscn │ ├── fps_monitor.gd │ ├── fps_monitor.gd.uid │ ├── graphics_settings_manager.gd │ ├── graphics_settings_manager.gd.uid │ ├── images │ ├── icon.png │ ├── icon.png.import │ ├── icon.svg │ ├── icon.svg.import │ ├── logo.png │ ├── logo.png.import │ ├── logo.svg │ ├── logo.svg.import │ ├── smart-graphics-settings-icon.svg │ ├── smart-graphics-settings-icon.svg.import │ ├── title-text.png │ └── title-text.png.import │ ├── plugin.cfg │ ├── plugin.gd │ ├── plugin.gd.uid │ ├── smart_graphics_settings.gd │ ├── smart_graphics_settings.gd.uid │ └── ui_panel_background.tres └── project.godot /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | *.translation 9 | 10 | # Mono-specific ignores 11 | .mono/ 12 | data_*/ 13 | mono_crash.*.json 14 | 15 | # Build artifacts 16 | *.o 17 | *.os 18 | *.obj 19 | *.bc 20 | *.pyc 21 | *.dblite 22 | *.pdb 23 | *.lib 24 | *.config 25 | *.creator 26 | *.creator.user 27 | *.files 28 | *.includes 29 | *.idb 30 | 31 | # Editor-specific ignores 32 | .vscode/ 33 | .idea/ 34 | .vs/ 35 | *.swp 36 | *~ 37 | *.tmp 38 | *.bak 39 | 40 | # OS-specific ignores 41 | .DS_Store 42 | ._* 43 | .Spotlight-V100 44 | .Trashes 45 | ehthumbs.db 46 | Thumbs.db 47 | desktop.ini 48 | 49 | # Demo exports 50 | addons/smart_graphics_settings/demo/exports/ 51 | *.exe 52 | *.pck 53 | *.zip 54 | 55 | # Log files 56 | *.log 57 | 58 | # Temporary files 59 | *.tmp 60 | *.temp 61 | 62 | # Cursor 63 | .cursorrules 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lucas Becker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Smart Graphics Settings 3 |

4 | 5 | # Smart Graphics Settings for Godot 4.4 6 | 7 | A powerful adaptive graphics settings system for Godot 4.4 that automatically adjusts visual quality based on performance to maintain a smooth framerate. 8 | 9 | ## Features 10 | 11 | - **Adaptive Quality**: Automatically adjusts graphics settings to maintain target FPS 12 | - **Comprehensive Settings Management**: Controls render scale, anti-aliasing, shadows, reflections, and more 13 | - **User-friendly UI**: Built-in settings panel for players to customize their experience 14 | - **Performance Monitoring**: Real-time FPS tracking and performance analysis 15 | - **Platform-specific Optimizations**: Detects and applies optimal settings for different devices 16 | - **Fully Customizable**: Extensive configuration options for developers 17 | 18 | ## Installation 19 | 20 | 1. Download or clone this repository 21 | 2. Copy the `addons/smart_graphics_settings` folder into your Godot project's `addons` directory 22 | 3. Enable the plugin in Project Settings → Plugins 23 | 24 | ## Quick Start 25 | 26 | 1. Add the SmartGraphicsSettings node to your main scene or use the autoload singleton 27 | 2. Configure your desired target FPS and adjustment settings 28 | 3. Run your game - graphics will automatically adjust to maintain performance 29 | 30 | ## Access the SmartGraphicsSettings singleton directly 31 | 32 | ### Enable or disable adaptive graphics 33 | SmartGraphicsSettings.set_enabled(true) 34 | 35 | ### Set target FPS 36 | SmartGraphicsSettings.set_target_fps(60) 37 | 38 | ### Show the settings UI 39 | SmartGraphicsSettings.toggle_ui() 40 | 41 | ## Important Note on Initialization 42 | 43 | When accessing `SmartGraphicsSettings` properties like `adaptive_graphics` or calling functions like `set_target_fps` immediately in your script's `_ready()` function, you might encounter issues because the addon performs some initialization steps deferred (after `_ready()` has finished). 44 | 45 | To ensure the system is fully initialized before you interact with it at startup, connect to the `SmartGraphicsSettings.initialized` signal: 46 | 47 | ```gdscript 48 | func _ready(): 49 | if SmartGraphicsSettings.get_adaptive_graphics(): # Check if already initialized 50 | _on_graphics_ready() 51 | else: 52 | SmartGraphicsSettings.initialized.connect(_on_graphics_ready) 53 | 54 | func _on_graphics_ready(): 55 | # Now it's safe to access SmartGraphicsSettings functions and properties 56 | print("Smart Graphics Settings Ready!") 57 | SmartGraphicsSettings.set_target_fps(90) 58 | ``` 59 | 60 | See the [technical documentation](addons/smart_graphics_settings/README.md#handling-initialization-timing) for more details. 61 | 62 | ## Demo 63 | 64 | A demo scene is included to showcase the functionality. Open `addons/smart_graphics_settings/demo/demo_scene.tscn` to try it out. 65 | 66 | ## Documentation 67 | 68 | For detailed documentation on all features and configuration options, see the [README](addons/smart_graphics_settings/README.md) in the addon directory. 69 | 70 | ## License 71 | 72 | This project is licensed under the MIT License - see the [LICENSE](addons/smart_graphics_settings/LICENSE) file for details. 73 | 74 | ## Credits 75 | 76 | Created by Lucas Becker. Open to community-submitted issues and pull requests. 77 | 78 | ## Support 79 | 80 | If you find this plugin useful, please consider: 81 | 82 | - Starring the repository on GitHub 83 | - Contributing to the project 84 | - Reporting any issues you encounter 85 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lucas Becker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/README.md: -------------------------------------------------------------------------------- 1 | # Smart Graphics Settings - Technical Documentation 2 | 3 | This document provides detailed technical information about the Smart Graphics Settings addon for Godot 4.4, including its architecture, available settings, and API reference. 4 | 5 | ## Architecture 6 | 7 | The addon consists of several key components: 8 | 9 | 1. **SmartGraphicsSettings** (`smart_graphics_settings.gd`): The main controller that manages the UI and serves as the entry point. 10 | 2. **AdaptiveGraphics** (`adaptive_graphics.gd`): Handles the automatic adjustment of graphics settings based on performance. 11 | 3. **GraphicsSettingsManager** (`graphics_settings_manager.gd`): Manages all available graphics settings and their application. 12 | 4. **FPSMonitor** (`fps_monitor.gd`): Monitors and analyzes framerate performance. 13 | 5. **AdaptiveGraphicsUI** (`adaptive_graphics_ui.gd`): Provides the user interface for adjusting settings. 14 | 15 | ## Installation and Setup 16 | 17 | ### Manual Installation 18 | 19 | 1. Copy the `addons/smart_graphics_settings` folder to your project's `addons` directory 20 | 2. Enable the plugin in Project Settings → Plugins 21 | 22 | ### Autoload Singleton (Recommended) 23 | 24 | The plugin automatically registers a singleton named `SmartGraphicsSettings`. To use it: 25 | 26 | ```gdscript 27 | SmartGraphicsSettings 28 | ``` 29 | 30 | ### Manual Node Addition 31 | 32 | You can also add the SmartGraphicsSettings node manually to any scene: 33 | 34 | ```gdscript 35 | var settings_node: Node = preload("res://addons/smart_graphics_settings/smart_graphics_settings.gd").new() 36 | add_child(settings_node) 37 | ``` 38 | 39 | ## Configuration Options 40 | 41 | ### AdaptiveGraphics Properties 42 | 43 | | Property | Type | Description | 44 | |----------|------|-------------| 45 | | `target_fps` | `int` | Target framerate to maintain (default: 60) | 46 | | `fps_tolerance` | `int` | Acceptable FPS range around target (default: 5) | 47 | | `adjustment_cooldown` | `float` | Seconds between adjustments (default: 3.0) | 48 | | `measurement_period` | `float` | Seconds to measure FPS before adjusting (default: 2.0) | 49 | | `enabled` | `bool` | Whether adaptive graphics is enabled (default: true) | 50 | | `allow_quality_increase` | `bool` | Whether to increase quality when FPS is high (default: false) | 51 | | `use_threading` | `bool` | Whether to use threading for adjustments (default: true) | 52 | | `setting_change_delay` | `float` | Seconds between applying each setting change (default: 0.5) | 53 | | `match_refresh_rate` | `bool` | Whether to match target FPS to display refresh rate (default: false) | 54 | 55 | ### Available Graphics Settings 56 | 57 | The following settings can be adjusted automatically or manually: 58 | 59 | #### Render Scale 60 | 61 | - Controls the internal rendering resolution 62 | - Priority: Highest (adjusted first) 63 | - Values: 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 1.0 64 | 65 | #### Anti-Aliasing 66 | 67 | - Controls the anti-aliasing method 68 | - Priority: High 69 | - Values: Off, FXAA, MSAA 2x, MSAA 4x, MSAA 8x, TAA 70 | 71 | #### Shadows 72 | 73 | - Controls shadow quality and resolution 74 | - Priority: Medium 75 | - Values: Off, Low, Medium, High, Ultra 76 | 77 | #### Reflections 78 | 79 | - Controls screen-space reflections quality 80 | - Priority: Low 81 | - Values: Off, Low, Medium, High 82 | 83 | #### Global Illumination 84 | 85 | - Controls global illumination method and quality 86 | - Priority: Lowest (adjusted last) 87 | - Values: Off, Low, Medium, High 88 | 89 | ## API Reference 90 | 91 | ### SmartGraphicsSettings 92 | 93 | ```gdscript 94 | # Signal emitted when the AdaptiveGraphics node has been initialized. 95 | # Connect to this signal to safely access adaptive_graphics after startup. 96 | signal initialized 97 | 98 | # Toggle the settings UI visibility 99 | func toggle_ui() -> void 100 | 101 | # Apply a specific quality preset (1-5) 102 | func apply_quality_preset(preset_index: int) -> void 103 | 104 | # Save current settings to config file 105 | func save_settings() -> void 106 | 107 | # Load settings from config file 108 | func load_settings() -> void 109 | 110 | # Get the adaptive graphics controller 111 | func get_adaptive_graphics() -> AdaptiveGraphics 112 | ``` 113 | 114 | ### AdaptiveGraphics 115 | 116 | ```gdscript 117 | # Signal emitted when settings change 118 | signal changed_settings 119 | 120 | # Start the adaptive graphics system 121 | func start() -> void 122 | 123 | # Stop the adaptive graphics system 124 | func stop() -> void 125 | 126 | # Force an immediate adjustment 127 | func force_adjustment() -> void 128 | 129 | # Get the current action being performed 130 | func get_current_action() -> String 131 | 132 | # Set a specific setting value 133 | func set_setting(setting_name: String, value_index: int) -> void 134 | 135 | # Get the current value of a setting 136 | func get_setting(setting_name: String) -> int 137 | ``` 138 | 139 | ### GraphicsSettingsManager 140 | 141 | ```gdscript 142 | # Apply a specific quality preset (1-5) 143 | func apply_preset(preset_index: int) -> void 144 | 145 | # Get all available settings 146 | func get_available_settings() -> Dictionary 147 | 148 | # Get the current value of a setting 149 | func get_setting_value(setting_name: String) -> Variant 150 | 151 | # Set a specific setting value 152 | func set_setting_value(setting_name: String, value_index: int) -> void 153 | 154 | # Register a custom setting 155 | func register_setting(setting_name: String, values: Array[Variant], 156 | current_index: int, priority: SettingPriority, 157 | type: SettingType) -> void 158 | ``` 159 | 160 | ## Custom Settings 161 | 162 | You can register custom settings to be managed by the system: 163 | 164 | ```gdscript 165 | # Access SmartGraphicsSettings singleton directly 166 | # No need for get_node() as it's registered as an autoload 167 | var settings_manager = SmartGraphicsSettings.adaptive_graphics.settings_manager 168 | 169 | # Register a custom setting 170 | var values: Array[Variant] = [false, true] 171 | settings_manager.register_setting( 172 | "my_custom_setting", 173 | values, 174 | 1, # Current index (true) 175 | GraphicsSettingsManager.SettingPriority.POST_PROCESSING, 176 | GraphicsSettingsManager.SettingType.ENVIRONMENT 177 | ) 178 | 179 | # Connect to the changed_settings signal 180 | SmartGraphicsSettings.adaptive_graphics.changed_settings.connect(_on_settings_changed) 181 | 182 | func _on_settings_changed() -> void: 183 | # Handle custom setting changes 184 | var custom_value = settings_manager.get_setting_value("my_custom_setting") 185 | print("Custom setting value: ", custom_value) 186 | ``` 187 | 188 | ## Handling Initialization Timing 189 | 190 | Due to Godot's node initialization order and the use of `call_deferred` within the addon, the `SmartGraphicsSettings.adaptive_graphics` instance might not be immediately available when accessed from another node's `_ready()` function. Trying to access it too early can result in getting a `null` value. 191 | 192 | To safely access `adaptive_graphics` and its properties or methods at startup, you should connect to the `initialized` signal emitted by the `SmartGraphicsSettings` singleton. This signal is emitted once the internal `AdaptiveGraphics` node is ready. 193 | 194 | **Example:** 195 | 196 | ```gdscript 197 | # In a script that needs to customize settings (e.g., attached to your main scene root) 198 | 199 | func _ready(): 200 | # Check if the singleton and its property are already available (e.g., if the scene is reloaded) 201 | if SmartGraphicsSettings != null and SmartGraphicsSettings.adaptive_graphics != null: 202 | _on_smart_graphics_ready() 203 | elif SmartGraphicsSettings != null: 204 | # Connect to the signal if not yet ready 205 | SmartGraphicsSettings.initialized.connect(_on_smart_graphics_ready) 206 | else: 207 | push_error("SmartGraphicsSettings singleton not found!") 208 | 209 | func _on_smart_graphics_ready(): 210 | # Now it's safe to access adaptive_graphics 211 | if SmartGraphicsSettings.adaptive_graphics: 212 | print("SmartGraphicsSettings is ready! Customizing...") 213 | # Example: Set a custom target FPS 214 | SmartGraphicsSettings.set_target_fps(90) 215 | # Apply other custom settings here using SmartGraphicsSettings wrapper functions 216 | # or directly via SmartGraphicsSettings.adaptive_graphics if needed. 217 | else: 218 | push_error("Failed to get adaptive_graphics even after initialized signal.") 219 | 220 | ``` 221 | 222 | ## Troubleshooting 223 | 224 | ### Performance Issues 225 | 226 | - If you experience stuttering during adjustments, try setting `use_threading` to false 227 | - Increase `adjustment_cooldown` to reduce the frequency of adjustments 228 | 229 | ### UI Issues 230 | 231 | - If the UI doesn't appear, check that the input action `toggle_graphics_settings` is properly registered 232 | - The default key to toggle the UI is F7 233 | - If you encounter errors related to `adaptive_graphics` being `null` at startup, ensure you are waiting for the `initialized` signal as described in the "Handling Initialization Timing" section. 234 | 235 | ### Custom Renderer Support 236 | 237 | - For custom renderers, you may need to manually register compatible settings 238 | - Use the `register_setting` method to add renderer-specific settings 239 | 240 | ## Best Practices 241 | 242 | 1. **Initialize Early**: Configure the addon's parameters (e.g., via Project Settings if applicable, or saved configuration files) before the game starts intensive rendering. For runtime access to the `adaptive_graphics` object from your scripts at startup, wait for the `SmartGraphicsSettings.initialized` signal. 243 | 2. **Default Presets**: Provide sensible default presets for different hardware capabilities 244 | 3. **Save User Preferences**: Always save and restore user settings between sessions 245 | 4. **Testing**: Test on various hardware configurations to ensure proper adaptation 246 | 5. **Feedback**: Provide visual feedback when settings are being adjusted automatically 247 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/adaptive_graphics.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg") 3 | class_name AdaptiveGraphics 4 | extends Node 5 | 6 | ## Signal emitted when graphics settings are changed 7 | signal changed_settings 8 | 9 | ## Target FPS to maintain 10 | @export var target_fps: int = 60 11 | 12 | ## Allow FPS to be within this range of target 13 | @export var fps_tolerance: int = 5 14 | 15 | ## Seconds between adjustments 16 | @export var adjustment_cooldown: float = 3.0 17 | 18 | ## Seconds to measure FPS before adjusting 19 | @export var measurement_period: float = 2.0 20 | 21 | ## Whether adaptive graphics is enabled 22 | @export var enabled: bool = true 23 | 24 | ## Whether to increase quality when FPS is high 25 | @export var allow_quality_increase: bool = false 26 | 27 | ## Whether to use threading for adjustments 28 | @export var use_threading: bool = true 29 | 30 | ## Seconds between applying each setting change 31 | @export var setting_change_delay: float = 0.5 32 | 33 | ## Whether to automatically set target FPS to match display refresh rate 34 | @export var match_refresh_rate: bool = false 35 | 36 | ## Internal state 37 | var fps_monitor: FPSMonitor 38 | var settings_manager: GraphicsSettingsManager 39 | var cooldown_timer: float = 0.0 40 | var measurement_timer: float = 0.0 41 | var is_measuring: bool = false 42 | var is_adjusting: bool = false 43 | var settings_changed: bool = false 44 | var threading_supported: bool = false 45 | var current_vsync_mode: int = -1 46 | var display_refresh_rate: float = 60.0 47 | var _current_action: String = "Idle" # Internal current action storage 48 | 49 | ## Threading components 50 | var adjustment_thread: Thread 51 | var adjustment_mutex: Mutex 52 | var thread_exit: bool = false 53 | var pending_adjustments: Array[Dictionary] = [] 54 | var adjustment_timer: Timer 55 | 56 | ## Platform information for debugging 57 | var platform_info: Dictionary = {} 58 | 59 | ## Presets for quick configuration 60 | enum QualityPreset { 61 | ULTRA_LOW, 62 | LOW, 63 | MEDIUM, 64 | HIGH, 65 | ULTRA 66 | } 67 | 68 | ## Preset configurations 69 | var presets: Dictionary[int, Dictionary] = { 70 | QualityPreset.ULTRA_LOW: { 71 | "render_scale": 0, # 0.5 72 | "msaa": 0, # Disabled 73 | "shadow_quality": 0, # Disabled 74 | "shadow_size": 0, # 1024 75 | "fxaa": 0, # Disabled 76 | "ssao": 0, # Disabled 77 | "ssr": 0, # Disabled 78 | "sdfgi": 0, # Disabled 79 | "glow": 0, # Disabled 80 | "volumetric_fog": 0, # Disabled 81 | "dof": 0, # Disabled 82 | "motion_blur": 0 # Disabled 83 | }, 84 | QualityPreset.LOW: { 85 | "render_scale": 1, # 0.6 86 | "msaa": 0, # Disabled 87 | "shadow_quality": 1, # Low 88 | "shadow_size": 0, # 1024 89 | "fxaa": 1, # Enabled 90 | "ssao": 0, # Disabled 91 | "ssr": 0, # Disabled 92 | "sdfgi": 0, # Disabled 93 | "glow": 1, # Enabled 94 | "volumetric_fog": 0, # Disabled 95 | "dof": 0, # Disabled 96 | "motion_blur": 0 # Disabled 97 | }, 98 | QualityPreset.MEDIUM: { 99 | "render_scale": 3, # 0.8 100 | "msaa": 1, # 2X 101 | "shadow_quality": 2, # Medium 102 | "shadow_size": 1, # 2048 103 | "fxaa": 1, # Enabled 104 | "ssao": 1, # Enabled 105 | "ssao_quality": 1, # Low 106 | "ssr": 0, # Disabled 107 | "sdfgi": 0, # Disabled 108 | "glow": 1, # Enabled 109 | "volumetric_fog": 0, # Disabled 110 | "dof": 1, # Enabled 111 | "motion_blur": 0 # Disabled 112 | }, 113 | QualityPreset.HIGH: { 114 | "render_scale": 4, # 0.9 115 | "msaa": 2, # 4X 116 | "shadow_quality": 3, # High 117 | "shadow_size": 2, # 4096 118 | "fxaa": 1, # Enabled 119 | "ssao": 1, # Enabled 120 | "ssao_quality": 2, # Medium 121 | "ssr": 1, # Enabled 122 | "ssr_max_steps": 2, # 32 123 | "sdfgi": 1, # Enabled 124 | "glow": 1, # Enabled 125 | "volumetric_fog": 1, # Enabled 126 | "volumetric_fog_density": 2, # 0.03 127 | "dof": 1, # Enabled 128 | "motion_blur": 1 # Enabled 129 | }, 130 | QualityPreset.ULTRA: { 131 | "render_scale": 5, # 1.0 132 | "msaa": 3, # 8X 133 | "shadow_quality": 4, # Ultra 134 | "shadow_size": 3, # 8192 135 | "fxaa": 1, # Enabled 136 | "ssao": 1, # Enabled 137 | "ssao_quality": 3, # High 138 | "ssr": 1, # Enabled 139 | "ssr_max_steps": 3, # 64 140 | "sdfgi": 1, # Enabled 141 | "glow": 1, # Enabled 142 | "volumetric_fog": 1, # Enabled 143 | "volumetric_fog_density": 3, # 0.05 144 | "dof": 1, # Enabled 145 | "motion_blur": 1 # Enabled 146 | } 147 | } 148 | 149 | func _init() -> void: 150 | # Initialize current action 151 | current_action = "Initializing" 152 | 153 | # Gather platform information for better debugging 154 | platform_info = { 155 | "os_name": OS.get_name(), 156 | "model_name": OS.get_model_name(), 157 | "processor_name": OS.get_processor_name(), 158 | "processor_count": OS.get_processor_count() 159 | } 160 | 161 | # Check if threading is supported on this platform using multiple methods 162 | var has_threads_feature: bool = OS.has_feature("threads") 163 | var has_multiple_processors: bool = OS.get_processor_count() > 1 164 | var is_web_platform: bool = OS.has_feature("web") 165 | 166 | # Determine threading support based on platform and features 167 | threading_supported = has_threads_feature and not is_web_platform 168 | 169 | # Additional platform-specific checks 170 | if OS.get_name() == "Web": 171 | threading_supported = false 172 | print_debug("Smart Graphics Settings: Threading disabled on Web platform") 173 | elif OS.get_name() == "Android" or OS.get_name() == "iOS": 174 | # Mobile platforms may have threading limitations 175 | threading_supported = has_threads_feature and has_multiple_processors 176 | print_debug("Smart Graphics Settings: Mobile platform detected, threading support: ", threading_supported) 177 | 178 | # Apply mobile-specific optimizations 179 | fps_tolerance = 8 # Allow more variation on mobile 180 | adjustment_cooldown = 5.0 # Less frequent adjustments to save battery 181 | measurement_period = 3.0 # Longer measurement period for more stable readings 182 | elif OS.get_name() == "Windows" or OS.get_name() == "macOS" or OS.get_name() == "Linux": 183 | # Desktop platforms can use more precise settings 184 | fps_tolerance = 3 185 | adjustment_cooldown = 2.0 186 | measurement_period = 1.5 187 | 188 | # Default to threaded mode if supported 189 | use_threading = threading_supported 190 | 191 | # Log threading support status 192 | print_debug("Smart Graphics Settings: Threading support detected: ", threading_supported) 193 | if threading_supported: 194 | print_debug("Smart Graphics Settings: Using ", OS.get_processor_count(), " processors") 195 | else: 196 | print_debug("Smart Graphics Settings: Threading disabled, using single-threaded mode") 197 | 198 | # Set current action to idle after initialization 199 | current_action = "Idle" 200 | 201 | func _ready() -> void: 202 | if not Engine.is_editor_hint(): 203 | current_action = "Setting Up" 204 | 205 | # Initialize FPS monitor 206 | fps_monitor = FPSMonitor.new() 207 | add_child(fps_monitor) 208 | 209 | # Setup threading if supported 210 | if threading_supported and use_threading: 211 | setup_threading() 212 | 213 | # Get the current VSync mode and display refresh rate 214 | update_vsync_and_refresh_rate() 215 | 216 | # If match_refresh_rate is enabled, set target FPS to match display refresh rate 217 | if match_refresh_rate: 218 | target_fps = int(display_refresh_rate) 219 | if target_fps <= 0: # Fallback if we couldn't get the refresh rate 220 | target_fps = 60 221 | 222 | settings_manager = GraphicsSettingsManager.new() 223 | add_child(settings_manager) 224 | 225 | # Try to load saved settings 226 | settings_manager.load_graphics_settings() 227 | 228 | # Create timer for staggered setting application 229 | adjustment_timer = Timer.new() 230 | adjustment_timer.one_shot = true 231 | adjustment_timer.timeout.connect(_on_adjustment_timer_timeout) 232 | add_child(adjustment_timer) 233 | 234 | current_action = "Ready" 235 | 236 | # Set to Idle after a short delay to allow UI to update 237 | await get_tree().create_timer(0.5).timeout 238 | current_action = "Idle" 239 | 240 | func _exit_tree() -> void: 241 | # Clean up threading resources 242 | if threading_supported and use_threading and adjustment_thread != null and adjustment_thread.is_started(): 243 | thread_exit = true 244 | adjustment_thread.wait_to_finish() 245 | 246 | func setup_threading() -> void: 247 | # Create mutex for thread synchronization 248 | adjustment_mutex = Mutex.new() 249 | 250 | # Create timer for delayed setting changes 251 | adjustment_timer = Timer.new() 252 | adjustment_timer.wait_time = setting_change_delay 253 | adjustment_timer.one_shot = true 254 | adjustment_timer.timeout.connect(_on_adjustment_timer_timeout) 255 | add_child(adjustment_timer) 256 | 257 | # Start adjustment thread 258 | thread_exit = false 259 | adjustment_thread = Thread.new() 260 | var thread_start_error = adjustment_thread.start(thread_function) 261 | 262 | if thread_start_error != OK: 263 | push_error("Smart Graphics Settings: Failed to start adjustment thread. Error code: ", thread_start_error) 264 | threading_supported = false 265 | use_threading = false 266 | print_debug("Smart Graphics Settings: Falling back to single-threaded mode") 267 | else: 268 | print_debug("Smart Graphics Settings: Adjustment thread started successfully") 269 | 270 | func _thread_evaluate_performance() -> void: 271 | # Get a thread-safe copy of FPS data 272 | var avg_fps: float = fps_monitor.get_average_fps() 273 | var is_stable: bool = fps_monitor.is_fps_stable() 274 | 275 | # Reset measuring flag with proper locking 276 | adjustment_mutex.lock() 277 | is_measuring = false 278 | var current_target_fps: int = target_fps 279 | var current_fps_tolerance: int = fps_tolerance 280 | var current_allow_increase: bool = allow_quality_increase 281 | adjustment_mutex.unlock() 282 | 283 | # Get the effective maximum FPS based on VSync settings 284 | var max_fps: float = get_effective_max_fps() 285 | var effective_target: int = current_target_fps 286 | 287 | # If VSync is limiting our FPS and our target is higher, adjust the target 288 | if max_fps > 0 and current_target_fps > max_fps: 289 | effective_target = int(max_fps) 290 | 291 | # Only adjust if FPS is stable (to avoid reacting to temporary spikes) 292 | if is_stable: 293 | if avg_fps < effective_target - current_fps_tolerance: 294 | queue_quality_decrease() 295 | 296 | adjustment_mutex.lock() 297 | cooldown_timer = adjustment_cooldown 298 | adjustment_mutex.unlock() 299 | 300 | call_deferred("set_settings_changed", true) 301 | elif current_allow_increase and avg_fps > effective_target + current_fps_tolerance * 2: 302 | queue_quality_increase() 303 | 304 | adjustment_mutex.lock() 305 | cooldown_timer = adjustment_cooldown * 2 # Longer cooldown for increases 306 | adjustment_mutex.unlock() 307 | 308 | call_deferred("set_settings_changed", true) 309 | else: 310 | # If FPS is not stable, just reset the measuring state 311 | adjustment_mutex.lock() 312 | is_adjusting = false 313 | adjustment_mutex.unlock() 314 | 315 | ## Helper function to safely set settings_changed from thread 316 | func set_settings_changed(value: bool) -> void: 317 | adjustment_mutex.lock() 318 | settings_changed = value 319 | adjustment_mutex.unlock() 320 | 321 | if value: 322 | settings_manager.save_graphics_settings() 323 | 324 | func _process(delta: float) -> void: 325 | # Skip processing in editor 326 | if Engine.is_editor_hint() or not enabled: 327 | return 328 | 329 | # Update timers with proper locking 330 | adjustment_mutex.lock() 331 | if cooldown_timer > 0: 332 | cooldown_timer -= delta 333 | 334 | var current_cooldown: float = cooldown_timer 335 | var current_is_measuring: bool = is_measuring 336 | var current_is_adjusting: bool = is_adjusting 337 | var current_use_threading: bool = use_threading 338 | var current_threading_supported: bool = threading_supported 339 | adjustment_mutex.unlock() 340 | 341 | if not current_use_threading or not current_threading_supported: 342 | # Synchronous mode 343 | if current_is_measuring: 344 | adjustment_mutex.lock() 345 | measurement_timer -= delta 346 | var current_measurement_timer: float = measurement_timer 347 | adjustment_mutex.unlock() 348 | 349 | if current_measurement_timer <= 0: 350 | adjustment_mutex.lock() 351 | is_measuring = false 352 | adjustment_mutex.unlock() 353 | evaluate_performance() 354 | elif current_cooldown <= 0 and not current_is_adjusting: 355 | # Start a new measurement period 356 | start_measurement() 357 | else: 358 | # Threaded mode - just start measurement when ready 359 | if current_cooldown <= 0 and not current_is_measuring and not current_is_adjusting: 360 | # Check if there are pending adjustments 361 | adjustment_mutex.lock() 362 | var has_pending: bool = not pending_adjustments.is_empty() 363 | adjustment_mutex.unlock() 364 | 365 | if not has_pending: 366 | start_measurement() 367 | 368 | func start_measurement() -> void: 369 | fps_monitor.clear_history() 370 | 371 | adjustment_mutex.lock() 372 | measurement_timer = measurement_period 373 | is_measuring = true 374 | current_action = "Measuring Performance..." 375 | adjustment_mutex.unlock() 376 | 377 | func evaluate_performance() -> void: 378 | current_action = "Analyzing Performance..." 379 | 380 | var avg_fps: float = fps_monitor.get_average_fps() 381 | var is_stable: bool = fps_monitor.is_fps_stable() 382 | 383 | # Get the effective maximum FPS based on VSync settings 384 | var max_fps = get_effective_max_fps() 385 | var effective_target = target_fps 386 | 387 | # If VSync is limiting our FPS and our target is higher, adjust the target 388 | if max_fps > 0 and target_fps > max_fps: 389 | effective_target = max_fps 390 | 391 | # Only adjust if FPS is stable (to avoid reacting to temporary spikes) 392 | if is_stable: 393 | if avg_fps < effective_target - fps_tolerance: 394 | decrease_quality() 395 | cooldown_timer = adjustment_cooldown 396 | settings_changed = true 397 | elif allow_quality_increase and avg_fps > effective_target + fps_tolerance * 2: 398 | increase_quality() 399 | cooldown_timer = adjustment_cooldown * 2 # Longer cooldown for increases 400 | settings_changed = true 401 | 402 | # Save settings if they've changed 403 | if settings_changed: 404 | settings_manager.save_graphics_settings() 405 | settings_changed = false 406 | 407 | current_action = "Performance Analysis Complete" 408 | await get_tree().create_timer(1.0).timeout 409 | current_action = "Monitoring Performance" 410 | 411 | func decrease_quality() -> void: 412 | is_adjusting = true 413 | current_action = "Optimizing Performance..." 414 | 415 | # Try to decrease quality of settings in priority order 416 | for setting_name in settings_manager.get_settings_by_priority(): 417 | if settings_manager.decrease_setting_quality(setting_name): 418 | print("Decreased quality of ", setting_name, " to maintain target FPS") 419 | current_action = "Decreased " + setting_name + " Quality" 420 | is_adjusting = false 421 | return 422 | 423 | is_adjusting = false 424 | current_action = "Monitoring Performance" 425 | 426 | func increase_quality() -> void: 427 | is_adjusting = true 428 | current_action = "Improving Visual Quality..." 429 | 430 | # Try to increase quality of settings in reverse priority order 431 | var settings: Array[String] = settings_manager.get_settings_by_priority() 432 | settings.reverse() 433 | 434 | for setting_name in settings: 435 | if settings_manager.increase_setting_quality(setting_name): 436 | print("Increased quality of ", setting_name, " as performance allows") 437 | current_action = "Increased " + setting_name + " Quality" 438 | is_adjusting = false 439 | return 440 | 441 | is_adjusting = false 442 | current_action = "Monitoring Performance" 443 | 444 | ## Queue a quality decrease for threaded processing 445 | func queue_quality_decrease() -> void: 446 | adjustment_mutex.lock() 447 | is_adjusting = true 448 | current_action = "Preparing Performance Optimization..." 449 | adjustment_mutex.unlock() 450 | 451 | # Try to decrease quality of settings in priority order 452 | for setting_name in settings_manager.get_settings_by_priority(): 453 | var setting: GraphicsSettingsManager.Setting = settings_manager.available_settings[setting_name] 454 | if setting.current_index > 0: 455 | var new_index: int = setting.current_index - 1 456 | 457 | adjustment_mutex.lock() 458 | pending_adjustments.append({ 459 | "setting_name": setting_name, 460 | "index": new_index, 461 | "is_decrease": true 462 | }) 463 | adjustment_mutex.unlock() 464 | 465 | # Start processing the queue if not already processing 466 | call_deferred("process_next_adjustment") 467 | 468 | print("Queued decrease of quality for ", setting_name) 469 | return 470 | 471 | adjustment_mutex.lock() 472 | is_adjusting = false 473 | current_action = "Monitoring Performance" 474 | adjustment_mutex.unlock() 475 | 476 | ## Queue a quality increase for threaded processing 477 | func queue_quality_increase() -> void: 478 | adjustment_mutex.lock() 479 | is_adjusting = true 480 | current_action = "Preparing Quality Improvement..." 481 | adjustment_mutex.unlock() 482 | 483 | # Try to increase quality of settings in reverse priority order 484 | var settings: Array[String] = settings_manager.get_settings_by_priority() 485 | settings.reverse() 486 | 487 | for setting_name in settings: 488 | var setting: GraphicsSettingsManager.Setting = settings_manager.available_settings[setting_name] 489 | if setting.current_index < setting.values.size() - 1: 490 | var new_index: int = setting.current_index + 1 491 | 492 | adjustment_mutex.lock() 493 | pending_adjustments.append({ 494 | "setting_name": setting_name, 495 | "index": new_index, 496 | "is_decrease": false 497 | }) 498 | adjustment_mutex.unlock() 499 | 500 | # Start processing the queue if not already processing 501 | call_deferred("process_next_adjustment") 502 | 503 | print("Queued increase of quality for ", setting_name) 504 | return 505 | 506 | adjustment_mutex.lock() 507 | is_adjusting = false 508 | current_action = "Monitoring Performance" 509 | adjustment_mutex.unlock() 510 | 511 | ## Process the next adjustment in the queue 512 | func process_next_adjustment() -> void: 513 | adjustment_mutex.lock() 514 | var has_adjustments: bool = not pending_adjustments.is_empty() 515 | var adjustment: Dictionary = {} 516 | 517 | if has_adjustments: 518 | adjustment = pending_adjustments[0] 519 | pending_adjustments.remove_at(0) 520 | 521 | adjustment_mutex.unlock() 522 | 523 | if has_adjustments: 524 | var setting_name: String = adjustment.setting_name 525 | var new_index: int = adjustment.index 526 | var is_decrease: bool = adjustment.is_decrease 527 | 528 | # Update current action 529 | adjustment_mutex.lock() 530 | if is_decrease: 531 | current_action = "Decreasing " + setting_name + " Quality..." 532 | else: 533 | current_action = "Increasing " + setting_name + " Quality..." 534 | adjustment_mutex.unlock() 535 | 536 | # Apply the setting change 537 | settings_manager.available_settings[setting_name].current_index = new_index 538 | settings_manager.apply_setting(setting_name) 539 | 540 | # Emit the changed_settings signal 541 | emit_signal("changed_settings") 542 | 543 | if is_decrease: 544 | print("Decreased quality of ", setting_name, " to maintain target FPS") 545 | else: 546 | print("Increased quality of ", setting_name, " as performance allows") 547 | 548 | # Schedule the next adjustment after a delay 549 | adjustment_mutex.lock() 550 | var has_more: bool = not pending_adjustments.is_empty() 551 | adjustment_mutex.unlock() 552 | 553 | if has_more: 554 | adjustment_timer.start(setting_change_delay) 555 | else: 556 | # No more adjustments, reset state and start cooldown 557 | adjustment_mutex.lock() 558 | is_adjusting = false 559 | current_action = "Monitoring Performance" 560 | cooldown_timer = adjustment_cooldown 561 | adjustment_mutex.unlock() 562 | 563 | ## Timer callback for staggered setting application 564 | func _on_adjustment_timer_timeout() -> void: 565 | process_next_adjustment() 566 | 567 | ## Apply a quality preset 568 | func apply_preset(preset: QualityPreset) -> void: 569 | if not presets.has(preset): 570 | return 571 | 572 | var preset_settings: Dictionary = get_preset_for_current_renderer(preset) 573 | 574 | # Stop any ongoing adjustments 575 | if use_threading and threading_supported: 576 | adjustment_mutex.lock() 577 | pending_adjustments.clear() 578 | is_adjusting = true 579 | current_action = "Applying " + QualityPreset.keys()[preset] + " Quality Preset..." 580 | adjustment_mutex.unlock() 581 | else: 582 | is_adjusting = true 583 | current_action = "Applying " + QualityPreset.keys()[preset] + " Quality Preset..." 584 | 585 | # Apply all preset settings 586 | for setting_name in preset_settings: 587 | if settings_manager.available_settings.has(setting_name) and settings_manager.is_setting_applicable(setting_name): 588 | settings_manager.available_settings[setting_name].current_index = preset_settings[setting_name] 589 | settings_manager.apply_setting(setting_name) 590 | 591 | settings_changed = true 592 | settings_manager.save_graphics_settings() 593 | 594 | # Emit the changed_settings signal 595 | emit_signal("changed_settings") 596 | 597 | # Reset state 598 | if use_threading and threading_supported: 599 | adjustment_mutex.lock() 600 | is_adjusting = false 601 | current_action = "Applied " + QualityPreset.keys()[preset] + " Quality Preset" 602 | adjustment_mutex.unlock() 603 | else: 604 | is_adjusting = false 605 | current_action = "Applied " + QualityPreset.keys()[preset] + " Quality Preset" 606 | 607 | # Show the completion message briefly before returning to monitoring 608 | await get_tree().create_timer(1.0).timeout 609 | current_action = "Monitoring Performance" 610 | 611 | cooldown_timer = adjustment_cooldown 612 | print("Applied preset: ", QualityPreset.keys()[preset]) 613 | 614 | ## Get a preset adjusted for the current renderer 615 | func get_preset_for_current_renderer(preset_type: QualityPreset) -> Dictionary: 616 | var base_preset: Dictionary = presets[preset_type].duplicate() 617 | var renderer_type: GraphicsSettingsManager.RendererType = settings_manager.current_renderer 618 | 619 | # Filter out settings that don't apply to the current renderer 620 | var filtered_preset: Dictionary = {} 621 | for setting_name in base_preset: 622 | if settings_manager.is_setting_applicable(setting_name): 623 | filtered_preset[setting_name] = base_preset[setting_name] 624 | 625 | return filtered_preset 626 | 627 | ## Toggle threading mode 628 | func set_threading_enabled(enabled: bool) -> void: 629 | if enabled == use_threading: 630 | return 631 | 632 | if enabled and not threading_supported: 633 | push_warning("Smart Graphics Settings: Threading is not supported on this platform") 634 | return 635 | 636 | use_threading = enabled 637 | 638 | # If enabling threading and it's supported, set up the thread 639 | if use_threading and threading_supported: 640 | if adjustment_thread == null or not adjustment_thread.is_started(): 641 | setup_threading() 642 | # If disabling threading, clean up the thread 643 | elif adjustment_thread != null and adjustment_thread.is_started(): 644 | cleanup_threading() 645 | 646 | ## Updates the current VSync mode and display refresh rate 647 | func update_vsync_and_refresh_rate() -> void: 648 | current_vsync_mode = DisplayServer.window_get_vsync_mode() 649 | display_refresh_rate = DisplayServer.screen_get_refresh_rate() 650 | if display_refresh_rate <= 0: # Fallback if we couldn't get the refresh rate 651 | display_refresh_rate = 60.0 652 | 653 | ## Get the effective maximum FPS based on VSync settings 654 | func get_effective_max_fps() -> float: 655 | update_vsync_and_refresh_rate() 656 | 657 | # If VSync is enabled or adaptive, the max FPS is limited by the refresh rate 658 | if current_vsync_mode == DisplayServer.VSYNC_ENABLED: 659 | return display_refresh_rate 660 | elif current_vsync_mode == DisplayServer.VSYNC_ADAPTIVE: 661 | # For adaptive VSync, we can go below the refresh rate but not above 662 | return display_refresh_rate 663 | 664 | # For disabled VSync or mailbox, there's no upper limit 665 | return 0.0 # 0 means no limit 666 | 667 | ## Set the target FPS to match the display refresh rate 668 | func set_target_fps_to_refresh_rate() -> void: 669 | update_vsync_and_refresh_rate() 670 | target_fps = int(display_refresh_rate) 671 | if target_fps <= 0: # Fallback if we couldn't get the refresh rate 672 | target_fps = 60 673 | 674 | ## Set the VSync mode 675 | func set_vsync_mode(mode: int) -> void: 676 | DisplayServer.window_set_vsync_mode(mode) 677 | update_vsync_and_refresh_rate() 678 | 679 | # If match_refresh_rate is enabled, update the target FPS 680 | if match_refresh_rate: 681 | set_target_fps_to_refresh_rate() 682 | 683 | ## Get detailed threading support information 684 | func get_threading_support_info() -> Dictionary: 685 | return { 686 | "threading_supported": threading_supported, 687 | "use_threading": use_threading, 688 | "platform": OS.get_name(), 689 | "processor_count": OS.get_processor_count(), 690 | "has_threads_feature": OS.has_feature("threads"), 691 | "is_web_platform": OS.has_feature("web"), 692 | "thread_active": adjustment_thread != null and adjustment_thread.is_started() if threading_supported else false 693 | } 694 | 695 | ## Thread function for performance evaluation 696 | func thread_function() -> void: 697 | while not thread_exit: 698 | # Sleep to avoid busy waiting 699 | OS.delay_msec(100) 700 | 701 | # Check if we need to analyze performance 702 | var should_analyze: bool = false 703 | 704 | adjustment_mutex.lock() 705 | should_analyze = is_measuring and fps_monitor.fps_history.size() >= fps_monitor.history_size 706 | adjustment_mutex.unlock() 707 | 708 | if should_analyze: 709 | _thread_evaluate_performance() 710 | 711 | ## Clean up threading resources 712 | func cleanup_threading() -> void: 713 | if adjustment_thread != null and adjustment_thread.is_started(): 714 | # Signal thread to exit 715 | thread_exit = true 716 | # Wait for thread to finish 717 | adjustment_thread.wait_to_finish() 718 | adjustment_thread = null 719 | 720 | # Clean up mutex 721 | adjustment_mutex = null 722 | 723 | # Remove timer if it exists 724 | if adjustment_timer != null and adjustment_timer.is_inside_tree(): 725 | adjustment_timer.queue_free() 726 | adjustment_timer = null 727 | 728 | print_debug("Smart Graphics Settings: Threading resources cleaned up") 729 | 730 | ## Get the current action in a thread-safe way 731 | func get_current_action() -> String: 732 | if use_threading and threading_supported and adjustment_mutex != null: 733 | adjustment_mutex.lock() 734 | var action = _current_action 735 | adjustment_mutex.unlock() 736 | return action 737 | return _current_action 738 | 739 | ## Set the current action in a thread-safe way 740 | func set_current_action(action: String) -> void: 741 | if use_threading and threading_supported and adjustment_mutex != null: 742 | adjustment_mutex.lock() 743 | _current_action = action 744 | adjustment_mutex.unlock() 745 | else: 746 | _current_action = action 747 | 748 | ## Current action property 749 | var current_action: String: 750 | get: 751 | return get_current_action() 752 | set(value): 753 | set_current_action(value) 754 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/adaptive_graphics.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bmw8w2g48j3tt 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/adaptive_graphics_ui.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg") 3 | class_name AdaptiveGraphicsUI 4 | extends Control 5 | 6 | ## Path to the AdaptiveGraphics node 7 | @export var adaptive_graphics_path: NodePath = NodePath("") 8 | 9 | ## Reference to the AdaptiveGraphics node 10 | var adaptive_graphics: AdaptiveGraphics 11 | 12 | ## UI Controls 13 | @onready var target_fps_slider: HSlider = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/TargetFPSContainer/TargetFPSSlider 14 | @onready var target_fps_value: SpinBox = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/TargetFPSContainer/TargetFPSValue 15 | @onready var target_fps_reset: Button = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/TargetFPSContainer/ResetButton 16 | @onready var enabled_checkbox: CheckBox = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/EnabledCheckbox 17 | @onready var allow_increase_checkbox: CheckBox = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/AllowIncreaseCheckbox 18 | @onready var threading_checkbox: CheckBox = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/ThreadingCheckbox 19 | @onready var match_refresh_rate_checkbox: CheckBox = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/MatchRefreshRateCheckbox 20 | @onready var vsync_option: OptionButton = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/VSyncOption 21 | @onready var preset_option: OptionButton = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/PresetOption 22 | @onready var fps_label: Label = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/FPSLabel 23 | @onready var status_label: Label = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/StatusLabel 24 | @onready var renderer_label: Label = $CenterContainer/PanelContainer/MarginContainer/VBoxContent/RendererLabel 25 | 26 | ## Default target FPS value 27 | var default_target_fps: int = 60 28 | 29 | ## Timer for updating FPS display 30 | var update_timer: Timer 31 | 32 | ## Current quality preset (for UI feedback) 33 | var current_preset: String = "Custom" 34 | 35 | ## Current settings state (for detecting changes) 36 | var current_settings: Dictionary = {} 37 | 38 | func _ready() -> void: 39 | # If adaptive_graphics is already set directly, use it 40 | if adaptive_graphics: 41 | # Already set directly, no need to look up 42 | pass 43 | # Otherwise, try to get the AdaptiveGraphics node from the path 44 | elif not adaptive_graphics_path.is_empty(): 45 | adaptive_graphics = get_node_or_null(adaptive_graphics_path) 46 | 47 | # If we couldn't get it from the path, try to find it through the singleton 48 | if not adaptive_graphics and Engine.has_singleton("SmartGraphicsSettings"): 49 | # Access SmartGraphicsSettings directly as an autoload 50 | if SmartGraphicsSettings and SmartGraphicsSettings.has_method("get_adaptive_graphics"): 51 | adaptive_graphics = SmartGraphicsSettings.get_adaptive_graphics() 52 | 53 | if not adaptive_graphics: 54 | push_error("AdaptiveGraphicsUI: Failed to find AdaptiveGraphics node at path: " + str(adaptive_graphics_path)) 55 | return 56 | 57 | # Connect to the settings_changed signal 58 | if not adaptive_graphics.changed_settings.is_connected(_on_changed_settings): 59 | adaptive_graphics.changed_settings.connect(_on_changed_settings) 60 | 61 | # Store default target FPS 62 | default_target_fps = adaptive_graphics.target_fps 63 | 64 | # Initialize UI with current values 65 | target_fps_slider.value = adaptive_graphics.target_fps 66 | target_fps_value.value = adaptive_graphics.target_fps 67 | enabled_checkbox.button_pressed = adaptive_graphics.enabled 68 | allow_increase_checkbox.button_pressed = adaptive_graphics.allow_quality_increase 69 | 70 | # Setup threading checkbox 71 | threading_checkbox.button_pressed = adaptive_graphics.use_threading 72 | threading_checkbox.disabled = not adaptive_graphics.threading_supported 73 | if not adaptive_graphics.threading_supported: 74 | var platform_name: String = OS.get_name() 75 | threading_checkbox.tooltip_text = "Threading not supported on this platform (%s)" % platform_name 76 | 77 | # Add a small warning icon next to the checkbox if threading is not supported 78 | var warning_icon = TextureRect.new() 79 | warning_icon.texture = get_theme_icon("NodeWarning", "EditorIcons") 80 | warning_icon.tooltip_text = "Threading is not available on %s or has been disabled due to platform limitations. The extension will use single-threaded mode instead." % platform_name 81 | warning_icon.custom_minimum_size = Vector2(16, 16) 82 | warning_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED 83 | 84 | # Add the warning icon as a sibling to the checkbox 85 | var parent = threading_checkbox.get_parent() 86 | parent.add_child(warning_icon) 87 | parent.move_child(warning_icon, threading_checkbox.get_index() + 1) 88 | else: 89 | # Add processor count information to the tooltip 90 | var processor_count = OS.get_processor_count() 91 | threading_checkbox.tooltip_text = "Enable multi-threaded processing (%d processors available)" % processor_count 92 | 93 | # Setup match refresh rate checkbox 94 | if match_refresh_rate_checkbox: 95 | match_refresh_rate_checkbox.button_pressed = adaptive_graphics.match_refresh_rate 96 | match_refresh_rate_checkbox.tooltip_text = "Set target FPS to match display refresh rate (%d Hz)" % int(adaptive_graphics.display_refresh_rate) 97 | 98 | # Apply simple theme to dropdowns for Godot 4.4 99 | _apply_simple_dropdown_theme() 100 | 101 | # Setup VSync dropdown - Simple approach for Godot 4.4 102 | if vsync_option: 103 | # Clear existing items 104 | vsync_option.clear() 105 | 106 | # Add VSync options 107 | vsync_option.add_item("Disabled", 0) 108 | vsync_option.add_item("Enabled", 1) 109 | vsync_option.add_item("Adaptive", 2) 110 | vsync_option.add_item("Mailbox", 3) 111 | 112 | # Select the current VSync mode 113 | adaptive_graphics.update_vsync_and_refresh_rate() 114 | vsync_option.select(adaptive_graphics.current_vsync_mode) 115 | 116 | # Update tooltip with current refresh rate 117 | vsync_option.tooltip_text = "Control vertical synchronization with the display (Refresh rate: %d Hz)" % int(adaptive_graphics.display_refresh_rate) 118 | 119 | # Setup preset dropdown - Simple approach for Godot 4.4 120 | preset_option.clear() 121 | preset_option.add_item("Ultra Low", AdaptiveGraphics.QualityPreset.ULTRA_LOW) 122 | preset_option.add_item("Low", AdaptiveGraphics.QualityPreset.LOW) 123 | preset_option.add_item("Medium", AdaptiveGraphics.QualityPreset.MEDIUM) 124 | preset_option.add_item("High", AdaptiveGraphics.QualityPreset.HIGH) 125 | preset_option.add_item("Ultra", AdaptiveGraphics.QualityPreset.ULTRA) 126 | preset_option.add_item("Custom", -1) 127 | 128 | # Store current settings for change detection 129 | _store_current_settings() 130 | 131 | # Detect and select the current preset 132 | current_preset = _detect_preset_from_settings() 133 | 134 | # Select the appropriate preset in the dropdown 135 | if current_preset == "Custom": 136 | # Select the "Custom" option (last item) 137 | preset_option.select(preset_option.item_count - 1) 138 | else: 139 | # Find the preset by name and select it 140 | var preset_index: int = AdaptiveGraphics.QualityPreset.get(current_preset, -1) 141 | if preset_index >= 0: 142 | for i in range(preset_option.item_count): 143 | if preset_option.get_item_id(i) == preset_index: 144 | preset_option.select(i) 145 | break 146 | 147 | # Create timer for updating UI 148 | update_timer = Timer.new() 149 | update_timer.wait_time = 0.5 150 | update_timer.timeout.connect(_on_update_timer_timeout) 151 | update_timer.autostart = true 152 | add_child(update_timer) 153 | 154 | # Update UI immediately 155 | _update_ui() 156 | 157 | func _store_current_settings() -> void: 158 | if not adaptive_graphics or not adaptive_graphics.settings_manager: 159 | return 160 | 161 | current_settings.clear() 162 | 163 | for setting_name in adaptive_graphics.settings_manager.available_settings: 164 | if adaptive_graphics.settings_manager.is_setting_applicable(setting_name): 165 | var setting = adaptive_graphics.settings_manager.available_settings[setting_name] 166 | current_settings[setting_name] = setting.current_index 167 | 168 | func _detect_preset_from_settings() -> String: 169 | if not adaptive_graphics: 170 | return "Custom" 171 | 172 | # Check if current settings match any preset 173 | for preset_name in AdaptiveGraphics.QualityPreset.keys(): 174 | var preset_index = AdaptiveGraphics.QualityPreset[preset_name] 175 | if _settings_match_preset(preset_index): 176 | return preset_name 177 | 178 | return "Custom" 179 | 180 | func _settings_match_preset(preset_index: int) -> bool: 181 | if not adaptive_graphics or not adaptive_graphics.settings_manager: 182 | return false 183 | 184 | if not adaptive_graphics.presets.has(preset_index): 185 | return false 186 | 187 | var preset_settings = adaptive_graphics.get_preset_for_current_renderer(preset_index) 188 | 189 | for setting_name in preset_settings: 190 | if not adaptive_graphics.settings_manager.available_settings.has(setting_name): 191 | continue 192 | 193 | if not adaptive_graphics.settings_manager.is_setting_applicable(setting_name): 194 | continue 195 | 196 | var current_index = adaptive_graphics.settings_manager.available_settings[setting_name].current_index 197 | if current_index != preset_settings[setting_name]: 198 | return false 199 | 200 | return true 201 | 202 | func _apply_simple_dropdown_theme() -> void: 203 | # Simple theme adjustments for Godot 4.4 204 | if vsync_option: 205 | vsync_option.custom_minimum_size.y = 30 206 | 207 | if preset_option: 208 | preset_option.custom_minimum_size.y = 30 209 | 210 | func _on_update_timer_timeout() -> void: 211 | _update_ui() 212 | 213 | func _update_ui() -> void: 214 | if not adaptive_graphics: 215 | return 216 | 217 | # Update FPS display 218 | if fps_label: 219 | # Make sure the FPS monitor exists 220 | if adaptive_graphics.fps_monitor: 221 | var avg_fps: float = adaptive_graphics.fps_monitor.get_average_fps() 222 | var is_stable: bool = adaptive_graphics.fps_monitor.is_fps_stable() 223 | var stability_text: String = "stable" if is_stable else "unstable" 224 | 225 | # Ensure we're not showing 0 FPS 226 | if avg_fps < 0.1: 227 | avg_fps = Engine.get_frames_per_second() 228 | 229 | fps_label.text = "FPS: %.1f (%s)" % [avg_fps, stability_text] 230 | 231 | # Color code based on performance 232 | var target_fps: int = adaptive_graphics.target_fps 233 | var tolerance: int = adaptive_graphics.fps_tolerance 234 | 235 | if avg_fps >= target_fps - tolerance: 236 | fps_label.modulate = Color(0.2, 1.0, 0.2) # Green for good performance 237 | elif avg_fps >= target_fps - tolerance * 2: 238 | fps_label.modulate = Color(1.0, 1.0, 0.2) # Yellow for borderline performance 239 | else: 240 | fps_label.modulate = Color(1.0, 0.2, 0.2) # Red for poor performance 241 | else: 242 | # Fallback if FPS monitor doesn't exist 243 | var current_fps: float = Engine.get_frames_per_second() 244 | fps_label.text = "FPS: %.1f (unknown)" % current_fps 245 | fps_label.modulate = Color(1.0, 1.0, 1.0) # White for unknown status 246 | 247 | # Update status display 248 | if status_label: 249 | # Get the current action directly from adaptive_graphics 250 | var current_action: String = adaptive_graphics.current_action 251 | 252 | # Get other status information 253 | var renderer_type: GraphicsSettingsManager.RendererType = adaptive_graphics.settings_manager.current_renderer 254 | var renderer_name: String = GraphicsSettingsManager.RendererType.keys()[renderer_type] 255 | 256 | var vsync_mode: int = adaptive_graphics.current_vsync_mode 257 | var vsync_modes: Array[String] = ["Disabled", "Enabled", "Adaptive", "Mailbox"] 258 | 259 | var threading_info: Dictionary = adaptive_graphics.get_threading_support_info() 260 | var threading_active: bool = threading_info.thread_active 261 | 262 | # Build the status text 263 | var status_text: String = "Status: %s\n" % current_action 264 | status_text += "Threading: %s" % ("Active" if threading_active else "Inactive") 265 | 266 | status_label.text = status_text 267 | 268 | # Update renderer display 269 | if renderer_label and adaptive_graphics.settings_manager: 270 | var renderer_type = adaptive_graphics.settings_manager.current_renderer 271 | var renderer_name = GraphicsSettingsManager.RendererType.keys()[renderer_type] 272 | renderer_label.text = "Renderer: " + renderer_name 273 | 274 | # Add FSR info if available 275 | if adaptive_graphics.settings_manager.fsr_available: 276 | renderer_label.text += " (FSR supported)" 277 | 278 | # Check if settings have changed and update the preset dropdown 279 | if _have_settings_changed(): 280 | # Store the new settings 281 | _store_current_settings() 282 | 283 | # Detect which preset matches the current settings 284 | var detected_preset: String = _detect_preset_from_settings() 285 | 286 | # Update the preset dropdown 287 | if detected_preset != current_preset: 288 | current_preset = detected_preset 289 | 290 | # Find and select the matching preset in the dropdown 291 | if detected_preset == "Custom": 292 | # Select the "Custom" option (last item) 293 | preset_option.select(preset_option.item_count - 1) 294 | else: 295 | # Find the preset by name and select it 296 | var preset_index: int = AdaptiveGraphics.QualityPreset.get(detected_preset, -1) 297 | if preset_index >= 0: 298 | for i in range(preset_option.item_count): 299 | if preset_option.get_item_id(i) == preset_index: 300 | preset_option.select(i) 301 | break 302 | 303 | func _have_settings_changed() -> bool: 304 | if not adaptive_graphics or not adaptive_graphics.settings_manager: 305 | return false 306 | 307 | for setting_name in adaptive_graphics.settings_manager.available_settings: 308 | if adaptive_graphics.settings_manager.is_setting_applicable(setting_name): 309 | var setting = adaptive_graphics.settings_manager.available_settings[setting_name] 310 | 311 | if not current_settings.has(setting_name) or current_settings[setting_name] != setting.current_index: 312 | return true 313 | 314 | return false 315 | 316 | func _on_target_fps_slider_value_changed(value: float) -> void: 317 | if adaptive_graphics: 318 | adaptive_graphics.target_fps = int(value) 319 | target_fps_value.value = value 320 | 321 | func _on_target_fps_value_changed(value: float) -> void: 322 | if adaptive_graphics: 323 | adaptive_graphics.target_fps = int(value) 324 | target_fps_slider.value = value 325 | 326 | func _on_reset_button_pressed() -> void: 327 | if adaptive_graphics: 328 | adaptive_graphics.target_fps = default_target_fps 329 | target_fps_slider.value = default_target_fps 330 | target_fps_value.value = default_target_fps 331 | 332 | func _on_enabled_checkbox_toggled(button_pressed: bool) -> void: 333 | if adaptive_graphics: 334 | adaptive_graphics.enabled = button_pressed 335 | 336 | func _on_allow_increase_checkbox_toggled(button_pressed: bool) -> void: 337 | if adaptive_graphics: 338 | adaptive_graphics.allow_quality_increase = button_pressed 339 | 340 | func _on_threading_checkbox_toggled(button_pressed: bool) -> void: 341 | if adaptive_graphics: 342 | adaptive_graphics.set_threading_enabled(button_pressed) 343 | 344 | func _on_match_refresh_rate_checkbox_toggled(button_pressed: bool) -> void: 345 | if adaptive_graphics: 346 | adaptive_graphics.match_refresh_rate = button_pressed 347 | if button_pressed: 348 | adaptive_graphics.set_target_fps_to_refresh_rate() 349 | target_fps_slider.value = adaptive_graphics.target_fps 350 | target_fps_value.value = adaptive_graphics.target_fps 351 | 352 | func _on_vsync_option_item_selected(index: int) -> void: 353 | if adaptive_graphics: 354 | adaptive_graphics.set_vsync_mode(index) 355 | 356 | func _on_preset_option_item_selected(index: int) -> void: 357 | var preset_id = preset_option.get_item_id(index) 358 | 359 | if preset_id >= 0 and adaptive_graphics: 360 | adaptive_graphics.apply_preset(preset_id) 361 | current_preset = AdaptiveGraphics.QualityPreset.keys()[preset_id] 362 | _store_current_settings() 363 | 364 | # Update UI immediately 365 | _update_ui() 366 | 367 | func _on_changed_settings() -> void: 368 | # Update the UI when settings are changed from any source 369 | _update_ui() 370 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/adaptive_graphics_ui.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bkwbb0swp5fw1 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/adaptive_graphics_ui.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://c45xs6yq7yahr"] 2 | 3 | [ext_resource type="Script" uid="uid://bkwbb0swp5fw1" path="res://addons/smart_graphics_settings/adaptive_graphics_ui.gd" id="1_yvmqe"] 4 | [ext_resource type="StyleBox" uid="uid://by7x1kn8sb4eu" path="res://addons/smart_graphics_settings/ui_panel_background.tres" id="2_dxjne"] 5 | [ext_resource type="Texture2D" uid="uid://lddqovy3e0cd" path="res://addons/smart_graphics_settings/images/title-text.png" id="3_mpp0l"] 6 | 7 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mpp0l"] 8 | content_margin_left = 8.0 9 | content_margin_top = 4.0 10 | content_margin_right = 8.0 11 | content_margin_bottom = 4.0 12 | bg_color = Color(0.2, 0.2, 0.2, 1) 13 | corner_radius_top_left = 4 14 | corner_radius_top_right = 4 15 | corner_radius_bottom_right = 4 16 | corner_radius_bottom_left = 4 17 | 18 | [sub_resource type="Theme" id="Theme_popup"] 19 | PopupMenu/constants/h_separation = 8 20 | PopupMenu/constants/item_end_padding = 8 21 | PopupMenu/constants/item_start_padding = 8 22 | PopupMenu/constants/v_separation = 8 23 | PopupMenu/styles/hover = SubResource("StyleBoxFlat_mpp0l") 24 | PopupMenu/styles/panel = SubResource("StyleBoxFlat_mpp0l") 25 | 26 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_option"] 27 | content_margin_left = 8.0 28 | content_margin_top = 4.0 29 | content_margin_right = 8.0 30 | content_margin_bottom = 4.0 31 | bg_color = Color(0.15, 0.15, 0.15, 1) 32 | corner_radius_top_left = 4 33 | corner_radius_top_right = 4 34 | corner_radius_bottom_right = 4 35 | corner_radius_bottom_left = 4 36 | 37 | [node name="AdaptiveGraphicsUI" type="Control"] 38 | layout_mode = 3 39 | anchors_preset = 15 40 | anchor_right = 1.0 41 | anchor_bottom = 1.0 42 | grow_horizontal = 2 43 | grow_vertical = 2 44 | script = ExtResource("1_yvmqe") 45 | 46 | [node name="CenterContainer" type="CenterContainer" parent="."] 47 | layout_mode = 1 48 | anchors_preset = 15 49 | anchor_right = 1.0 50 | anchor_bottom = 1.0 51 | grow_horizontal = 2 52 | grow_vertical = 2 53 | 54 | [node name="PanelContainer" type="PanelContainer" parent="CenterContainer"] 55 | custom_minimum_size = Vector2(500, 0) 56 | layout_mode = 2 57 | theme_override_styles/panel = ExtResource("2_dxjne") 58 | 59 | [node name="MarginContainer" type="MarginContainer" parent="CenterContainer/PanelContainer"] 60 | layout_mode = 2 61 | theme_override_constants/margin_left = 20 62 | theme_override_constants/margin_top = 20 63 | theme_override_constants/margin_right = 20 64 | theme_override_constants/margin_bottom = 20 65 | 66 | [node name="VBoxContent" type="VBoxContainer" parent="CenterContainer/PanelContainer/MarginContainer"] 67 | layout_mode = 2 68 | theme_override_constants/separation = 10 69 | 70 | [node name="Godot-smart-graphics-settings-text" type="Sprite2D" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 71 | position = Vector2(230, 0) 72 | scale = Vector2(1.5, 1.5) 73 | texture = ExtResource("3_mpp0l") 74 | region_enabled = true 75 | region_rect = Rect2(40, 142, 220, 16) 76 | 77 | [node name="TitleLabel" type="RichTextLabel" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 78 | layout_mode = 2 79 | horizontal_alignment = 1 80 | 81 | [node name="StatusLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 82 | layout_mode = 2 83 | text = "Status: Initializing..." 84 | horizontal_alignment = 1 85 | text_overrun_behavior = 3 86 | 87 | [node name="FPSLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 88 | layout_mode = 2 89 | text = "Current FPS: 0.0" 90 | text_overrun_behavior = 3 91 | 92 | [node name="TargetFPSLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 93 | layout_mode = 2 94 | text = "Target FPS" 95 | 96 | [node name="TargetFPSContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 97 | layout_mode = 2 98 | theme_override_constants/separation = 10 99 | 100 | [node name="TargetFPSSlider" type="HSlider" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent/TargetFPSContainer"] 101 | layout_mode = 2 102 | size_flags_horizontal = 3 103 | min_value = 30.0 104 | max_value = 144.0 105 | value = 60.0 106 | rounded = true 107 | tick_count = 8 108 | ticks_on_borders = true 109 | 110 | [node name="TargetFPSValue" type="SpinBox" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent/TargetFPSContainer"] 111 | layout_mode = 2 112 | min_value = 30.0 113 | max_value = 144.0 114 | value = 60.0 115 | rounded = true 116 | 117 | [node name="ResetButton" type="Button" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent/TargetFPSContainer"] 118 | layout_mode = 2 119 | tooltip_text = "Reset to default" 120 | text = "Reset" 121 | 122 | [node name="EnabledCheckbox" type="CheckBox" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 123 | layout_mode = 2 124 | button_pressed = true 125 | text = "Enable Adaptive Graphics" 126 | 127 | [node name="AllowIncreaseCheckbox" type="CheckBox" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 128 | layout_mode = 2 129 | tooltip_text = "When enabled, the system will increase quality if performance is good" 130 | text = "Allow Quality Increase" 131 | 132 | [node name="ThreadingCheckbox" type="CheckBox" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 133 | layout_mode = 2 134 | tooltip_text = "Use a separate thread for performance analysis (recommended)" 135 | button_pressed = true 136 | text = "Use Threading" 137 | 138 | [node name="MatchRefreshRateCheckbox" type="CheckBox" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 139 | layout_mode = 2 140 | tooltip_text = "Set target FPS to match your display's refresh rate" 141 | text = "Match Display Refresh Rate" 142 | 143 | [node name="VSyncLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 144 | layout_mode = 2 145 | text = "VSync Mode" 146 | 147 | [node name="VSyncOption" type="OptionButton" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 148 | layout_mode = 2 149 | tooltip_text = "Control vertical synchronization with the display" 150 | focus_mode = 0 151 | theme = SubResource("Theme_popup") 152 | theme_override_constants/h_separation = 8 153 | theme_override_styles/focus = SubResource("StyleBoxFlat_option") 154 | theme_override_styles/hover = SubResource("StyleBoxFlat_option") 155 | theme_override_styles/pressed = SubResource("StyleBoxFlat_option") 156 | theme_override_styles/normal = SubResource("StyleBoxFlat_option") 157 | theme_override_constants/arrow_margin = 8 158 | selected = 0 159 | item_count = 4 160 | popup/item_0/text = "Disabled" 161 | popup/item_0/id = 0 162 | popup/item_1/text = "Enabled" 163 | popup/item_1/id = 1 164 | popup/item_2/text = "Adaptive" 165 | popup/item_2/id = 2 166 | popup/item_3/text = "Mailbox" 167 | popup/item_3/id = 3 168 | 169 | [node name="PresetLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 170 | layout_mode = 2 171 | text = "Quality Preset" 172 | 173 | [node name="PresetOption" type="OptionButton" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContent"] 174 | layout_mode = 2 175 | size_flags_horizontal = 3 176 | focus_mode = 0 177 | theme = SubResource("Theme_popup") 178 | theme_override_constants/h_separation = 8 179 | theme_override_styles/focus = SubResource("StyleBoxFlat_option") 180 | theme_override_styles/hover = SubResource("StyleBoxFlat_option") 181 | theme_override_styles/pressed = SubResource("StyleBoxFlat_option") 182 | theme_override_styles/normal = SubResource("StyleBoxFlat_option") 183 | theme_override_constants/arrow_margin = 8 184 | selected = 2 185 | fit_to_longest_item = false 186 | item_count = 5 187 | popup/item_0/text = "Ultra Low" 188 | popup/item_0/id = 0 189 | popup/item_1/text = "Low" 190 | popup/item_1/id = 1 191 | popup/item_2/text = "Medium" 192 | popup/item_2/id = 2 193 | popup/item_3/text = "High" 194 | popup/item_3/id = 3 195 | popup/item_4/text = "Ultra" 196 | popup/item_4/id = 4 197 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/demo/README.md: -------------------------------------------------------------------------------- 1 | # Smart Graphics Settings Demo 2 | 3 | This demo scene showcases the functionality of the Smart Graphics Settings addon for Godot 4.4. It provides a practical example of how the adaptive graphics system works in a real-world scenario. 4 | 5 | ## Getting Started 6 | 7 | 1. Open the demo scene: `addons/smart_graphics_settings/demo/demo_scene.tscn` 8 | 2. Run the scene to see the adaptive graphics in action 9 | 3. Use the keyboard shortcuts to interact with the demo 10 | 11 | ## Demo Features 12 | 13 | The demo includes: 14 | 15 | - A 3D environment with dynamic lighting and shadows 16 | - Stress testing capabilities to simulate different performance scenarios 17 | - Real-time FPS display and performance metrics 18 | - UI for adjusting and monitoring graphics settings 19 | 20 | ## Controls 21 | 22 | | Key | Action | 23 | |-----|--------| 24 | | F7 | Toggle graphics settings UI | 25 | | 1-5 | Apply quality presets (1=Low, 5=Ultra) | 26 | | Space | Spawn stress test objects | 27 | | Esc | Clear stress test objects | 28 | 29 | ## Stress Testing 30 | 31 | The demo includes a stress test feature that allows you to spawn multiple 3D objects to test how the adaptive graphics system responds to changing performance conditions: 32 | 33 | 1. Press Space to spawn a batch of objects (default: 100 objects per batch) 34 | 2. Observe how the FPS drops and the adaptive graphics system automatically adjusts settings 35 | 3. Press Esc to clear all stress test objects 36 | 37 | ## Quality Presets 38 | 39 | The demo includes 5 quality presets that you can apply using the number keys 1-5: 40 | 41 | 1. **Low**: Optimized for low-end hardware 42 | 2. **Medium**: Balanced performance and quality 43 | 3. **High**: Good visual quality with reasonable performance 44 | 4. **Ultra**: Maximum visual quality 45 | 5. **Custom**: User-defined settings 46 | 47 | ## Customizing the Demo 48 | 49 | You can modify the demo scene to test different scenarios: 50 | 51 | ### Changing the Stress Test Parameters 52 | 53 | Open `demo_scene.gd` and modify the following variables: 54 | 55 | ```gdscript 56 | # Number of objects to spawn per batch 57 | var spawn_count: int = 100 58 | 59 | # Change this to adjust the types of meshes used in the stress test 60 | func _ready() -> void: 61 | # Initialize mesh types 62 | mesh_types.append(BoxMesh.new()) 63 | mesh_types.append(SphereMesh.new()) 64 | mesh_types.append(TorusMesh.new()) 65 | mesh_types.append(CylinderMesh.new()) 66 | ``` 67 | 68 | ### Testing Different Environments 69 | 70 | The demo scene uses a standard 3D environment, but you can modify it to test specific rendering features: 71 | 72 | 1. Add more complex geometry to test geometry-heavy scenes 73 | 2. Add more lights to test lighting performance 74 | 3. Add reflective surfaces to test reflection performance 75 | 4. Add transparent materials to test transparency performance 76 | 77 | ## Performance Analysis 78 | 79 | The demo includes real-time performance metrics that help you understand how the adaptive graphics system is working: 80 | 81 | 1. **FPS Counter**: Shows the current framerate 82 | 2. **Settings Display**: Shows which settings are currently active 83 | 3. **Adjustment Status**: Indicates when the system is measuring or adjusting settings 84 | 85 | ## Troubleshooting 86 | 87 | If you encounter issues with the demo: 88 | 89 | - Make sure the plugin is properly installed and enabled 90 | - Check that the SmartGraphicsSettings singleton is available 91 | - Verify that your Godot version is 4.4 or later 92 | - Check the console for any error messages 93 | 94 | ## Next Steps 95 | 96 | After exploring the demo, you can: 97 | 98 | 1. Integrate the Smart Graphics Settings into your own project 99 | 2. Customize the settings and presets to match your game's requirements 100 | 3. Extend the system with custom settings specific to your game 101 | 102 | For more information, refer to the [main documentation](../README.md). 103 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/demo/demo_scene.gd: -------------------------------------------------------------------------------- 1 | extends Node3D 2 | 3 | ## Available mesh types for stress testing 4 | var mesh_types: Array[Mesh] = [] 5 | 6 | ## Stress test objects container 7 | @onready var stress_objects: Node3D = $StressObjects 8 | 9 | ## Reference to the SmartGraphicsSettings singleton 10 | # No need to store a reference as we can access it directly 11 | 12 | ## Number of objects to spawn per batch 13 | var spawn_count: int = 100 14 | 15 | func _ready() -> void: 16 | # Initialize mesh types 17 | mesh_types.append(BoxMesh.new()) 18 | mesh_types.append(SphereMesh.new()) 19 | mesh_types.append(TorusMesh.new()) 20 | mesh_types.append(CylinderMesh.new()) 21 | 22 | # Set up input actions for quality presets 23 | if not InputMap.has_action("quality_preset_1"): 24 | for i in range(1, 6): 25 | var action_name: String = "quality_preset_%d" % i 26 | InputMap.add_action(action_name) 27 | var event: InputEventKey = InputEventKey.new() 28 | event.keycode = KEY_1 + i - 1 # KEY_1, KEY_2, etc. 29 | InputMap.action_add_event(action_name, event) 30 | 31 | # Set up stress test input 32 | if not InputMap.has_action("stress_test"): 33 | InputMap.add_action("stress_test") 34 | var event: InputEventKey = InputEventKey.new() 35 | event.keycode = KEY_SPACE 36 | InputMap.action_add_event("stress_test", event) 37 | 38 | # SmartGraphicsSettings is available directly as an autoload singleton 39 | if not is_instance_valid(SmartGraphicsSettings): 40 | push_error("Demo: Failed to find SmartGraphicsSettings singleton") 41 | return 42 | 43 | # Create a timer to update the UI 44 | var timer: Timer = Timer.new() 45 | timer.wait_time = 0.5 46 | timer.timeout.connect(_update_ui) 47 | timer.autostart = true 48 | add_child(timer) 49 | 50 | # Update UI immediately 51 | _update_ui() 52 | 53 | func _input(event: InputEvent) -> void: 54 | # Handle quality preset hotkeys 55 | for i in range(1, 6): 56 | var action_name: String = "quality_preset_%d" % i 57 | if event.is_action_pressed(action_name): 58 | var preset: int = i - 1 # Convert to 0-based index 59 | if is_instance_valid(SmartGraphicsSettings): 60 | SmartGraphicsSettings.apply_preset(preset) 61 | print("Applied quality preset: ", i) 62 | return 63 | 64 | # Handle stress test 65 | if event.is_action_pressed("stress_test"): 66 | spawn_stress_objects() 67 | 68 | ## Spawn a batch of objects to stress the renderer 69 | func spawn_stress_objects() -> void: 70 | for i in range(spawn_count): 71 | var mesh_instance: MeshInstance3D = MeshInstance3D.new() 72 | var mesh_type: Mesh = mesh_types[randi() % mesh_types.size()] 73 | mesh_instance.mesh = mesh_type 74 | 75 | # Random position 76 | var x: float = randf_range(-10, 10) 77 | var z: float = randf_range(-10, 10) 78 | mesh_instance.position = Vector3(x, 1.0, z) 79 | 80 | # Random rotation 81 | mesh_instance.rotation = Vector3( 82 | randf_range(0, TAU), 83 | randf_range(0, TAU), 84 | randf_range(0, TAU) 85 | ) 86 | 87 | # Random scale 88 | var scale: float = randf_range(0.5, 1.5) 89 | mesh_instance.scale = Vector3(scale, scale, scale) 90 | 91 | stress_objects.add_child(mesh_instance) 92 | 93 | print("Spawned %d more objects. Total: %d" % [spawn_count, stress_objects.get_child_count()]) 94 | 95 | ## Update the UI with current status information 96 | func _update_ui() -> void: 97 | if not is_instance_valid(SmartGraphicsSettings): 98 | return 99 | 100 | ## Helper function to get the name of a VSync mode 101 | func _get_vsync_name(mode: int) -> String: 102 | var vsync_modes: Array[String] = ["Disabled", "Enabled", "Adaptive", "Mailbox"] 103 | if mode >= 0 and mode < vsync_modes.size(): 104 | return vsync_modes[mode] 105 | return "Unknown" 106 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/demo/demo_scene.gd.uid: -------------------------------------------------------------------------------- 1 | uid://covnqac8lnxhh 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/demo/demo_scene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=12 format=3 uid="uid://c8yvnqnvqnvn"] 2 | 3 | [ext_resource type="Script" uid="uid://covnqac8lnxhh" path="res://addons/smart_graphics_settings/demo/demo_scene.gd" id="1_yvmqe"] 4 | [ext_resource type="StyleBox" uid="uid://by7x1kn8sb4eu" path="res://addons/smart_graphics_settings/ui_panel_background.tres" id="2_p853p"] 5 | 6 | [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_yvmqe"] 7 | sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1) 8 | ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1) 9 | 10 | [sub_resource type="Sky" id="Sky_yvmqe"] 11 | sky_material = SubResource("ProceduralSkyMaterial_yvmqe") 12 | 13 | [sub_resource type="Environment" id="Environment_yvmqe"] 14 | background_mode = 2 15 | sky = SubResource("Sky_yvmqe") 16 | tonemap_mode = 2 17 | glow_enabled = true 18 | volumetric_fog_enabled = true 19 | volumetric_fog_density = 0.01 20 | 21 | [sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_p853p"] 22 | 23 | [sub_resource type="BoxMesh" id="BoxMesh_yvmqe"] 24 | 25 | [sub_resource type="SphereMesh" id="SphereMesh_yvmqe"] 26 | 27 | [sub_resource type="TorusMesh" id="TorusMesh_yvmqe"] 28 | 29 | [sub_resource type="CylinderMesh" id="CylinderMesh_yvmqe"] 30 | 31 | [sub_resource type="PlaneMesh" id="PlaneMesh_yvmqe"] 32 | size = Vector2(20, 20) 33 | 34 | [node name="DemoScene" type="Node3D"] 35 | script = ExtResource("1_yvmqe") 36 | 37 | [node name="WorldEnvironment" type="WorldEnvironment" parent="."] 38 | environment = SubResource("Environment_yvmqe") 39 | 40 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] 41 | transform = Transform3D(-0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, 0.75, -0.433013, 0, 0, 0) 42 | shadow_enabled = true 43 | 44 | [node name="Camera3D" type="Camera3D" parent="."] 45 | transform = Transform3D(1, 0, 0, 0, 0.965926, 0.258819, 0, -0.258819, 0.965926, 0, 2, 8) 46 | attributes = SubResource("CameraAttributesPractical_p853p") 47 | 48 | [node name="Objects" type="Node3D" parent="."] 49 | 50 | [node name="Box" type="MeshInstance3D" parent="Objects"] 51 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0.5, 0) 52 | mesh = SubResource("BoxMesh_yvmqe") 53 | 54 | [node name="Sphere" type="MeshInstance3D" parent="Objects"] 55 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.5, 0) 56 | mesh = SubResource("SphereMesh_yvmqe") 57 | 58 | [node name="Torus" type="MeshInstance3D" parent="Objects"] 59 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) 60 | mesh = SubResource("TorusMesh_yvmqe") 61 | 62 | [node name="Cylinder" type="MeshInstance3D" parent="Objects"] 63 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0.5, 0) 64 | mesh = SubResource("CylinderMesh_yvmqe") 65 | 66 | [node name="Box2" type="MeshInstance3D" parent="Objects"] 67 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0.5, 0) 68 | mesh = SubResource("BoxMesh_yvmqe") 69 | 70 | [node name="Ground" type="MeshInstance3D" parent="."] 71 | mesh = SubResource("PlaneMesh_yvmqe") 72 | 73 | [node name="StressObjects" type="Node3D" parent="."] 74 | 75 | [node name="PanelContainer" type="PanelContainer" parent="."] 76 | self_modulate = Color(1, 1, 1, 0.5) 77 | custom_minimum_size = Vector2(450, 0) 78 | theme_override_styles/panel = ExtResource("2_p853p") 79 | 80 | [node name="MarginContainer" type="MarginContainer" parent="PanelContainer"] 81 | layout_mode = 2 82 | theme_override_constants/margin_left = 16 83 | theme_override_constants/margin_top = 16 84 | theme_override_constants/margin_right = 16 85 | theme_override_constants/margin_bottom = 16 86 | 87 | [node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"] 88 | layout_mode = 2 89 | 90 | [node name="Instructions" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] 91 | layout_mode = 2 92 | text = "Smart Graphics Settings Demo 93 | Press F7 to toggle settings UI 94 | Press 1-5 to apply quality presets (1=Ultra Low, 5=Ultra) 95 | Press Space to spawn 100 more objects (stress test)" 96 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/fps_monitor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg") 3 | class_name FPSMonitor 4 | extends Node 5 | 6 | ## Stores the history of FPS values for analysis 7 | var fps_history: Array[float] = [] 8 | 9 | ## Running sum of FPS values for efficient average calculation 10 | var running_sum: float = 0.0 11 | 12 | ## Number of frames to keep in history (about 1 second at 60fps) 13 | var history_size: int = 60 14 | 15 | ## Threshold for determining if FPS is stable (variance must be below this) 16 | var stable_threshold: float = 5.0 17 | 18 | ## Mutex for thread-safe access to FPS data 19 | var fps_mutex: Mutex = Mutex.new() 20 | 21 | ## Hysteresis for stability detection to prevent rapid fluctuations 22 | var stability_hysteresis: int = 5 23 | var stability_counter: int = 0 24 | var last_stability_state: bool = true # Default to stable 25 | 26 | ## Process function that collects FPS data each frame 27 | func _process(_delta: float) -> void: 28 | # Skip processing in editor 29 | if Engine.is_editor_hint(): 30 | return 31 | 32 | var current_fps: float = Engine.get_frames_per_second() 33 | 34 | fps_mutex.lock() 35 | # Update running sum and history 36 | running_sum += current_fps 37 | fps_history.append(current_fps) 38 | 39 | if fps_history.size() > history_size: 40 | running_sum -= fps_history.pop_front() 41 | fps_mutex.unlock() 42 | 43 | ## Calculate the average FPS from the history in a thread-safe way 44 | func get_average_fps() -> float: 45 | fps_mutex.lock() 46 | var result: float = 0.0 47 | var size: int = fps_history.size() 48 | 49 | if size > 0: 50 | result = running_sum / size 51 | else: 52 | # Return current FPS if no history is available 53 | result = Engine.get_frames_per_second() 54 | 55 | fps_mutex.unlock() 56 | 57 | return result 58 | 59 | ## Get a thread-safe copy of the FPS history 60 | func get_fps_history_copy() -> Array[float]: 61 | fps_mutex.lock() 62 | var copy: Array[float] = fps_history.duplicate() 63 | fps_mutex.unlock() 64 | return copy 65 | 66 | ## Determine if the FPS is stable (low variance) in a thread-safe way 67 | ## Uses hysteresis to prevent rapid fluctuations between stable and unstable states 68 | func is_fps_stable() -> bool: 69 | fps_mutex.lock() 70 | var size: int = fps_history.size() 71 | 72 | # Need at least a few frames to make a determination 73 | if size < 10: 74 | fps_mutex.unlock() 75 | return true # Default to stable with insufficient data 76 | 77 | var avg: float = running_sum / size 78 | var variance: float = 0.0 79 | 80 | for fps in fps_history: 81 | variance += pow(fps - avg, 2) 82 | 83 | variance /= size 84 | 85 | # Calculate the coefficient of variation (CV) - a normalized measure of dispersion 86 | var cv: float = 0.0 87 | if avg > 0: 88 | cv = sqrt(variance) / avg 89 | 90 | fps_mutex.unlock() 91 | 92 | # Determine the raw stability state 93 | # More lenient threshold for stability 94 | var raw_stability: bool = variance < stable_threshold * 2 and cv < 0.15 95 | 96 | # Apply hysteresis to prevent rapid fluctuations 97 | if raw_stability != last_stability_state: 98 | stability_counter += 1 99 | if stability_counter >= stability_hysteresis: 100 | last_stability_state = raw_stability 101 | stability_counter = 0 102 | else: 103 | stability_counter = 0 104 | 105 | return last_stability_state 106 | 107 | ## Clear the FPS history in a thread-safe way 108 | func clear_history() -> void: 109 | fps_mutex.lock() 110 | fps_history.clear() 111 | running_sum = 0.0 112 | stability_counter = 0 113 | last_stability_state = true # Default to stable when cleared 114 | fps_mutex.unlock() 115 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/fps_monitor.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dtu4fp0v58hio 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/graphics_settings_manager.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg") 3 | class_name GraphicsSettingsManager 4 | extends Node 5 | 6 | ## Enum for setting priority (determines adjustment order) 7 | enum SettingPriority { 8 | RENDER_SCALE = 0, # Minimal visual impact, adjust first 9 | ANTI_ALIASING = 10, # Low visual impact 10 | POST_PROCESSING = 20, # Medium visual impact 11 | SHADOWS = 30, # Medium-high visual impact 12 | REFLECTIONS = 40, # High visual impact 13 | GLOBAL_ILLUMINATION = 50 # Major visual impact, adjust last 14 | } 15 | 16 | ## Enum for setting type (determines how the setting is applied) 17 | enum SettingType { 18 | VIEWPORT, 19 | ENVIRONMENT, 20 | CAMERA, 21 | RENDER_SCALE 22 | } 23 | 24 | ## Enum for renderer type 25 | enum RendererType { 26 | FORWARD_PLUS, 27 | MOBILE, 28 | COMPATIBILITY, 29 | CUSTOM 30 | } 31 | 32 | ## Current renderer being used 33 | var current_renderer: RendererType = RendererType.FORWARD_PLUS 34 | 35 | ## Define setting structure with strong typing 36 | class Setting: 37 | var values: Array[Variant] 38 | var current_index: int 39 | var priority: SettingPriority 40 | var type: SettingType 41 | 42 | func _init(p_values: Array[Variant], p_current_index: int, p_priority: SettingPriority, p_type: SettingType) -> void: 43 | values = p_values 44 | current_index = p_current_index 45 | priority = p_priority 46 | type = p_type 47 | 48 | ## Dictionary of available settings 49 | var available_settings: Dictionary[String, Setting] = {} 50 | 51 | ## References to nodes 52 | var viewport: Viewport 53 | var environment: Environment 54 | var camera: Camera3D 55 | var original_window_size: Vector2i 56 | 57 | ## Platform information 58 | var platform_info: Dictionary = {} 59 | 60 | ## Whether FSR is available on this platform 61 | var fsr_available: bool = false 62 | 63 | func _init() -> void: 64 | # Gather platform information 65 | platform_info = { 66 | "os_name": OS.get_name(), 67 | "model_name": OS.get_model_name(), 68 | "processor_name": OS.get_processor_name(), 69 | "processor_count": OS.get_processor_count(), 70 | "renderer": RenderingServer.get_rendering_device().get_device_name() if RenderingServer.get_rendering_device() else "Unknown" 71 | } 72 | 73 | # Check if FSR is available 74 | fsr_available = OS.has_feature("fsr") 75 | 76 | # Initialize common settings with strong typing and more granular priorities 77 | available_settings = { 78 | # Render Scale (highest performance impact, lowest visual degradation) 79 | "render_scale": Setting.new( 80 | [0.5, 0.6, 0.7, 0.8, 0.9, 1.0], 81 | 5, # Start with highest quality 82 | SettingPriority.RENDER_SCALE, 83 | SettingType.RENDER_SCALE 84 | ), 85 | 86 | # Common Viewport Settings for all renderers 87 | "msaa": Setting.new( 88 | [Viewport.MSAA_DISABLED, Viewport.MSAA_2X, Viewport.MSAA_4X, Viewport.MSAA_8X], 89 | 3, 90 | SettingPriority.ANTI_ALIASING, 91 | SettingType.VIEWPORT 92 | ), 93 | "shadow_quality": Setting.new( 94 | [ 95 | Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_DISABLED, 96 | Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_1, 97 | Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_4, 98 | Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_16, 99 | Viewport.SHADOW_ATLAS_QUADRANT_SUBDIV_64 100 | ], 101 | 4, 102 | SettingPriority.SHADOWS, 103 | SettingType.VIEWPORT 104 | ), 105 | "shadow_size": Setting.new( 106 | [1024, 2048, 4096, 8192], 107 | 2, 108 | SettingPriority.SHADOWS, 109 | SettingType.VIEWPORT 110 | ), 111 | "fxaa": Setting.new( 112 | [Viewport.SCREEN_SPACE_AA_DISABLED, Viewport.SCREEN_SPACE_AA_FXAA], 113 | 1, 114 | SettingPriority.ANTI_ALIASING, 115 | SettingType.VIEWPORT 116 | ), 117 | 118 | # Camera Settings (common to all renderers) 119 | "dof": Setting.new( 120 | [false, true], 121 | 1, 122 | SettingPriority.POST_PROCESSING, 123 | SettingType.CAMERA 124 | ) 125 | # Removed motion_blur as it's not supported in Godot 4.4 126 | } 127 | 128 | # Renderer-specific settings will be initialized in _ready() 129 | 130 | func _ready() -> void: 131 | # Skip initialization in editor 132 | if Engine.is_editor_hint(): 133 | return 134 | 135 | # Get viewport with error handling 136 | viewport = get_viewport() 137 | if not viewport: 138 | push_error("Smart Graphics Settings: Failed to get viewport") 139 | return 140 | 141 | original_window_size = DisplayServer.window_get_size() 142 | 143 | # Detect current renderer 144 | detect_renderer() 145 | 146 | # Initialize renderer-specific settings 147 | initialize_renderer_specific_settings() 148 | 149 | # Find WorldEnvironment and Camera3D in the scene 150 | var world_env: WorldEnvironment = find_world_environment() 151 | if world_env: 152 | environment = world_env.environment 153 | else: 154 | # Try to get environment from the viewport's world 155 | if viewport and viewport.get_world_3d() and viewport.get_world_3d().environment: 156 | environment = viewport.get_world_3d().environment 157 | else: 158 | push_warning("Smart Graphics Settings: No WorldEnvironment found. Environment settings will not be applied.") 159 | 160 | camera = find_main_camera() 161 | if not camera: 162 | push_warning("Smart Graphics Settings: No Camera3D found. Camera settings will not be applied.") 163 | 164 | ## Detect which renderer is currently being used 165 | func detect_renderer() -> void: 166 | var rendering_method: String = ProjectSettings.get_setting("rendering/renderer/rendering_method", "forward_plus") 167 | 168 | # Check for custom rendering pipelines 169 | if ProjectSettings.has_setting("rendering/custom_pipeline/enabled") and ProjectSettings.get_setting("rendering/custom_pipeline/enabled", false): 170 | print("Smart Graphics Settings: Detected custom rendering pipeline") 171 | current_renderer = RendererType.CUSTOM 172 | return 173 | 174 | match rendering_method: 175 | "forward_plus": 176 | current_renderer = RendererType.FORWARD_PLUS 177 | "mobile": 178 | current_renderer = RendererType.MOBILE 179 | "gl_compatibility": 180 | current_renderer = RendererType.COMPATIBILITY 181 | _: 182 | # Default to Forward+ if unknown 183 | current_renderer = RendererType.FORWARD_PLUS 184 | push_warning("Smart Graphics Settings: Unknown rendering method: %s. Defaulting to Forward+." % rendering_method) 185 | 186 | print("Smart Graphics Settings: Detected renderer: ", RendererType.keys()[current_renderer]) 187 | 188 | ## Initialize settings specific to the detected renderer 189 | func initialize_renderer_specific_settings() -> void: 190 | match current_renderer: 191 | RendererType.FORWARD_PLUS: 192 | # Forward+ specific settings 193 | available_settings["ssao"] = Setting.new( 194 | [false, true], 195 | 1, 196 | SettingPriority.POST_PROCESSING, 197 | SettingType.VIEWPORT 198 | ) 199 | available_settings["ssao_quality"] = Setting.new( 200 | [0, 1, 2, 3, 4], # Very Low, Low, Medium, High, Ultra 201 | 3, 202 | SettingPriority.POST_PROCESSING, 203 | SettingType.VIEWPORT 204 | ) 205 | available_settings["ssr"] = Setting.new( 206 | [false, true], 207 | 1, 208 | SettingPriority.REFLECTIONS, 209 | SettingType.VIEWPORT 210 | ) 211 | available_settings["ssr_max_steps"] = Setting.new( 212 | [8, 16, 32, 64], 213 | 3, 214 | SettingPriority.REFLECTIONS, 215 | SettingType.VIEWPORT 216 | ) 217 | available_settings["sdfgi"] = Setting.new( 218 | [false, true], 219 | 1, 220 | SettingPriority.GLOBAL_ILLUMINATION, 221 | SettingType.VIEWPORT 222 | ) 223 | available_settings["glow"] = Setting.new( 224 | [false, true], 225 | 1, 226 | SettingPriority.POST_PROCESSING, 227 | SettingType.VIEWPORT 228 | ) 229 | 230 | # Environment Settings (Forward+ specific) 231 | available_settings["volumetric_fog"] = Setting.new( 232 | [false, true], 233 | 1, 234 | SettingPriority.POST_PROCESSING, 235 | SettingType.ENVIRONMENT 236 | ) 237 | available_settings["volumetric_fog_density"] = Setting.new( 238 | [0.01, 0.02, 0.03, 0.05], 239 | 3, 240 | SettingPriority.POST_PROCESSING, 241 | SettingType.ENVIRONMENT 242 | ) 243 | 244 | RendererType.MOBILE: 245 | # Mobile-specific settings 246 | # Mobile has more limited features 247 | available_settings["glow"] = Setting.new( 248 | [false, true], 249 | 1, 250 | SettingPriority.POST_PROCESSING, 251 | SettingType.VIEWPORT 252 | ) 253 | 254 | # Mobile-specific optimizations 255 | # Adjust render scale values for mobile 256 | if available_settings.has("render_scale"): 257 | available_settings["render_scale"] = Setting.new( 258 | [0.5, 0.6, 0.7, 0.75, 0.8, 1.0], # More aggressive scaling options 259 | 4, # Default to 0.8 on mobile 260 | SettingPriority.RENDER_SCALE, 261 | SettingType.RENDER_SCALE 262 | ) 263 | 264 | RendererType.COMPATIBILITY: 265 | # Compatibility-specific settings 266 | available_settings["glow"] = Setting.new( 267 | [false, true], 268 | 1, 269 | SettingPriority.POST_PROCESSING, 270 | SettingType.VIEWPORT 271 | ) 272 | # Some limited SSAO might be available in compatibility mode 273 | available_settings["ssao"] = Setting.new( 274 | [false, true], 275 | 1, 276 | SettingPriority.POST_PROCESSING, 277 | SettingType.VIEWPORT 278 | ) 279 | 280 | RendererType.CUSTOM: 281 | # For custom renderers, we'll keep only the most basic settings 282 | # and let the user extend as needed 283 | push_warning("Smart Graphics Settings: Custom rendering pipeline detected. Only basic settings will be available.") 284 | 285 | ## Find the WorldEnvironment node in the scene 286 | func find_world_environment() -> WorldEnvironment: 287 | var root: Node = get_tree().root 288 | if not root: 289 | push_error("Smart Graphics Settings: Unable to access scene tree root") 290 | return null 291 | 292 | var world_env: WorldEnvironment = find_node_of_type(root, "WorldEnvironment") as WorldEnvironment 293 | 294 | # If not found directly, try to get it from the current scene 295 | if not world_env and get_tree().current_scene: 296 | world_env = find_node_of_type(get_tree().current_scene, "WorldEnvironment") as WorldEnvironment 297 | 298 | if not world_env: 299 | push_warning("Smart Graphics Settings: No WorldEnvironment node found in the scene") 300 | 301 | return world_env 302 | 303 | ## Find the main Camera3D in the scene 304 | func find_main_camera() -> Camera3D: 305 | var root: Node = get_tree().root 306 | if not root: 307 | push_error("Smart Graphics Settings: Unable to access scene tree root") 308 | return null 309 | 310 | var camera: Camera3D = find_node_of_type(root, "Camera3D") as Camera3D 311 | 312 | # If not found directly, try to get it from the current scene 313 | if not camera and get_tree().current_scene: 314 | camera = find_node_of_type(get_tree().current_scene, "Camera3D") as Camera3D 315 | 316 | # If still not found, try to get the current camera 317 | if not camera and get_viewport() and get_viewport().get_camera_3d(): 318 | camera = get_viewport().get_camera_3d() 319 | 320 | return camera 321 | 322 | ## Helper function to find a node of a specific type 323 | func find_node_of_type(node: Node, type_name: String) -> Node: 324 | if not node: 325 | return null 326 | 327 | if node.get_class() == type_name: 328 | return node 329 | 330 | for child in node.get_children(): 331 | var found: Node = find_node_of_type(child, type_name) 332 | if found: 333 | return found 334 | 335 | return null 336 | 337 | ## Check if a setting is applicable to the current renderer 338 | func is_setting_applicable(setting_name: String) -> bool: 339 | if not available_settings.has(setting_name): 340 | return false 341 | 342 | # Define which settings apply to which renderers 343 | match current_renderer: 344 | RendererType.FORWARD_PLUS: 345 | return true # All settings apply to Forward+ 346 | 347 | RendererType.MOBILE: 348 | # Return false for Forward+ specific settings 349 | if setting_name in ["sdfgi", "ssr", "ssr_max_steps", "volumetric_fog", 350 | "volumetric_fog_density", "ssao", "ssao_quality"]: 351 | return false 352 | return true 353 | 354 | RendererType.COMPATIBILITY: 355 | # Similar to Mobile but might have different constraints 356 | if setting_name in ["sdfgi", "ssr", "ssr_max_steps", "volumetric_fog", 357 | "volumetric_fog_density"]: 358 | return false 359 | return true 360 | 361 | RendererType.CUSTOM: 362 | # For custom renderers, only allow basic settings 363 | if setting_name in ["render_scale", "msaa", "shadow_quality", "shadow_size", "fxaa"]: 364 | return true 365 | return false 366 | 367 | return false 368 | 369 | ## Order settings by priority for adjustment 370 | func get_settings_by_priority() -> Array[String]: 371 | var settings_by_priority: Dictionary = {} 372 | 373 | # Initialize dictionary with all priority levels 374 | for priority in range(SettingPriority.RENDER_SCALE, SettingPriority.GLOBAL_ILLUMINATION + 10, 10): 375 | settings_by_priority[priority] = [] 376 | 377 | # Only include settings applicable to the current renderer 378 | for setting_name in available_settings: 379 | if is_setting_applicable(setting_name): 380 | var priority: SettingPriority = available_settings[setting_name].priority 381 | if not settings_by_priority.has(priority): 382 | settings_by_priority[priority] = [] 383 | settings_by_priority[priority].append(setting_name) 384 | 385 | var result: Array[String] = [] 386 | 387 | # Sort by priority 388 | var priorities: Array = settings_by_priority.keys() 389 | priorities.sort() 390 | 391 | for priority in priorities: 392 | result.append_array(settings_by_priority[priority]) 393 | 394 | return result 395 | 396 | ## Decrease quality of a specific setting 397 | func decrease_setting_quality(setting_name: String) -> bool: 398 | if not available_settings.has(setting_name): 399 | push_warning("Smart Graphics Settings: Attempted to decrease non-existent setting: %s" % setting_name) 400 | return false 401 | 402 | if not is_setting_applicable(setting_name): 403 | push_warning("Smart Graphics Settings: Setting %s is not applicable to the current renderer" % setting_name) 404 | return false 405 | 406 | var setting: Setting = available_settings[setting_name] 407 | if setting.current_index > 0: 408 | setting.current_index -= 1 409 | apply_setting(setting_name) 410 | return true 411 | return false 412 | 413 | ## Increase quality of a specific setting 414 | func increase_setting_quality(setting_name: String) -> bool: 415 | if not available_settings.has(setting_name): 416 | push_warning("Smart Graphics Settings: Attempted to increase non-existent setting: %s" % setting_name) 417 | return false 418 | 419 | if not is_setting_applicable(setting_name): 420 | push_warning("Smart Graphics Settings: Setting %s is not applicable to the current renderer" % setting_name) 421 | return false 422 | 423 | var setting: Setting = available_settings[setting_name] 424 | if setting.current_index < setting.values.size() - 1: 425 | setting.current_index += 1 426 | apply_setting(setting_name) 427 | return true 428 | return false 429 | 430 | ## Apply the current setting value to the game 431 | func apply_setting(setting_name: String) -> void: 432 | # Check if this setting exists 433 | if not available_settings.has(setting_name): 434 | push_warning("Smart Graphics Settings: Attempted to apply non-existent setting: %s" % setting_name) 435 | return 436 | 437 | # Check if this setting is applicable to the current renderer 438 | if not is_setting_applicable(setting_name): 439 | push_warning("Smart Graphics Settings: Setting %s is not applicable to the current renderer: %s" % [setting_name, RendererType.keys()[current_renderer]]) 440 | return 441 | 442 | var setting: Setting = available_settings[setting_name] 443 | var value = setting.values[setting.current_index] 444 | 445 | # Apply the setting based on the setting type and name 446 | match setting.type: 447 | SettingType.VIEWPORT: 448 | if not viewport: 449 | push_warning("Smart Graphics Settings: Cannot apply viewport setting %s - no viewport found" % setting_name) 450 | return 451 | 452 | match setting_name: 453 | "msaa": 454 | # In Godot 4.4, we need to use different approaches for Window vs Viewport 455 | if viewport is Window: 456 | # For the main viewport (Window), we need to use the viewport's own methods 457 | var vp: Viewport = get_viewport() 458 | if vp: 459 | vp.msaa_3d = value 460 | else: 461 | viewport.msaa = value 462 | "shadow_quality": 463 | if viewport is Window: 464 | # For the main viewport (Window) 465 | var vp: Viewport = get_viewport() 466 | if vp: 467 | # Get the viewport RID 468 | var viewport_rid: RID = vp.get_viewport_rid() 469 | # Set the shadow atlas quadrant subdivision 470 | RenderingServer.viewport_set_positional_shadow_atlas_quadrant_subdivision(viewport_rid, 0, value) 471 | else: 472 | viewport.shadow_atlas_quad_0 = value 473 | "shadow_size": 474 | if viewport is Window: 475 | # For the main viewport (Window) 476 | var vp: Viewport = get_viewport() 477 | if vp: 478 | # Get the viewport RID 479 | var viewport_rid: RID = vp.get_viewport_rid() 480 | RenderingServer.viewport_set_positional_shadow_atlas_size(viewport_rid, value) 481 | else: 482 | viewport.shadow_atlas_size = value 483 | "fxaa": 484 | if viewport is Window: 485 | # For the main viewport (Window) 486 | var vp: Viewport = get_viewport() 487 | if vp: 488 | vp.screen_space_aa = value 489 | else: 490 | viewport.screen_space_aa = value 491 | "ssao": 492 | if viewport is Window: 493 | # For the main viewport (Window) 494 | if get_viewport() and get_viewport().get_world_3d() and get_viewport().get_world_3d().environment: 495 | get_viewport().get_world_3d().environment.ssao_enabled = value 496 | else: 497 | viewport.ssao_enabled = value 498 | "ssao_quality": 499 | # In Godot 4.4, SSAO quality is controlled via ProjectSettings 500 | ProjectSettings.set_setting("rendering/environment/ssao/quality", value) 501 | "ssr": 502 | if viewport is Window: 503 | # For the main viewport (Window) 504 | if get_viewport() and get_viewport().get_world_3d() and get_viewport().get_world_3d().environment: 505 | get_viewport().get_world_3d().environment.ssr_enabled = value 506 | else: 507 | viewport.ssr_enabled = value 508 | "ssr_max_steps": 509 | if viewport is Window: 510 | # For the main viewport (Window) 511 | if get_viewport() and get_viewport().get_world_3d() and get_viewport().get_world_3d().environment: 512 | get_viewport().get_world_3d().environment.ssr_max_steps = value 513 | else: 514 | viewport.ssr_max_steps = value 515 | "sdfgi": 516 | if viewport is Window: 517 | # For the main viewport (Window) 518 | if get_viewport() and get_viewport().get_world_3d() and get_viewport().get_world_3d().environment: 519 | get_viewport().get_world_3d().environment.sdfgi_enabled = value 520 | else: 521 | viewport.sdfgi_enabled = value 522 | "glow": 523 | if viewport is Window: 524 | # For the main viewport (Window) 525 | if get_viewport() and get_viewport().get_world_3d() and get_viewport().get_world_3d().environment: 526 | get_viewport().get_world_3d().environment.glow_enabled = value 527 | else: 528 | viewport.glow_enabled = value 529 | 530 | SettingType.ENVIRONMENT: 531 | if not environment: 532 | # Try to get the environment from the world if not directly set 533 | if viewport and viewport.get_world_3d() and viewport.get_world_3d().environment: 534 | environment = viewport.get_world_3d().environment 535 | else: 536 | push_warning("Smart Graphics Settings: Cannot apply environment setting %s - no environment found" % setting_name) 537 | return 538 | 539 | match setting_name: 540 | "volumetric_fog": 541 | environment.volumetric_fog_enabled = value 542 | "volumetric_fog_density": 543 | environment.volumetric_fog_density = value 544 | 545 | SettingType.CAMERA: 546 | if not camera: 547 | push_warning("Smart Graphics Settings: Cannot apply camera setting %s - no camera found" % setting_name) 548 | return 549 | 550 | match setting_name: 551 | "dof": 552 | if camera.attributes: 553 | camera.attributes.dof_blur_far_enabled = value 554 | else: 555 | push_warning("Smart Graphics Settings: Camera has no attributes resource assigned. Cannot set DOF settings.") 556 | 557 | SettingType.RENDER_SCALE: 558 | if setting_name == "render_scale": 559 | var scale_factor: float = value 560 | 561 | if viewport is Window: 562 | # For the main viewport (Window) 563 | var vp: Viewport = get_viewport() 564 | if not vp: 565 | push_warning("Smart Graphics Settings: Cannot apply render scale - no viewport found") 566 | return 567 | 568 | # Set the scaling mode 569 | if scale_factor < 1.0: 570 | if fsr_available: 571 | vp.scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR 572 | else: 573 | vp.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR 574 | vp.scaling_3d_scale = scale_factor 575 | else: 576 | vp.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR 577 | vp.scaling_3d_scale = 1.0 578 | else: 579 | # For other viewports 580 | viewport.size = Vector2i(original_window_size.x * scale_factor, original_window_size.y * scale_factor) 581 | if scale_factor < 1.0: 582 | if fsr_available: 583 | viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR 584 | else: 585 | viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR 586 | viewport.scaling_3d_scale = scale_factor 587 | else: 588 | viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR 589 | viewport.scaling_3d_scale = 1.0 590 | 591 | print("Smart Graphics Settings: Applied setting: ", setting_name, " = ", value) 592 | 593 | ## Save current graphics settings to user://graphics_settings.cfg 594 | func save_graphics_settings() -> void: 595 | var config: ConfigFile = ConfigFile.new() 596 | 597 | # Save renderer type 598 | config.set_value("system", "renderer", current_renderer) 599 | 600 | # Save platform info for debugging 601 | config.set_value("system", "platform", OS.get_name()) 602 | 603 | for setting_name in available_settings: 604 | var setting: Setting = available_settings[setting_name] 605 | config.set_value("graphics", setting_name, setting.current_index) 606 | 607 | var err: Error = config.save("user://graphics_settings.cfg") 608 | if err != OK: 609 | push_error("Smart Graphics Settings: Error saving graphics settings: %s" % error_string(err)) 610 | 611 | ## Load graphics settings from user://graphics_settings.cfg 612 | func load_graphics_settings() -> void: 613 | var config: ConfigFile = ConfigFile.new() 614 | var err: Error = config.load("user://graphics_settings.cfg") 615 | 616 | if err != OK: 617 | push_warning("Smart Graphics Settings: No saved settings found or error loading settings: %s" % error_string(err)) 618 | return 619 | 620 | # Check if settings were saved with a different renderer 621 | if config.has_section_key("system", "renderer"): 622 | var saved_renderer: int = config.get_value("system", "renderer") 623 | if saved_renderer != current_renderer: 624 | push_warning("Smart Graphics Settings: Saved settings were for a different renderer. Some settings may not apply.") 625 | 626 | for setting_name in available_settings: 627 | if config.has_section_key("graphics", setting_name): 628 | var index: int = config.get_value("graphics", setting_name) 629 | 630 | # Validate index is within bounds 631 | if index >= 0 and index < available_settings[setting_name].values.size(): 632 | available_settings[setting_name].current_index = index 633 | 634 | # Only apply if the setting is applicable to the current renderer 635 | if is_setting_applicable(setting_name): 636 | apply_setting(setting_name) 637 | else: 638 | push_warning("Smart Graphics Settings: Invalid index %d for setting %s" % [index, setting_name]) 639 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/graphics_settings_manager.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dxiesa4lkyahn 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasbecker-dev/godot-smart-graphics-settings/9b13563421a1b8392746ebbd4ada8ef0ecc71dc7/addons/smart_graphics_settings/images/icon.png -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cbgd20ttbe1so" 6 | path="res://.godot/imported/icon.png-6edfe3cd310a893587d7cbd42a585bf7.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smart_graphics_settings/images/icon.png" 14 | dest_files=["res://.godot/imported/icon.png-6edfe3cd310a893587d7cbd42a585bf7.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 39 | 41 | 44 | 48 | 52 | 53 | 61 | 67 | 73 | 74 | 84 | 94 | 95 | 100 | 110 | 117 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c7sfc7rk0pmk3" 6 | path="res://.godot/imported/icon.svg-2e8b671c9495e19f2be6245bd98bb60b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smart_graphics_settings/images/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-2e8b671c9495e19f2be6245bd98bb60b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasbecker-dev/godot-smart-graphics-settings/9b13563421a1b8392746ebbd4ada8ef0ecc71dc7/addons/smart_graphics_settings/images/logo.png -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/logo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dhnf6os2ilgoc" 6 | path="res://.godot/imported/logo.png-c61a5866afd139c3a2f9b887bc98f9e8.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smart_graphics_settings/images/logo.png" 14 | dest_files=["res://.godot/imported/logo.png-c61a5866afd139c3a2f9b887bc98f9e8.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/logo.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cwjnxnk1s4g5y" 6 | path="res://.godot/imported/logo.svg-2fa80a039b847a2a85a5d303589929a2.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smart_graphics_settings/images/logo.svg" 14 | dest_files=["res://.godot/imported/logo.svg-2fa80a039b847a2a85a5d303589929a2.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 39 | 40 | 42 | 46 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://djk3dq2vcmnf3" 6 | path="res://.godot/imported/smart-graphics-settings-icon.svg-4d2d8773c531c43062886eae43689de4.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg" 14 | dest_files=["res://.godot/imported/smart-graphics-settings-icon.svg-4d2d8773c531c43062886eae43689de4.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/title-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasbecker-dev/godot-smart-graphics-settings/9b13563421a1b8392746ebbd4ada8ef0ecc71dc7/addons/smart_graphics_settings/images/title-text.png -------------------------------------------------------------------------------- /addons/smart_graphics_settings/images/title-text.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://lddqovy3e0cd" 6 | path="res://.godot/imported/title-text.png-25aa98b4f9a3d7969c3e9bc241e8ce22.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smart_graphics_settings/images/title-text.png" 14 | dest_files=["res://.godot/imported/title-text.png-25aa98b4f9a3d7969c3e9bc241e8ce22.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Smart Graphics Settings" 4 | description="Adaptive graphics settings that automatically adjust based on performance" 5 | author="Smart Graphics Team" 6 | version="0.1.1" 7 | script="plugin.gd" 8 | icon="icon.svg" -------------------------------------------------------------------------------- /addons/smart_graphics_settings/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree() -> void: 5 | # Register the addon 6 | add_autoload_singleton("SmartGraphicsSettings", "res://addons/smart_graphics_settings/smart_graphics_settings.gd") 7 | 8 | # Register custom types with the icon 9 | var icon_path: String = "res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg" 10 | var icon: Texture2D 11 | 12 | if ResourceLoader.exists(icon_path): 13 | icon = load(icon_path) 14 | else: 15 | push_warning("Smart Graphics Settings: Icon not found at %s" % icon_path) 16 | 17 | # Register custom classes with the icon 18 | var adaptive_graphics_script: String = "res://addons/smart_graphics_settings/adaptive_graphics.gd" 19 | var fps_monitor_script: String = "res://addons/smart_graphics_settings/fps_monitor.gd" 20 | var graphics_settings_manager_script: String = "res://addons/smart_graphics_settings/graphics_settings_manager.gd" 21 | var adaptive_graphics_ui_script: String = "res://addons/smart_graphics_settings/adaptive_graphics_ui.gd" 22 | 23 | if ResourceLoader.exists(adaptive_graphics_script): 24 | add_custom_type("AdaptiveGraphics", "Node", load(adaptive_graphics_script), icon) 25 | else: 26 | push_error("Smart Graphics Settings: AdaptiveGraphics script not found at %s" % adaptive_graphics_script) 27 | 28 | if ResourceLoader.exists(fps_monitor_script): 29 | add_custom_type("FPSMonitor", "Node", load(fps_monitor_script), icon) 30 | else: 31 | push_error("Smart Graphics Settings: FPSMonitor script not found at %s" % fps_monitor_script) 32 | 33 | if ResourceLoader.exists(graphics_settings_manager_script): 34 | add_custom_type("GraphicsSettingsManager", "Node", load(graphics_settings_manager_script), icon) 35 | else: 36 | push_error("Smart Graphics Settings: GraphicsSettingsManager script not found at %s" % graphics_settings_manager_script) 37 | 38 | if ResourceLoader.exists(adaptive_graphics_ui_script): 39 | add_custom_type("AdaptiveGraphicsUI", "Control", load(adaptive_graphics_ui_script), icon) 40 | else: 41 | push_error("Smart Graphics Settings: AdaptiveGraphicsUI script not found at %s" % adaptive_graphics_ui_script) 42 | 43 | print("Smart Graphics Settings: Plugin initialized successfully") 44 | 45 | func _exit_tree() -> void: 46 | # Clean up when the plugin is disabled 47 | remove_autoload_singleton("SmartGraphicsSettings") 48 | 49 | # Remove custom types 50 | remove_custom_type("AdaptiveGraphics") 51 | remove_custom_type("FPSMonitor") 52 | remove_custom_type("GraphicsSettingsManager") 53 | remove_custom_type("AdaptiveGraphicsUI") 54 | 55 | print("Smart Graphics Settings: Plugin cleaned up successfully") -------------------------------------------------------------------------------- /addons/smart_graphics_settings/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bootef3oowc4q 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/smart_graphics_settings.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/smart_graphics_settings/images/smart-graphics-settings-icon.svg") 3 | extends Node 4 | 5 | ## Signal emitted when the AdaptiveGraphics node has been initialized. 6 | ## Connect to this signal to safely access adaptive_graphics after startup. 7 | signal initialized 8 | 9 | ## The main AdaptiveGraphics controller 10 | var adaptive_graphics: AdaptiveGraphics 11 | 12 | ## Whether the UI is currently visible 13 | var ui_visible: bool = false 14 | 15 | ## The UI instance 16 | var ui_instance: AdaptiveGraphicsUI 17 | 18 | ## UI scene resource 19 | var ui_scene: PackedScene = preload("res://addons/smart_graphics_settings/adaptive_graphics_ui.tscn") 20 | 21 | ## Platform information for optimizations 22 | var platform_info: Dictionary = {} 23 | 24 | func _ready() -> void: 25 | # Gather platform information 26 | platform_info = { 27 | "os_name": OS.get_name(), 28 | "model_name": OS.get_model_name(), 29 | "processor_name": OS.get_processor_name(), 30 | "processor_count": OS.get_processor_count() 31 | } 32 | 33 | # Only initialize adaptive graphics when running the game, not in the editor 34 | if not Engine.is_editor_hint(): 35 | # Create the adaptive graphics controller using a deferred call 36 | # This ensures classes are registered before we try to use them 37 | call_deferred("_initialize_adaptive_graphics") 38 | 39 | # Register input action for toggling UI 40 | if not InputMap.has_action("toggle_graphics_settings"): 41 | InputMap.add_action("toggle_graphics_settings") 42 | var event: InputEventKey = InputEventKey.new() 43 | event.keycode = KEY_F7 44 | InputMap.action_add_event("toggle_graphics_settings", event) 45 | 46 | # Apply platform-specific optimizations 47 | call_deferred("_apply_platform_optimizations") 48 | 49 | func _initialize_adaptive_graphics() -> void: 50 | # Try to create the adaptive graphics controller 51 | if ClassDB.class_exists("AdaptiveGraphics") or Engine.has_singleton("AdaptiveGraphics") or ResourceLoader.exists("res://addons/smart_graphics_settings/adaptive_graphics.gd"): 52 | adaptive_graphics = AdaptiveGraphics.new() 53 | 54 | # Check if creation was successful before proceeding 55 | if adaptive_graphics: 56 | add_child(adaptive_graphics) 57 | # Emit the signal AFTER initialization is complete 58 | initialized.emit() 59 | 60 | # Wait for the settings manager to be initialized 61 | await get_tree().process_frame 62 | 63 | # Log the detected renderer 64 | if adaptive_graphics.settings_manager: 65 | var renderer_type: GraphicsSettingsManager.RendererType = adaptive_graphics.settings_manager.current_renderer 66 | var renderer_name: String = GraphicsSettingsManager.RendererType.keys()[renderer_type] 67 | print("Smart Graphics Settings: Detected renderer - ", renderer_name) 68 | else: 69 | push_error("Smart Graphics Settings: Failed to instantiate AdaptiveGraphics.") 70 | else: 71 | push_error("Smart Graphics Settings: AdaptiveGraphics class not found. Make sure the addon is properly installed.") 72 | 73 | func _apply_platform_optimizations() -> void: 74 | if not adaptive_graphics: 75 | await get_tree().process_frame 76 | if not adaptive_graphics: 77 | push_error("Smart Graphics Settings: Failed to apply platform optimizations - AdaptiveGraphics not initialized") 78 | return 79 | 80 | # Apply platform-specific optimizations 81 | match OS.get_name(): 82 | "Android", "iOS": 83 | # Mobile platforms often benefit from more aggressive settings 84 | adaptive_graphics.fps_tolerance = 8 # Allow more variation on mobile 85 | adaptive_graphics.adjustment_cooldown = 5.0 # Less frequent adjustments to save battery 86 | print("Smart Graphics Settings: Applied mobile platform optimizations") 87 | "Web": 88 | # Web platform limitations 89 | adaptive_graphics.use_threading = false 90 | adaptive_graphics.threading_supported = false 91 | print("Smart Graphics Settings: Applied web platform optimizations") 92 | "Windows", "macOS", "Linux": 93 | # Desktop platforms can use more precise settings 94 | adaptive_graphics.fps_tolerance = 3 95 | adaptive_graphics.adjustment_cooldown = 2.0 96 | print("Smart Graphics Settings: Applied desktop platform optimizations") 97 | 98 | func _input(event: InputEvent) -> void: 99 | # Only process input when running the game, not in the editor 100 | if Engine.is_editor_hint(): 101 | return 102 | 103 | # Only check for the action if it exists 104 | if InputMap.has_action("toggle_graphics_settings") and event.is_action_pressed("toggle_graphics_settings"): 105 | toggle_ui() 106 | 107 | ## Toggle the graphics settings UI 108 | func toggle_ui() -> void: 109 | if ui_visible: 110 | hide_ui() 111 | else: 112 | show_ui() 113 | 114 | ## Show the graphics settings UI 115 | func show_ui() -> void: 116 | if ui_instance: 117 | ui_instance.show() 118 | else: 119 | if not ui_scene: 120 | push_error("Smart Graphics Settings: UI scene not found") 121 | return 122 | 123 | ui_instance = ui_scene.instantiate() as AdaptiveGraphicsUI 124 | if not ui_instance: 125 | push_error("Smart Graphics Settings: Failed to instantiate UI") 126 | return 127 | 128 | if not adaptive_graphics: 129 | push_error("Smart Graphics Settings: AdaptiveGraphics not initialized") 130 | return 131 | 132 | # Set the path directly to the adaptive_graphics node 133 | if adaptive_graphics.is_inside_tree(): 134 | ui_instance.adaptive_graphics_path = adaptive_graphics.get_path() 135 | else: 136 | # If the node isn't in the tree yet, we'll set the reference directly 137 | ui_instance.adaptive_graphics = adaptive_graphics 138 | 139 | var root: Viewport = get_tree().root 140 | if not root: 141 | push_error("Smart Graphics Settings: Failed to get scene root") 142 | return 143 | 144 | root.add_child(ui_instance) 145 | 146 | ui_visible = true 147 | 148 | ## Hide the graphics settings UI 149 | func hide_ui() -> void: 150 | if ui_instance: 151 | ui_instance.hide() 152 | 153 | ui_visible = false 154 | 155 | ## Apply a quality preset 156 | func apply_preset(preset: AdaptiveGraphics.QualityPreset) -> void: 157 | if adaptive_graphics: 158 | adaptive_graphics.apply_preset(preset) 159 | 160 | ## Enable or disable adaptive graphics 161 | func set_enabled(enabled: bool) -> void: 162 | if adaptive_graphics: 163 | adaptive_graphics.enabled = enabled 164 | 165 | ## Set the target FPS 166 | func set_target_fps(fps: int) -> void: 167 | if adaptive_graphics: 168 | adaptive_graphics.target_fps = fps 169 | 170 | ## Get the current average FPS 171 | func get_average_fps() -> float: 172 | if adaptive_graphics and adaptive_graphics.fps_monitor: 173 | return adaptive_graphics.fps_monitor.get_average_fps() 174 | return 0.0 175 | 176 | ## Check if FPS is stable 177 | func is_fps_stable() -> bool: 178 | if adaptive_graphics and adaptive_graphics.fps_monitor: 179 | return adaptive_graphics.fps_monitor.is_fps_stable() 180 | return false 181 | 182 | ## Set whether to match the target FPS to the display refresh rate 183 | func set_match_refresh_rate(enabled: bool) -> void: 184 | if adaptive_graphics: 185 | adaptive_graphics.match_refresh_rate = enabled 186 | if enabled: 187 | adaptive_graphics.set_target_fps_to_refresh_rate() 188 | 189 | ## Set the target FPS to match the display refresh rate 190 | func set_target_fps_to_refresh_rate() -> void: 191 | if adaptive_graphics: 192 | adaptive_graphics.set_target_fps_to_refresh_rate() 193 | 194 | ## Get the display refresh rate 195 | func get_display_refresh_rate() -> float: 196 | if adaptive_graphics: 197 | adaptive_graphics.update_vsync_and_refresh_rate() 198 | return adaptive_graphics.display_refresh_rate 199 | return 60.0 200 | 201 | ## Set the VSync mode 202 | func set_vsync_mode(mode: int) -> void: 203 | if adaptive_graphics: 204 | adaptive_graphics.set_vsync_mode(mode) 205 | 206 | ## Get the current VSync mode 207 | func get_vsync_mode() -> int: 208 | if adaptive_graphics: 209 | adaptive_graphics.update_vsync_and_refresh_rate() 210 | return adaptive_graphics.current_vsync_mode 211 | return DisplayServer.VSYNC_ENABLED # Default fallback 212 | 213 | ## Get detailed information about the current state 214 | func get_status_info() -> Dictionary: 215 | var info: Dictionary = { 216 | "enabled": false, 217 | "target_fps": 60, 218 | "current_fps": 0.0, 219 | "fps_stable": false, 220 | "vsync_mode": DisplayServer.VSYNC_ENABLED, 221 | "refresh_rate": 60.0, 222 | "renderer": "Unknown", 223 | "current_action": "Not initialized", 224 | "threading": false, 225 | "platform": platform_info 226 | } 227 | 228 | if adaptive_graphics: 229 | info.enabled = adaptive_graphics.enabled 230 | info.target_fps = adaptive_graphics.target_fps 231 | info.current_fps = get_average_fps() 232 | info.fps_stable = is_fps_stable() 233 | info.vsync_mode = get_vsync_mode() 234 | info.refresh_rate = get_display_refresh_rate() 235 | info.current_action = adaptive_graphics.current_action 236 | info.threading = adaptive_graphics.use_threading 237 | 238 | if adaptive_graphics.settings_manager: 239 | var renderer_type: GraphicsSettingsManager.RendererType = adaptive_graphics.settings_manager.current_renderer 240 | info.renderer = GraphicsSettingsManager.RendererType.keys()[renderer_type] 241 | 242 | return info 243 | 244 | ## Get the adaptive graphics controller 245 | func get_adaptive_graphics() -> AdaptiveGraphics: 246 | return adaptive_graphics 247 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/smart_graphics_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d1pnver04h28x 2 | -------------------------------------------------------------------------------- /addons/smart_graphics_settings/ui_panel_background.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StyleBoxFlat" format=3 uid="uid://by7x1kn8sb4eu"] 2 | 3 | [resource] 4 | bg_color = Color(0.1, 0.1, 0.1, 0.8) 5 | corner_radius_top_left = 8 6 | corner_radius_top_right = 8 7 | corner_radius_bottom_right = 8 8 | corner_radius_bottom_left = 8 9 | shadow_color = Color(0, 0, 0, 0.3) 10 | shadow_size = 4 11 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Smart Graphics Settings" 14 | config/description="Adaptive graphics settings for Godot 4.4" 15 | config/version="0.1.1" 16 | run/main_scene="uid://c8yvnqnvqnvn" 17 | config/features=PackedStringArray("4.4", "Forward Plus") 18 | config/icon="res://addons/smart_graphics_settings/smart-graphics-settings-icon.svg" 19 | 20 | [autoload] 21 | 22 | SmartGraphicsSettings="*res://addons/smart_graphics_settings/smart_graphics_settings.gd" 23 | 24 | [editor_plugins] 25 | 26 | enabled=PackedStringArray("res://addons/smart_graphics_settings/plugin.cfg") 27 | 28 | [input] 29 | 30 | ui_f7={ 31 | "deadzone": 0.5, 32 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194342,"physical_keycode":4194342,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 33 | ] 34 | } 35 | --------------------------------------------------------------------------------