mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
04691aaa48
Lands the non-networked Phase 0 tasks from multiplayer-todo.md (ship/camera/ arena refactors, sim constants, background FPS handling) plus a first pass at exposing graphics/performance settings (presets, resolution scaling, vsync, FPS cap, perf overlay) and a GPU profiling harness for the real-hardware follow-up in task 0.15b.
240 lines
9.6 KiB
GDScript
240 lines
9.6 KiB
GDScript
extends Node
|
|
|
|
# Autoload: persisted player-facing video preferences (preset, AA, glow,
|
|
# brightness, vsync, fps cap, resolution scale). AA/vsync/fps-cap/resolution
|
|
# scale are Viewport- or DisplayServer-wide and applied immediately. Glow and
|
|
# brightness instead scale each arena's own tuned Environment values (see
|
|
# arena.gd's _ready(), which calls apply_to_environment() once per arena
|
|
# load) rather than overwriting them outright, so the per-arena bloom tuning
|
|
# in arena_01/02/03.tscn survives underneath the user's preference.
|
|
|
|
signal settings_changed # Arenas re-apply preset-gated Environment/light state live.
|
|
|
|
# MSAA_2X appended at the end, not inserted, so existing user://settings.cfg
|
|
# files (which store this as a bare integer ordinal) keep meaning the same
|
|
# thing after this rung was added — see task 0.19.
|
|
enum AAMode { OFF, FXAA, MSAA, MSAA_FXAA, MSAA_2X }
|
|
|
|
# Ordinals are persisted the same way as AAMode above — append only.
|
|
enum Preset { LOW, MEDIUM, HIGH, CUSTOM }
|
|
enum VsyncMode { DISABLED, ENABLED, ADAPTIVE }
|
|
|
|
const SETTINGS_PATH := "user://settings.cfg"
|
|
|
|
# preset -> bundle applied to the individual fields below. CUSTOM has no
|
|
# bundle: selecting it just stops future preset changes from overwriting
|
|
# whatever the individual fields currently hold. Task 0.15b's measured
|
|
# per-effect costs (multiplayer-todo.md §5.5.1) were too noisy to rank these
|
|
# against each other, so each rung is "meaningfully fewer full-screen passes
|
|
# than the one above it" rather than a precisely tuned ladder.
|
|
const PRESET_BUNDLES := {
|
|
Preset.LOW: {
|
|
"sdfgi_enabled": false, "ssil_enabled": false, "ssao_enabled": false,
|
|
"shadows_enabled": false, "glow_enabled": false, "aa_mode": AAMode.OFF,
|
|
"resolution_scale": 0.8,
|
|
},
|
|
Preset.MEDIUM: {
|
|
"sdfgi_enabled": false, "ssil_enabled": false, "ssao_enabled": true,
|
|
"shadows_enabled": true, "glow_enabled": true, "aa_mode": AAMode.FXAA,
|
|
"resolution_scale": 1.0,
|
|
},
|
|
Preset.HIGH: {
|
|
"sdfgi_enabled": true, "ssil_enabled": true, "ssao_enabled": true,
|
|
"shadows_enabled": true, "glow_enabled": true, "aa_mode": AAMode.FXAA,
|
|
"resolution_scale": 1.0,
|
|
},
|
|
}
|
|
|
|
var preset: Preset = Preset.HIGH
|
|
var sdfgi_enabled: bool = true
|
|
var ssil_enabled: bool = true
|
|
var ssao_enabled: bool = true
|
|
var shadows_enabled: bool = true
|
|
var glow_enabled: bool = true
|
|
|
|
# FXAA alone, not MSAA_FXAA: 4x MSAA *and* FXAA stacked is redundant blur for
|
|
# most scenes and costs more than either alone (see multiplayer-todo.md 0.19).
|
|
var aa_mode: AAMode = AAMode.FXAA
|
|
var glow_scale: float = 1.0
|
|
var brightness: float = 1.0
|
|
|
|
# 0.17b: Viewport.scaling_3d_scale, 0.5-1.0. Distinct from window stretch
|
|
# (0.17c) — this scales the 3D viewport's own internal render resolution
|
|
# before the fixed-1080p blit, so it works regardless of the stretch
|
|
# decision. FSR2 rather than bilinear: a fixed-1080p target already discards
|
|
# native resolution (see 0.17c), so FSR2's per-pixel sharpening recovers more
|
|
# of that loss than a plain bilinear upscale at the same internal scale.
|
|
var resolution_scale: float = 1.0
|
|
var fsr_sharpness: float = 0.2
|
|
|
|
var vsync_mode: VsyncMode = VsyncMode.ADAPTIVE
|
|
# 0 = uncapped; otherwise divides DisplayServer.screen_get_refresh_rate() at
|
|
# apply time (not stored as a raw fps number) so the same preference re-derives
|
|
# correctly if the game later runs on a different-refresh-rate display. Only
|
|
# takes effect when vsync_mode == DISABLED — vsync itself already caps to the
|
|
# refresh rate (or an unpredictable multiple of it, for ADAPTIVE) otherwise.
|
|
var fps_cap_divisor: int = 0
|
|
|
|
var _applying_preset := false
|
|
|
|
|
|
func _ready() -> void:
|
|
_load()
|
|
# A headless server never renders; applying any of this to its root
|
|
# viewport or window is pure waste (mirrors the same guard at ship.gd and
|
|
# arena_boundary.gd).
|
|
if DisplayServer.get_name() != "headless":
|
|
apply_aa()
|
|
apply_resolution_scale()
|
|
apply_vsync()
|
|
|
|
|
|
func _load() -> void:
|
|
var cfg := ConfigFile.new()
|
|
if cfg.load(SETTINGS_PATH) != OK:
|
|
return
|
|
preset = cfg.get_value("video", "preset", preset) as Preset
|
|
sdfgi_enabled = cfg.get_value("video", "sdfgi_enabled", sdfgi_enabled)
|
|
ssil_enabled = cfg.get_value("video", "ssil_enabled", ssil_enabled)
|
|
ssao_enabled = cfg.get_value("video", "ssao_enabled", ssao_enabled)
|
|
shadows_enabled = cfg.get_value("video", "shadows_enabled", shadows_enabled)
|
|
glow_enabled = cfg.get_value("video", "glow_enabled", glow_enabled)
|
|
aa_mode = cfg.get_value("video", "aa_mode", aa_mode) as AAMode
|
|
glow_scale = cfg.get_value("video", "glow_scale", glow_scale)
|
|
brightness = cfg.get_value("video", "brightness", brightness)
|
|
resolution_scale = cfg.get_value("video", "resolution_scale", resolution_scale)
|
|
fsr_sharpness = cfg.get_value("video", "fsr_sharpness", fsr_sharpness)
|
|
vsync_mode = cfg.get_value("video", "vsync_mode", vsync_mode) as VsyncMode
|
|
fps_cap_divisor = cfg.get_value("video", "fps_cap_divisor", fps_cap_divisor)
|
|
|
|
|
|
func save() -> void:
|
|
var cfg := ConfigFile.new()
|
|
cfg.set_value("video", "preset", preset)
|
|
cfg.set_value("video", "sdfgi_enabled", sdfgi_enabled)
|
|
cfg.set_value("video", "ssil_enabled", ssil_enabled)
|
|
cfg.set_value("video", "ssao_enabled", ssao_enabled)
|
|
cfg.set_value("video", "shadows_enabled", shadows_enabled)
|
|
cfg.set_value("video", "glow_enabled", glow_enabled)
|
|
cfg.set_value("video", "aa_mode", aa_mode)
|
|
cfg.set_value("video", "glow_scale", glow_scale)
|
|
cfg.set_value("video", "brightness", brightness)
|
|
cfg.set_value("video", "resolution_scale", resolution_scale)
|
|
cfg.set_value("video", "fsr_sharpness", fsr_sharpness)
|
|
cfg.set_value("video", "vsync_mode", vsync_mode)
|
|
cfg.set_value("video", "fps_cap_divisor", fps_cap_divisor)
|
|
cfg.save(SETTINGS_PATH)
|
|
|
|
|
|
# Pushes a preset's bundle into the individual fields and applies everything
|
|
# live. CUSTOM is a no-op bundle-wise — it only matters as a marker so
|
|
# set_custom_field() below knows not to silently revert to Custom itself.
|
|
func apply_preset(new_preset: Preset) -> void:
|
|
preset = new_preset
|
|
if PRESET_BUNDLES.has(new_preset):
|
|
var bundle: Dictionary = PRESET_BUNDLES[new_preset]
|
|
_applying_preset = true
|
|
sdfgi_enabled = bundle["sdfgi_enabled"]
|
|
ssil_enabled = bundle["ssil_enabled"]
|
|
ssao_enabled = bundle["ssao_enabled"]
|
|
shadows_enabled = bundle["shadows_enabled"]
|
|
glow_enabled = bundle["glow_enabled"]
|
|
aa_mode = bundle["aa_mode"]
|
|
resolution_scale = bundle["resolution_scale"]
|
|
_applying_preset = false
|
|
apply_aa()
|
|
apply_resolution_scale()
|
|
settings_changed.emit()
|
|
|
|
|
|
# Called by the settings menu whenever the player edits an individual
|
|
# preset-gated field directly (not via the preset dropdown) — flips to
|
|
# Custom so the dropdown reflects reality instead of silently lying about
|
|
# which preset is "selected". No-ops during apply_preset's own writes above.
|
|
func mark_custom() -> void:
|
|
if not _applying_preset:
|
|
preset = Preset.CUSTOM
|
|
|
|
|
|
func apply_aa() -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
return
|
|
var viewport := get_tree().root
|
|
match aa_mode:
|
|
AAMode.OFF:
|
|
viewport.msaa_3d = Viewport.MSAA_DISABLED
|
|
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
|
AAMode.FXAA:
|
|
viewport.msaa_3d = Viewport.MSAA_DISABLED
|
|
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
|
|
AAMode.MSAA:
|
|
viewport.msaa_3d = Viewport.MSAA_4X
|
|
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
|
AAMode.MSAA_FXAA:
|
|
viewport.msaa_3d = Viewport.MSAA_4X
|
|
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
|
|
AAMode.MSAA_2X:
|
|
viewport.msaa_3d = Viewport.MSAA_2X
|
|
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
|
|
|
|
|
func apply_resolution_scale() -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
return
|
|
var viewport := get_tree().root
|
|
if resolution_scale >= 0.999:
|
|
viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR
|
|
viewport.scaling_3d_scale = 1.0
|
|
else:
|
|
viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR2
|
|
viewport.scaling_3d_scale = clampf(resolution_scale, 0.5, 1.0)
|
|
viewport.fsr_sharpness = fsr_sharpness
|
|
|
|
|
|
func apply_vsync() -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
return
|
|
match vsync_mode:
|
|
VsyncMode.DISABLED:
|
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
|
VsyncMode.ENABLED:
|
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
|
|
VsyncMode.ADAPTIVE:
|
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE)
|
|
apply_fps_cap()
|
|
|
|
|
|
# Public (not just called from apply_vsync) because gameplay-scene transitions
|
|
# (main_menu.gd's _leave_to_gameplay) need to apply the player's chosen cap
|
|
# rather than hardcoding an uncapped 0 — the menu's own refresh-rate cap is a
|
|
# separate, menu-only concern (settings_menu.gd/main_menu.gd _ready()).
|
|
func apply_fps_cap() -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
return
|
|
if vsync_mode != VsyncMode.DISABLED or fps_cap_divisor <= 0:
|
|
Engine.max_fps = 0
|
|
return
|
|
var refresh := DisplayServer.screen_get_refresh_rate()
|
|
# refresh_rate query returning -1 (or 0, unlikely but not contractually
|
|
# excluded) falls back to uncapped rather than dividing by a negative
|
|
# number into a nonsense cap.
|
|
if refresh <= 0.0:
|
|
Engine.max_fps = 0
|
|
return
|
|
Engine.max_fps = maxi(1, roundi(refresh / float(fps_cap_divisor)))
|
|
|
|
|
|
# Called once by each arena's _ready() (and again on settings_changed, so an
|
|
# already-loaded arena updates live) to fold the user's glow/brightness
|
|
# preference into that arena's own baked Environment tuning, and to gate the
|
|
# preset-controlled full-screen passes (§5.5 of multiplayer-todo.md).
|
|
func apply_to_environment(env: Environment) -> void:
|
|
if env == null:
|
|
return
|
|
env.glow_enabled = glow_enabled and glow_scale > 0.0
|
|
env.glow_intensity *= glow_scale
|
|
env.adjustment_brightness *= brightness
|
|
env.sdfgi_enabled = sdfgi_enabled
|
|
env.ssil_enabled = ssil_enabled
|
|
env.ssao_enabled = ssao_enabled
|