SHA256
Fix Render Freeze on Encoder Thread Failure
This commit is contained in:
4 files changed
+99
-19
No files matched your search
@@ -36,6 +36,7 @@ desktop.ini
|
|||||||
/.agent/
|
/.agent/
|
||||||
/GEMINI.md
|
/GEMINI.md
|
||||||
/.mcp.json
|
/.mcp.json
|
||||||
|
/CLAUDE.md
|
||||||
|
|
||||||
# Decompiled Game Code
|
# Decompiled Game Code
|
||||||
*.cs.txt
|
*.cs.txt
|
||||||
@@ -44,3 +45,6 @@ desktop.ini
|
|||||||
/AsyncInputAutoplayFix.md
|
/AsyncInputAutoplayFix.md
|
||||||
/AudioMixerExplanation.md
|
/AudioMixerExplanation.md
|
||||||
/O5Theme.md
|
/O5Theme.md
|
||||||
|
|
||||||
|
# Developer Utility Files
|
||||||
|
/REVIEW_CHECKLIST.md
|
||||||
+93
-17
@@ -91,7 +91,50 @@ internal class RenderManager : MonoBehaviour
|
|||||||
public long frameCount;
|
public long frameCount;
|
||||||
public bool skipPressToStart;
|
public bool skipPressToStart;
|
||||||
public string levelPath = "";
|
public string levelPath = "";
|
||||||
public bool IsRenderCancelled { get; private set; }
|
private readonly object _abortStateLock = new();
|
||||||
|
private volatile bool _isRenderCancelled;
|
||||||
|
private volatile bool _renderFailed;
|
||||||
|
private volatile string? _failureReason;
|
||||||
|
|
||||||
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
|
public bool IsRenderCancelled => _isRenderCancelled;
|
||||||
|
public bool IsRenderFailed => _renderFailed;
|
||||||
|
|
||||||
|
// Cancelled and failed are mutually exclusive; this is true for either (render must stop).
|
||||||
|
public bool IsRenderAborted => _isRenderCancelled || _renderFailed;
|
||||||
|
|
||||||
|
private void MarkCancelled()
|
||||||
|
{
|
||||||
|
lock (_abortStateLock)
|
||||||
|
{
|
||||||
|
if (!_renderFailed) _isRenderCancelled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first failure reason wins; ignored once the render is already cancelled or failed.
|
||||||
|
private void MarkFailed(string reason)
|
||||||
|
{
|
||||||
|
lock (_abortStateLock)
|
||||||
|
{
|
||||||
|
if (_isRenderCancelled || _renderFailed) return;
|
||||||
|
_failureReason = reason;
|
||||||
|
_renderFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearAbortState()
|
||||||
|
{
|
||||||
|
lock (_abortStateLock)
|
||||||
|
{
|
||||||
|
_isRenderCancelled = false;
|
||||||
|
_renderFailed = false;
|
||||||
|
_failureReason = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildAbortMessage() => IsRenderFailed
|
||||||
|
? $"Rendering failed: {_failureReason ?? "unknown error"}"
|
||||||
|
: "Rendering was cancelled by user.";
|
||||||
public RenderConfig Configuration = null!;
|
public RenderConfig Configuration = null!;
|
||||||
|
|
||||||
// Render Sessions State (OOP Instance Fields)
|
// Render Sessions State (OOP Instance Fields)
|
||||||
@@ -151,6 +194,7 @@ internal class RenderManager : MonoBehaviour
|
|||||||
|
|
||||||
public long frameCountForVideo;
|
public long frameCountForVideo;
|
||||||
public long lastVideoFrameIndex = -1;
|
public long lastVideoFrameIndex = -1;
|
||||||
|
// long cannot be volatile: access via Volatile.Read/Write (written on main thread, read on render thread)
|
||||||
public long lastRequestedVideoFrameIndex = -1;
|
public long lastRequestedVideoFrameIndex = -1;
|
||||||
public int readbackFailureCount;
|
public int readbackFailureCount;
|
||||||
public int throttleCount;
|
public int throttleCount;
|
||||||
@@ -170,8 +214,8 @@ internal class RenderManager : MonoBehaviour
|
|||||||
internal AutoResetEvent FrameAvailableEvent = new(false);
|
internal AutoResetEvent FrameAvailableEvent = new(false);
|
||||||
public Thread? RenderThread;
|
public Thread? RenderThread;
|
||||||
public long passCount;
|
public long passCount;
|
||||||
public bool endRequested;
|
public volatile bool endRequested;
|
||||||
public bool endSuccessful;
|
public volatile bool endSuccessful;
|
||||||
public long endFrame = -1;
|
public long endFrame = -1;
|
||||||
|
|
||||||
// Private Instance Fields
|
// Private Instance Fields
|
||||||
@@ -195,12 +239,15 @@ internal class RenderManager : MonoBehaviour
|
|||||||
|
|
||||||
public void ResetRenderState(bool success)
|
public void ResetRenderState(bool success)
|
||||||
{
|
{
|
||||||
IsRenderCancelled = !success;
|
if (success) _isRenderCancelled = false;
|
||||||
|
else MarkCancelled();
|
||||||
stage = RenderStage.None;
|
stage = RenderStage.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CleanUpActiveRender()
|
public void CleanUpActiveRender()
|
||||||
{
|
{
|
||||||
|
if (stage != RenderStage.None)
|
||||||
|
MarkFailed("the game exited or RenderManager was destroyed during rendering.");
|
||||||
ResetRenderState(success: false);
|
ResetRenderState(success: false);
|
||||||
|
|
||||||
AudioSpectrumGenerator.Clear();
|
AudioSpectrumGenerator.Clear();
|
||||||
@@ -283,8 +330,15 @@ internal class RenderManager : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Instance.stage < RenderStage.OnScene) return;
|
if (Instance.stage < RenderStage.OnScene) return;
|
||||||
|
if (stage >= RenderStage.Render && RenderThread is not { IsAlive: true })
|
||||||
|
{
|
||||||
|
MarkFailed("the render thread terminated unexpectedly.");
|
||||||
|
CancelRender();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
|
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
|
||||||
{
|
{
|
||||||
|
MarkFailed("the level failed or the game controller was lost during rendering.");
|
||||||
ResetRenderState(success: false);
|
ResetRenderState(success: false);
|
||||||
StopFFmpeg(false);
|
StopFFmpeg(false);
|
||||||
EndRender();
|
EndRender();
|
||||||
@@ -333,6 +387,12 @@ internal class RenderManager : MonoBehaviour
|
|||||||
|
|
||||||
while (Instance.stage >= RenderStage.Render)
|
while (Instance.stage >= RenderStage.Render)
|
||||||
{
|
{
|
||||||
|
if (RenderThread is not { IsAlive: true })
|
||||||
|
{
|
||||||
|
isThrottling = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int count;
|
int count;
|
||||||
lock (ImageQueue)
|
lock (ImageQueue)
|
||||||
{
|
{
|
||||||
@@ -362,7 +422,7 @@ internal class RenderManager : MonoBehaviour
|
|||||||
if (videoFrameIndex > lastVideoFrameIndex)
|
if (videoFrameIndex > lastVideoFrameIndex)
|
||||||
{
|
{
|
||||||
lastVideoFrameIndex = videoFrameIndex;
|
lastVideoFrameIndex = videoFrameIndex;
|
||||||
lastRequestedVideoFrameIndex = videoFrameIndex;
|
Volatile.Write(ref lastRequestedVideoFrameIndex, videoFrameIndex);
|
||||||
|
|
||||||
var targetTexture = texture;
|
var targetTexture = texture;
|
||||||
if (HasActiveFrameHooks && _frameHookPipeline != null)
|
if (HasActiveFrameHooks && _frameHookPipeline != null)
|
||||||
@@ -403,7 +463,6 @@ internal class RenderManager : MonoBehaviour
|
|||||||
|
|
||||||
Application.wantsToQuit -= quitDelegate;
|
Application.wantsToQuit -= quitDelegate;
|
||||||
RenderConfigWindow.CreateWindow();
|
RenderConfigWindow.CreateWindow();
|
||||||
// ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,10 +570,10 @@ internal class RenderManager : MonoBehaviour
|
|||||||
ImageQueue.Clear();
|
ImageQueue.Clear();
|
||||||
}
|
}
|
||||||
passCount = 0;
|
passCount = 0;
|
||||||
IsRenderCancelled = false;
|
ClearAbortState();
|
||||||
frameCountForVideo = 0;
|
frameCountForVideo = 0;
|
||||||
lastVideoFrameIndex = -1;
|
lastVideoFrameIndex = -1;
|
||||||
lastRequestedVideoFrameIndex = -1;
|
Volatile.Write(ref lastRequestedVideoFrameIndex, -1);
|
||||||
endRequested = false;
|
endRequested = false;
|
||||||
endSuccessful = false;
|
endSuccessful = false;
|
||||||
FrameAvailableEvent.Reset();
|
FrameAvailableEvent.Reset();
|
||||||
@@ -655,17 +714,17 @@ internal class RenderManager : MonoBehaviour
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsRenderCancelled) break;
|
if (IsRenderAborted) break;
|
||||||
|
|
||||||
if (endRequested)
|
if (endRequested)
|
||||||
{
|
{
|
||||||
if (passCount > lastRequestedVideoFrameIndex) break;
|
if (passCount > Volatile.Read(ref lastRequestedVideoFrameIndex)) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameAvailableEvent.WaitOne(50);
|
FrameAvailableEvent.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsRenderCancelled) videoFFmpeg.End();
|
if (!IsRenderAborted) videoFFmpeg.End();
|
||||||
videoFFmpeg.Dispose();
|
videoFFmpeg.Dispose();
|
||||||
|
|
||||||
lock (_allVideoFFmpegInstances)
|
lock (_allVideoFFmpegInstances)
|
||||||
@@ -674,12 +733,22 @@ internal class RenderManager : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
_activeVideoFFmpeg = null;
|
_activeVideoFFmpeg = null;
|
||||||
endSuccessful = !IsRenderCancelled;
|
endSuccessful = !IsRenderAborted;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Melon<RenderMod>.Logger.Error($"FFmpeg rendering thread error: {ex}");
|
Melon<RenderMod>.Logger.Error($"FFmpeg rendering thread error: {ex}");
|
||||||
|
|
||||||
|
MarkFailed("encoder thread error. See the log for details.");
|
||||||
|
try
|
||||||
|
{
|
||||||
videoFFmpeg.Dispose();
|
videoFFmpeg.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception disposeEx)
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Error($"Encoder dispose after thread error also failed: {disposeEx.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
lock (_allVideoFFmpegInstances)
|
lock (_allVideoFFmpegInstances)
|
||||||
{
|
{
|
||||||
_allVideoFFmpegInstances.Remove(videoFFmpeg);
|
_allVideoFFmpegInstances.Remove(videoFFmpeg);
|
||||||
@@ -708,7 +777,7 @@ internal class RenderManager : MonoBehaviour
|
|||||||
endRequested = true;
|
endRequested = true;
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
IsRenderCancelled = true;
|
MarkCancelled();
|
||||||
}
|
}
|
||||||
FrameAvailableEvent.Set();
|
FrameAvailableEvent.Set();
|
||||||
skipPressToStart = false;
|
skipPressToStart = false;
|
||||||
@@ -722,7 +791,8 @@ internal class RenderManager : MonoBehaviour
|
|||||||
if (success) StartCoroutine(StopRenderCoroutine());
|
if (success) StartCoroutine(StopRenderCoroutine());
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Melon<RenderMod>.Logger.Error("Rendering failed or was cancelled. Safely cleaning up state and resources.");
|
var reason = IsRenderFailed ? $"FAILED ({_failureReason})" : "CANCELLED";
|
||||||
|
Melon<RenderMod>.Logger.Error($"Rendering {reason}. Safely cleaning up state and resources.");
|
||||||
|
|
||||||
// 1. Wait for render background thread to finish processing remaining queued frames and terminate
|
// 1. Wait for render background thread to finish processing remaining queued frames and terminate
|
||||||
if (RenderThread is { IsAlive: true })
|
if (RenderThread is { IsAlive: true })
|
||||||
@@ -796,15 +866,20 @@ internal class RenderManager : MonoBehaviour
|
|||||||
CurrentOutPath = null;
|
CurrentOutPath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = BuildResult(false,
|
var result = BuildResult(false, BuildAbortMessage());
|
||||||
IsRenderCancelled ? "Rendering was cancelled by user." : "Rendering failed.");
|
|
||||||
TriggerOnComplete(result);
|
TriggerOnComplete(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator StopRenderCoroutine()
|
private IEnumerator StopRenderCoroutine()
|
||||||
{
|
{
|
||||||
yield return new WaitUntil(() => endSuccessful);
|
yield return new WaitUntil(() => endSuccessful || RenderThread is not { IsAlive: true });
|
||||||
|
if (!endSuccessful)
|
||||||
|
{
|
||||||
|
MarkFailed("the render thread terminated unexpectedly.");
|
||||||
|
CancelRender();
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
var result = BuildResult(success: true);
|
var result = BuildResult(success: true);
|
||||||
TriggerOnComplete(result);
|
TriggerOnComplete(result);
|
||||||
@@ -849,6 +924,7 @@ internal class RenderManager : MonoBehaviour
|
|||||||
AudioMixer?.Reset(); AudioMixer = null;
|
AudioMixer?.Reset(); AudioMixer = null;
|
||||||
SyncCalculator.Reset();
|
SyncCalculator.Reset();
|
||||||
|
|
||||||
|
AudioSpectrumGenerator.Clear();
|
||||||
RenderContext.Reset();
|
RenderContext.Reset();
|
||||||
|
|
||||||
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
|
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ internal static class AudioSpectrumGenerator
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentFrame = (RenderManager.Instance != null) ? RenderManager.Instance.frameCount : -1;
|
var currentFrame = RenderManager.Instance ? RenderManager.Instance.frameCount : -1;
|
||||||
|
|
||||||
lock (CachedSpectrums)
|
lock (CachedSpectrums)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ namespace ADOFAIRenderer.Util
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RenderManager.Instance && RenderManager.Instance.IsRenderCancelled)
|
if (RenderManager.Instance && RenderManager.Instance.IsRenderAborted)
|
||||||
{
|
{
|
||||||
RecycleState(state);
|
RecycleState(state);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in new issue
Block a user