SHA256
Compare commits
3
Commits
f153bf70aa
...
c62723cf5c
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
c62723cf5c
|
||
|
|
fee7077b16
|
||
|
|
9fbf057677
|
No files matched your search
+105
-64
@@ -682,49 +682,123 @@ internal class RenderManager : MonoBehaviour
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderThread = new Thread(() =>
|
var encoder = videoFFmpeg!;
|
||||||
|
RenderThread = new Thread(() => RunRenderThread(encoder))
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Priority = ThreadPriority.AboveNormal
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
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)
|
while (true)
|
||||||
{
|
{
|
||||||
CapturedFrame frame = default;
|
if (TryDequeueNextFrame(out var frame))
|
||||||
bool hasFrame = false;
|
{
|
||||||
|
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)
|
lock (ImageQueue)
|
||||||
{
|
{
|
||||||
if (ImageQueue.Count > 0)
|
if (ImageQueue.Count == 0) return false;
|
||||||
{
|
|
||||||
var firstKey = ImageQueue.Keys[0];
|
|
||||||
|
|
||||||
|
var firstKey = ImageQueue.Keys[0];
|
||||||
if (firstKey > passCount && Instance.stage < RenderStage.Render)
|
if (firstKey > passCount && Instance.stage < RenderStage.Render)
|
||||||
{
|
{
|
||||||
passCount = firstKey;
|
passCount = firstKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstKey == passCount)
|
if (firstKey != passCount) return false;
|
||||||
{
|
|
||||||
frame = ImageQueue[firstKey];
|
frame = ImageQueue[firstKey];
|
||||||
ImageQueue.RemoveAt(0);
|
ImageQueue.RemoveAt(0);
|
||||||
passCount++;
|
passCount++;
|
||||||
hasFrame = true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFrame)
|
/// <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 != null)
|
||||||
{
|
{
|
||||||
if (frame.ManagedBuffer.Length > 0)
|
if (frame.ManagedBuffer.Length == 0) return;
|
||||||
{
|
|
||||||
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted);
|
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted);
|
||||||
Interlocked.Increment(ref framesSent);
|
Interlocked.Increment(ref framesSent);
|
||||||
|
|
||||||
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
|
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0)
|
if (frame.NativePointer == IntPtr.Zero || frame.Length <= 0) return;
|
||||||
{
|
|
||||||
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted);
|
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted);
|
||||||
Interlocked.Increment(ref framesSent);
|
Interlocked.Increment(ref framesSent);
|
||||||
|
|
||||||
@@ -734,8 +808,14 @@ internal class RenderManager : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Configuration.AudioConfig.RenderAudio && SyncCalculator is { } syncCalculator)
|
/// <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 frameIdx = (int)(passCount - 1);
|
||||||
var effectiveVideoFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS);
|
var effectiveVideoFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS);
|
||||||
var frameTime = frameIdx / effectiveVideoFps;
|
var frameTime = frameIdx / effectiveVideoFps;
|
||||||
@@ -754,64 +834,25 @@ internal class RenderManager : MonoBehaviour
|
|||||||
ArrayPool<float>.Shared.Return(audioBuffer);
|
ArrayPool<float>.Shared.Return(audioBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsRenderAborted) break;
|
/// <summary>
|
||||||
|
/// True when the render is aborted, or when the end was requested and every requested frame has been sent.
|
||||||
if (endRequested)
|
/// </summary>
|
||||||
|
private bool ShouldStopEncodeLoop()
|
||||||
{
|
{
|
||||||
if (passCount > Volatile.Read(ref lastRequestedVideoFrameIndex)) break;
|
if (IsRenderAborted) return true;
|
||||||
|
|
||||||
|
return endRequested && passCount > Volatile.Read(ref lastRequestedVideoFrameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameAvailableEvent.WaitOne(50);
|
private void UnregisterEncoder(FFmpegNativeEncoder 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();
|
|
||||||
|
|
||||||
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)
|
lock (_allVideoFFmpegInstances)
|
||||||
{
|
{
|
||||||
_allVideoFFmpegInstances.Remove(videoFFmpeg);
|
_allVideoFFmpegInstances.Remove(videoFFmpeg);
|
||||||
}
|
}
|
||||||
_activeVideoFFmpeg = null;
|
_activeVideoFFmpeg = null;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Priority = ThreadPriority.AboveNormal
|
|
||||||
};
|
|
||||||
|
|
||||||
RenderThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CancelRender()
|
public void CancelRender()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user