diff --git a/Data/RenderConfig.cs b/Data/RenderConfig.cs index 8e2a90c..bc6bfc7 100644 --- a/Data/RenderConfig.cs +++ b/Data/RenderConfig.cs @@ -102,6 +102,15 @@ public class AdvancedConfig /// public bool HighResBloomCorrection { get; set; } = false; + // === [MARKER: FIX_SET_FRAMERATE_BUG START] === + /// + /// When true, fixes in-game and render bugs during SetFramerate events (where objects disappear on the first frame + /// or parallax/tiles lag behind) by deferring manual camera rendering to the end of the frame after all LateUpdates. + /// Default true. + /// + public bool FixCustomFrameRate { get; set; } = true; + // === [MARKER: FIX_SET_FRAMERATE_BUG END] === + /// File extension of the default output file name, without the dot (e.g. "mkv", "mp4"); selects the container. public string FileExtension { get; set; } = "mkv"; @@ -128,6 +137,9 @@ public class AdvancedConfig ProcessAudio = source.ProcessAudio; NonTile = source.NonTile; HighResBloomCorrection = source.HighResBloomCorrection; + // === [MARKER: FIX_SET_FRAMERATE_BUG START] === + FixCustomFrameRate = source.FixCustomFrameRate; + // === [MARKER: FIX_SET_FRAMERATE_BUG END] === FileExtension = source.FileExtension; firePlanetColor = source.firePlanetColor; icePlanetColor = source.icePlanetColor; diff --git a/Localization/Locale/en.json b/Localization/Locale/en.json index ecae246..4540cd9 100644 --- a/Localization/Locale/en.json +++ b/Localization/Locale/en.json @@ -43,6 +43,7 @@ "ui.render_config.process_audio": "Process Audio", "ui.render_config.non_tile": "Non-Tile Mode", "ui.render_config.high_res_bloom_correction": "High-Resolution Bloom Correction", + "ui.render_config.fix_custom_frame_rate": "Fix Custom FPS Render Timing", "ui.render_config.random_seed": "Random Seed (Optional)", "ui.render_config.validation_title": "Invalid Settings", "ui.render_config.validation_error": "Please check your input values:\n- Width and Height must be integers > 0\n- Render FPS and Video FPS must be > 0\n- Delay After End must be >= 0", diff --git a/Localization/Locale/ko.json b/Localization/Locale/ko.json index c54434d..6412a89 100644 --- a/Localization/Locale/ko.json +++ b/Localization/Locale/ko.json @@ -43,6 +43,7 @@ "ui.render_config.process_audio": "오디오 처리", "ui.render_config.non_tile": "논타일 모드", "ui.render_config.high_res_bloom_correction": "고해상도 블룸 보정", + "ui.render_config.fix_custom_frame_rate": "커스텀 FPS 렌더 타이밍 보정", "ui.render_config.random_seed": "랜덤 시드 (선택 사항)", "ui.render_config.validation_title": "설정 오류", "ui.render_config.validation_error": "입력값을 확인해 주세요:\n- 너비와 높이는 0보다 큰 정수여야 합니다.\n- 렌더 FPS와 영상 FPS는 0보다 커야 합니다.\n- 종료 후 대기 시간은 0 이상이어야 합니다.", diff --git a/Patch/Game/CustomFrameRateDeferPatch.cs b/Patch/Game/CustomFrameRateDeferPatch.cs new file mode 100644 index 0000000..6f4f116 --- /dev/null +++ b/Patch/Game/CustomFrameRateDeferPatch.cs @@ -0,0 +1,115 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using ADOFAIRenderer.Core; +using HarmonyLib; +using UnityEngine; + +namespace ADOFAIRenderer.Patch.Game; + +// === [MARKER: FIX_SET_FRAMERATE_BUG START] === +/// +/// Fixes the in-game and render bug where objects (floors, decorations, planets) disappear on the first frame of +/// a SetFramerate event (due to camera Z=0 near clip culling) and where parallax/tiles lag behind on subsequent frames. +/// +/// Defers the manual camera rendering (scrCamera.UpdateCustomFrameRateScreen) to the very end of the frame +/// (execution order int.MaxValue - 1), after all components (scrCamera, scrFloor, +/// scrDecorationManager, scrPlanet, scrParallax, and scrController) have finished their +/// LateUpdate. +/// +/// +[HarmonyPatch] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static class CustomFrameRateDeferPatch +{ + private static bool _isExecutingDeferred; + private static scrCamera? _pendingCamera; + + private static readonly Action? UpdateCustomFrameRateScreenInvoker = + AccessTools.MethodDelegate>(AccessTools.Method(typeof(scrCamera), "UpdateCustomFrameRateScreen")); + + /// + /// Checks whether the SetFramerate render timing fix is enabled. + /// Active if rendering session has the option enabled, or enabled by default during normal in-game play. + /// + public static bool IsEnabled + { + get + { + if (RenderManager.IsActive && RenderManager.Instance?.Configuration?.AdvancedConfig != null) + { + return RenderManager.Instance.Configuration.AdvancedConfig.FixCustomFrameRate; + } + + return true; + } + } + + /// + /// Intercepts manual camera render calls from SetCustomFrameRate (Update phase) or CheckUpdateFrameRateScreen + /// (LateUpdate phase). If not currently executing the deferred render pass, records the camera instance and + /// suppresses the premature render. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(scrCamera), "UpdateCustomFrameRateScreen")] + private static bool DeferManualRender(scrCamera __instance) + { + if (!IsEnabled) return true; + if (_isExecutingDeferred) return true; + + _pendingCamera = __instance; + return false; + } + + /// + /// Ensures that the runner component is attached to the camera when it starts. + /// + [HarmonyPostfix] + [HarmonyPatch(typeof(scrCamera), "Start")] + private static void AttachRunner(scrCamera __instance) + { + if (!__instance) return; + if (!__instance.gameObject.GetComponent()) + { + __instance.gameObject.AddComponent(); + } + } + + /// + /// Invoked at the very end of LateUpdate (Order int.MaxValue - 1). If a manual render was requested during the frame, + /// executes the camera's original UpdateCustomFrameRateScreen once with all objects fully prepared. + /// + internal static void ExecuteDeferredRender() + { + if (_pendingCamera == null) return; + + var cam = _pendingCamera; + _pendingCamera = null; + + if (!cam) return; + + _isExecutingDeferred = true; + try + { + UpdateCustomFrameRateScreenInvoker?.Invoke(cam); + } + finally + { + _isExecutingDeferred = false; + } + } +} + +/// +/// Runner component attached to scrCamera that executes deferred custom frame rate renders at the end of LateUpdate. +/// Execution order int.MaxValue - 1 guarantees it runs after all normal game components (including +/// scrController at 400), but right before PostRenderHook at int.MaxValue. +/// +[DefaultExecutionOrder(int.MaxValue - 1)] +internal class CustomFrameRateDeferredRunner : MonoBehaviour +{ + private void LateUpdate() + { + CustomFrameRateDeferPatch.ExecuteDeferredRender(); + } +} +// === [MARKER: FIX_SET_FRAMERATE_BUG END] === diff --git a/RenderMod.cs b/RenderMod.cs index 524af96..eb9cac6 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -9,7 +9,7 @@ using MelonLoader.Utils; using UnityEngine; using Object = UnityEngine.Object; -[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.3.1-INDEV", "ADOFAI Editor Forum")] +[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.3.2-INDEV", "ADOFAI Editor Forum")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] namespace ADOFAIRenderer; diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index ef29a38..c7e9ba2 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -140,6 +140,9 @@ internal static class RenderConfigWindow private static O5Toggle? _processAudioToggle; private static O5Toggle? _nonTileToggle; private static O5Toggle? _highResBloomCorrectionToggle; + // === [MARKER: FIX_SET_FRAMERATE_BUG START] === + private static O5Toggle? _fixCustomFrameRateToggle; + // === [MARKER: FIX_SET_FRAMERATE_BUG END] === // Planet color overrides: the toggle enables the override, the picker holds the color. A disabled toggle stores // null in AdvancedConfig (no override is applied by PersistencePatch) and blocks the picker. @@ -791,6 +794,24 @@ internal static class RenderConfigWindow generateLabel: false ); + // === [MARKER: FIX_SET_FRAMERATE_BUG START] === + // Fix SetFramerate Render Timing (AdvancedConfig.FixCustomFrameRate): defers manual camera rendering to frame end + _fixCustomFrameRateToggle = CreateToggleField( + ctx, CreateFieldSlot(advRight), + labelKey: "", + defaultValue: true, + value: config.AdvancedConfig.FixCustomFrameRate, + toggleTextKey: "ui.render_config.fix_custom_frame_rate", + id: "toggle_fix_custom_frame_rate", + onChanged: val => + { + config.AdvancedConfig.FixCustomFrameRate = val; + MarkConfigModified(); + }, + generateLabel: false + ); + // === [MARKER: FIX_SET_FRAMERATE_BUG END] === + // 7-1. Planet Color Settings (Optional). // For each planet: the toggle turns the override on/off (off stores null and blocks/collapses the picker); // the picker writes its color into the config only while the toggle is on. @@ -1000,6 +1021,9 @@ internal static class RenderConfigWindow _processAudioToggle?.Set(config.AdvancedConfig.ProcessAudio, false); _nonTileToggle?.Set(config.AdvancedConfig.NonTile, false); _highResBloomCorrectionToggle?.Set(config.AdvancedConfig.HighResBloomCorrection, false); + // === [MARKER: FIX_SET_FRAMERATE_BUG START] === + _fixCustomFrameRateToggle?.Set(config.AdvancedConfig.FixCustomFrameRate, false); + // === [MARKER: FIX_SET_FRAMERATE_BUG END] === // Planet colors: toggle on when an override is stored; the picker shows the stored color or the default and is // blocked without an override @@ -1182,6 +1206,9 @@ internal static class RenderConfigWindow if (_processAudioToggle != null) target.AdvancedConfig.ProcessAudio = _processAudioToggle.Value; if (_nonTileToggle != null) target.AdvancedConfig.NonTile = _nonTileToggle.Value; if (_highResBloomCorrectionToggle != null) target.AdvancedConfig.HighResBloomCorrection = _highResBloomCorrectionToggle.Value; + // === [MARKER: FIX_SET_FRAMERATE_BUG START] === + if (_fixCustomFrameRateToggle != null) target.AdvancedConfig.FixCustomFrameRate = _fixCustomFrameRateToggle.Value; + // === [MARKER: FIX_SET_FRAMERATE_BUG END] === // Planet Colors target.AdvancedConfig.firePlanetColor = (_firePlanetToggle?.Value == true && _firePlanetPicker != null)