diff --git a/API/RenderSession.cs b/API/RenderSession.cs
index 680c508..56ad5da 100644
--- a/API/RenderSession.cs
+++ b/API/RenderSession.cs
@@ -520,7 +520,9 @@ public class RenderSession
}
RenderManager.Instance.levelPath = levelPath;
- RenderManager.Instance.Configuration = renderConfig;
+ // The render uses its own copy of the configuration: values adjusted during the render
+ // (e.g. the audio sample rate in SetupPlay) stay in this copy and do not change the caller's object.
+ RenderManager.Instance.Configuration = renderConfig.Clone();
RenderManager.Instance.stage = RenderStage.Load;
diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs
index 34c4950..147da04 100644
--- a/UI/Window/RenderConfigWindow.cs
+++ b/UI/Window/RenderConfigWindow.cs
@@ -9,6 +9,7 @@ using ADOFAIRenderer.Data;
using ADOFAIRenderer.Localization;
using ADOFAIRenderer.Util.OS;
using MelonLoader;
+using Newtonsoft.Json;
using O5Kit.Control;
using O5Kit.Core;
using O5Kit.Factory;
@@ -32,6 +33,14 @@ internal static class RenderConfigWindow
private static Coroutine? _deleteResetCoroutine;
private static string? _lastLoadedPresetName;
+ ///
+ /// Configuration edited by this window. Kept for the whole game session; a render started from the window
+ /// receives a copy of it (RenderSession.Start), so values adjusted during the render are not written back here.
+ ///
+ private static RenderConfig? _uiConfig;
+
+ private static RenderConfig UiConfig => _uiConfig ??= new RenderConfig();
+
// Controls cache for validation & sync
private static O5Slider? _renderFpsSlider;
private static O5Slider? _videoFPSSlider;
@@ -111,8 +120,7 @@ internal static class RenderConfigWindow
ReloadPresetDropdown(GlobalSettings.Current.LastPresetName);
}
- // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
- var config = RenderManager.Instance.Configuration ??= new RenderConfig();
+ var config = UiConfig;
ApplyConfigToUI(config);
SetWindowActive(true);
@@ -125,8 +133,7 @@ internal static class RenderConfigWindow
var (_, scrollContent, _) =
O5Factory.ScrollView(ctx, content, spacing: 10f, padding: 8f);
- // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
- var config = RenderManager.Instance.Configuration ??= new RenderConfig();
+ var config = UiConfig;
EnsureDefaultConfig(config);
double[] renderFpsSnap = { 23.976, 24, 29.97, 30, 59.94, 60, 144,
@@ -744,11 +751,14 @@ internal static class RenderConfigWindow
{
EnsureDefaultConfig(config);
- _renderFpsSlider?.Set((float) config.RenderFPS, false);
- _videoFPSSlider?.Set((float) config.VideoFPS, false);
- _renderWidthSlider?.Set(config.RenderWidth, false);
- _renderHeightSlider?.Set(config.RenderHeight, false);
- _delayAfterEndSlider?.Set((float) config.DelayAfterEnd, false);
+ // Sliders show the config values as they are (noFilter: true).
+ // O5Slider.Set applies the slider filter (snapping to the nearest snap value) unless noFilter is set;
+ // the filter is meant for dragging, and typed values are not snapped either (O5Slider.SetFromInput).
+ _renderFpsSlider?.Set(config.RenderFPS, false, noFilter: true);
+ _videoFPSSlider?.Set(config.VideoFPS, false, noFilter: true);
+ _renderWidthSlider?.Set(config.RenderWidth, false, noFilter: true);
+ _renderHeightSlider?.Set(config.RenderHeight, false, noFilter: true);
+ _delayAfterEndSlider?.Set(config.DelayAfterEnd, false, noFilter: true);
_showJudgementToggle?.Set(config.ShowJudgementText, false);
_codecDropdown?.Set(config.VideoConfig.Codec, false);
@@ -756,13 +766,13 @@ internal static class RenderConfigWindow
_bitrateInput?.Set(config.VideoConfig.Bitrate.ToString(), false);
_maxBitrateInput?.Set(config.VideoConfig.MaxBitrate.ToString(), false);
_qpInput?.Set(config.VideoConfig.QP.ToString(), false);
- _keyframeIntervalSlider?.Set(config.VideoConfig.KeyframeInterval, false);
+ _keyframeIntervalSlider?.Set(config.VideoConfig.KeyframeInterval, false, noFilter: true);
_forceSoftwareToggle?.Set(config.VideoConfig.ForceSoftwareEncoder, false);
_renderAudioToggle?.Set(config.AudioConfig.RenderAudio, false);
_sampleRateDropdown?.Set(config.AudioConfig.AudioSampleRate, false);
_channelsDropdown?.Set(config.AudioConfig.AudioChannels, false);
- _audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false);
+ _audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false, noFilter: true);
_outputDirInput?.Set(GlobalSettings.EffectiveOutputDirectory, false);
@@ -789,17 +799,51 @@ internal static class RenderConfigWindow
UpdateVideoCodecUI(config.VideoConfig.Codec, config);
UpdateRateControlUI(config.VideoConfig.RateControlMode, noAnimate: true);
- if (!string.IsNullOrEmpty(GlobalSettings.Current.LastPresetName))
+ UpdatePresetLabel(config);
+ }
+
+ ///
+ /// Sets the preset dropdown from the config values.
+ /// Every value equals the last loaded preset (_lastLoadedPresetName): the preset name is shown.
+ /// Any value differs (edited, or the preset file changed): the Custom label is shown (MarkConfigModified),
+ /// the same state as after editing a value in the window.
+ ///
+ private static void UpdatePresetLabel(RenderConfig config)
+ {
+ if (_presetDropdown == null) return;
+
+ var presetName = _lastLoadedPresetName;
+ var preset = string.IsNullOrEmpty(presetName)
+ ? null
+ : PresetManager.GetPresets().FirstOrDefault(p => string.Equals(p.Name, presetName, StringComparison.OrdinalIgnoreCase));
+
+ if (preset == null || !ConfigValuesEqual(config, preset.Config))
{
- _presetDropdown?.Set(GlobalSettings.Current.LastPresetName, false);
- _lastLoadedPresetName = GlobalSettings.Current.LastPresetName;
- UpdateDeleteButtonState();
+ MarkConfigModified();
+ return;
}
+
+ if (_presetDropdown.Values.Contains(CustomPresetLabel))
+ {
+ _presetDropdown.SetValues(_presetDropdown.Values.Where(v => v != CustomPresetLabel).ToList());
+ }
+ _presetDropdown.Set(preset.Name, false);
+ UpdateDeleteButtonState();
+ }
+
+ ///
+ /// Compares two configs by their serialized JSON (the same form as the preset files), so every saved value is compared.
+ ///
+ private static bool ConfigValuesEqual(RenderConfig a, RenderConfig b)
+ {
+ var settings = new JsonSerializerSettings();
+ settings.Converters.Add(new ColorJsonConverter());
+ return JsonConvert.SerializeObject(a, settings) == JsonConvert.SerializeObject(b, settings);
}
private static void TryGoRender()
{
- var config = RenderManager.Instance.Configuration;
+ var config = UiConfig;
SyncUItoConfig(config);
// Sync Output Directory