Compare commits

3 Commits
4 changed files with 229 additions and 123 deletions

No files matched your search

+164 -123
View File
@@ -682,129 +682,8 @@ internal class RenderManager : MonoBehaviour
throw; throw;
} }
RenderThread = new Thread(() => var encoder = videoFFmpeg!;
{ RenderThread = new Thread(() => RunRenderThread(encoder))
try
{
while (true)
{
CapturedFrame frame = default;
bool hasFrame = false;
lock (ImageQueue)
{
if (ImageQueue.Count > 0)
{
var firstKey = ImageQueue.Keys[0];
if (firstKey > passCount && Instance.stage < RenderStage.Render)
{
passCount = firstKey;
}
if (firstKey == passCount)
{
frame = ImageQueue[firstKey];
ImageQueue.RemoveAt(0);
passCount++;
hasFrame = true;
}
}
}
if (hasFrame)
{
if (frame.ManagedBuffer != null)
{
if (frame.ManagedBuffer.Length > 0)
{
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted);
Interlocked.Increment(ref framesSent);
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
}
}
else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0)
{
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted);
Interlocked.Increment(ref framesSent);
if (frame.ReadbackState != null)
{
_frameReader?.RecycleState(frame.ReadbackState);
}
}
if (Configuration.AudioConfig.RenderAudio && SyncCalculator is { } syncCalculator)
{
var frameIdx = (int)(passCount - 1);
var effectiveVideoFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS);
var frameTime = frameIdx / effectiveVideoFps;
var channels = Configuration.AudioConfig.AudioChannels;
var samplesNeeded = syncCalculator.GetNextFrameSamples();
var audioBuffer = ArrayPool<float>.Shared.Rent(samplesNeeded * channels);
try
{
AudioMixer?.GenerateAudioForFrame(audioBuffer, samplesNeeded, frameTime, dspTimeBase);
videoFFmpeg.WriteAudioSamples(audioBuffer, samplesNeeded);
}
finally
{
ArrayPool<float>.Shared.Return(audioBuffer);
}
}
continue;
}
if (IsRenderAborted) break;
if (endRequested)
{
if (passCount > Volatile.Read(ref lastRequestedVideoFrameIndex)) break;
}
FrameAvailableEvent.WaitOne(50);
}
if (!IsRenderAborted)
{
// End throws when the file cannot be finished (e.g. the trailer cannot be written),
// which is handled as a render failure by the catch block below.
videoFFmpeg.End();
Volatile.Write(ref _incompleteOutputPath, null);
}
videoFFmpeg.Dispose();
lock (_allVideoFFmpegInstances)
{
_allVideoFFmpegInstances.Remove(videoFFmpeg);
}
_activeVideoFFmpeg = null;
endSuccessful = !IsRenderAborted;
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"FFmpeg rendering thread error: {ex}");
MarkFailed("encoder thread error. See the log for details.");
try
{
videoFFmpeg.Dispose();
}
catch (Exception disposeEx)
{
Melon<RenderMod>.Logger.Error($"Encoder dispose after thread error also failed: {disposeEx.Message}");
}
lock (_allVideoFFmpegInstances)
{
_allVideoFFmpegInstances.Remove(videoFFmpeg);
}
_activeVideoFFmpeg = null;
}
})
{ {
IsBackground = true, IsBackground = true,
Priority = ThreadPriority.AboveNormal Priority = ThreadPriority.AboveNormal
@@ -813,6 +692,168 @@ internal class RenderManager : MonoBehaviour
RenderThread.Start(); RenderThread.Start();
} }
/// <summary>
/// Body of the render thread. Sends queued frames and their audio to the encoder until the render ends,
/// finishes the output file, and releases the encoder. An exception is handled as a render failure.
/// </summary>
private void RunRenderThread(FFmpegNativeEncoder videoFFmpeg)
{
try
{
EncodeQueuedFrames(videoFFmpeg);
if (!IsRenderAborted)
{
// End throws when the file cannot be finished (e.g. the trailer cannot be written),
// which is handled as a render failure by the catch block below.
videoFFmpeg.End();
Volatile.Write(ref _incompleteOutputPath, null);
}
videoFFmpeg.Dispose();
UnregisterEncoder(videoFFmpeg);
endSuccessful = !IsRenderAborted;
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"FFmpeg rendering thread error: {ex}");
MarkFailed("encoder thread error. See the log for details.");
try
{
videoFFmpeg.Dispose();
}
catch (Exception disposeEx)
{
Melon<RenderMod>.Logger.Error($"Encoder dispose after thread error also failed: {disposeEx.Message}");
}
UnregisterEncoder(videoFFmpeg);
}
}
/// <summary>
/// Takes frames from ImageQueue in passCount order and sends each one to the encoder, followed by the audio
/// samples of that frame. Waits on FrameAvailableEvent (50ms) while no frame is ready.
/// Returns when the render is aborted or all requested frames have been sent after endRequested.
/// </summary>
private void EncodeQueuedFrames(FFmpegNativeEncoder videoFFmpeg)
{
while (true)
{
if (TryDequeueNextFrame(out var frame))
{
SendVideoFrame(videoFFmpeg, frame);
WriteAudioForFrame(videoFFmpeg);
continue;
}
if (ShouldStopEncodeLoop()) return;
FrameAvailableEvent.WaitOne(50);
}
}
/// <summary>
/// Removes the frame whose key equals passCount from ImageQueue and advances passCount.
/// Before the Render stage, passCount jumps to the first queued key (the first captured frame index is not 0).
/// Returns false when the queue is empty or the next frame has not arrived yet.
/// </summary>
private bool TryDequeueNextFrame(out CapturedFrame frame)
{
frame = default;
lock (ImageQueue)
{
if (ImageQueue.Count == 0) return false;
var firstKey = ImageQueue.Keys[0];
if (firstKey > passCount && Instance.stage < RenderStage.Render)
{
passCount = firstKey;
}
if (firstKey != passCount) return false;
frame = ImageQueue[firstKey];
ImageQueue.RemoveAt(0);
passCount++;
return true;
}
}
/// <summary>
/// Sends one captured frame to the encoder and releases its buffer (managed: ArrayPool, native: readback state).
/// A frame without data is skipped.
/// </summary>
private void SendVideoFrame(FFmpegNativeEncoder videoFFmpeg, in CapturedFrame frame)
{
if (frame.ManagedBuffer != null)
{
if (frame.ManagedBuffer.Length == 0) return;
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted);
Interlocked.Increment(ref framesSent);
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
return;
}
if (frame.NativePointer == IntPtr.Zero || frame.Length <= 0) return;
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted);
Interlocked.Increment(ref framesSent);
if (frame.ReadbackState != null)
{
_frameReader?.RecycleState(frame.ReadbackState);
}
}
/// <summary>
/// Mixes the audio of the frame that was just sent (index passCount - 1) and writes it to the encoder.
/// Does nothing when audio rendering is off.
/// </summary>
private void WriteAudioForFrame(FFmpegNativeEncoder videoFFmpeg)
{
if (!Configuration.AudioConfig.RenderAudio || SyncCalculator is not { } syncCalculator) return;
var frameIdx = (int)(passCount - 1);
var effectiveVideoFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS);
var frameTime = frameIdx / effectiveVideoFps;
var channels = Configuration.AudioConfig.AudioChannels;
var samplesNeeded = syncCalculator.GetNextFrameSamples();
var audioBuffer = ArrayPool<float>.Shared.Rent(samplesNeeded * channels);
try
{
AudioMixer?.GenerateAudioForFrame(audioBuffer, samplesNeeded, frameTime, dspTimeBase);
videoFFmpeg.WriteAudioSamples(audioBuffer, samplesNeeded);
}
finally
{
ArrayPool<float>.Shared.Return(audioBuffer);
}
}
/// <summary>
/// True when the render is aborted, or when the end was requested and every requested frame has been sent.
/// </summary>
private bool ShouldStopEncodeLoop()
{
if (IsRenderAborted) return true;
return endRequested && passCount > Volatile.Read(ref lastRequestedVideoFrameIndex);
}
private void UnregisterEncoder(FFmpegNativeEncoder videoFFmpeg)
{
lock (_allVideoFFmpegInstances)
{
_allVideoFFmpegInstances.Remove(videoFFmpeg);
}
_activeVideoFFmpeg = null;
}
public void CancelRender() public void CancelRender()
{ {
ResetRenderState(success: false); ResetRenderState(success: false);
+25
View File
@@ -0,0 +1,25 @@
using ADOFAIRenderer.Core;
using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
[HarmonyPatch(typeof(DetailedResults))]
// ReSharper disable once InconsistentNaming
public static class DetailedResultsPatch
{
[HarmonyPrefix]
[HarmonyPatch("Show")]
// ReSharper disable once InconsistentNaming
public static bool PreventShow(DetailedResults __instance)
{
if (!RenderManager.IsActive) return true;
if (__instance)
{
__instance.enabled = false;
if (__instance.gameObject) __instance.gameObject.SetActive(false);
}
return false;
}
}
+34
View File
@@ -41,4 +41,38 @@ public static class scrControllerPatch
{ {
return !RenderManager.IsActive; return !RenderManager.IsActive;
} }
[HarmonyPostfix]
[HarmonyPatch("OnLandOnPortal")]
public static void PreventWinTextsPostfix(scrController __instance)
{
if (!RenderManager.IsActive || __instance == null) return;
if (__instance.txtCongrats)
{
__instance.txtCongrats.text = string.Empty;
__instance.txtCongrats.gameObject.SetActive(false);
}
if (__instance.txtAprilCongrats)
{
__instance.txtAprilCongrats.text = string.Empty;
__instance.txtAprilCongrats.gameObject.SetActive(false);
}
if (__instance.txtAllStrictClear)
{
__instance.txtAllStrictClear.text = string.Empty;
__instance.txtAllStrictClear.gameObject.SetActive(false);
}
if (__instance.detailedResults)
{
__instance.detailedResults.enabled = false;
if (__instance.detailedResults.gameObject != null)
{
__instance.detailedResults.gameObject.SetActive(false);
}
}
}
} }
+6
View File
@@ -14,6 +14,12 @@ public class scrPressToStartPatch
if (!RenderManager.Instance || !RenderManager.Instance.skipPressToStart) return true; if (!RenderManager.Instance || !RenderManager.Instance.skipPressToStart) return true;
RenderManager.Instance.skipPressToStart = false; RenderManager.Instance.skipPressToStart = false;
// scrPressToStart.ShowText ends with ShowDifficultyContainer(mode). Skipping ShowText skips that call too,
// which leaves the difficulty selector active and clickable. DontShow deactivates difficultyContainer
// and makes the keyboard difficulty shortcuts (difficultyUIMode != DontShow checks) do nothing.
if (scrUIController.instance)
scrUIController.instance.ShowDifficultyContainer(DifficultyUIMode.DontShow);
return false; return false;
} }
} }