SHA256
Compare commits
3
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
f153bf70aa
|
||
|
|
93539307b1
|
||
|
|
c0e902fc1a
|
No files matched your search
+41
-12
@@ -226,6 +226,11 @@ internal class RenderManager : MonoBehaviour
|
|||||||
private GameObject? _debugMonitorObj;
|
private GameObject? _debugMonitorObj;
|
||||||
private GameObject? _escapeHandlerObj;
|
private GameObject? _escapeHandlerObj;
|
||||||
internal string? CurrentOutPath;
|
internal string? CurrentOutPath;
|
||||||
|
|
||||||
|
// Output file of the current render that is not finished yet.
|
||||||
|
// Set just before the encoder opens the file, cleared by the render thread after the encoder finished the file (End).
|
||||||
|
// Only this file is deleted when a render is cancelled, fails or the game exits (DeleteIncompleteOutputFile).
|
||||||
|
private string? _incompleteOutputPath;
|
||||||
private bool _renderSetup;
|
private bool _renderSetup;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
@@ -286,6 +291,32 @@ internal class RenderManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
RenderThread.Join(200);
|
RenderThread.Join(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The encoders are disposed above, which closes the output file. An unfinished file of this render is deleted.
|
||||||
|
DeleteIncompleteOutputFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the output file of the current render when the render did not finish it (_incompleteOutputPath).
|
||||||
|
/// Files of earlier, finished renders are never deleted.
|
||||||
|
/// Returns true when a file was deleted.
|
||||||
|
/// </summary>
|
||||||
|
private bool DeleteIncompleteOutputFile()
|
||||||
|
{
|
||||||
|
var path = Interlocked.Exchange(ref _incompleteOutputPath, null);
|
||||||
|
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
Melon<RenderMod>.Logger.Msg($"[File Cleanup] Deleted incomplete video: {path}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Error($"[File Cleanup] Failed to delete incomplete video: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
@@ -634,6 +665,7 @@ internal class RenderManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
_allVideoFFmpegInstances.Add(videoFFmpeg);
|
_allVideoFFmpegInstances.Add(videoFFmpeg);
|
||||||
}
|
}
|
||||||
|
Volatile.Write(ref _incompleteOutputPath, CurrentOutPath);
|
||||||
videoFFmpeg.Start(CurrentOutPath);
|
videoFFmpeg.Start(CurrentOutPath);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -735,7 +767,13 @@ internal class RenderManager : MonoBehaviour
|
|||||||
FrameAvailableEvent.WaitOne(50);
|
FrameAvailableEvent.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsRenderAborted) videoFFmpeg.End();
|
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();
|
videoFFmpeg.Dispose();
|
||||||
|
|
||||||
lock (_allVideoFFmpegInstances)
|
lock (_allVideoFFmpegInstances)
|
||||||
@@ -862,18 +900,9 @@ internal class RenderManager : MonoBehaviour
|
|||||||
scrController.instance.noFailInfiniteMargin = false;
|
scrController.instance.noFailInfiniteMargin = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Immediately delete cancelled incomplete temporary video file
|
// 6. Immediately delete the incomplete video file of this render
|
||||||
if (!string.IsNullOrEmpty(CurrentOutPath) && File.Exists(CurrentOutPath))
|
if (DeleteIncompleteOutputFile())
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(CurrentOutPath);
|
|
||||||
Melon<RenderMod>.Logger.Msg($"[File Cleanup] Deleted incomplete video: {CurrentOutPath}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Melon<RenderMod>.Logger.Error($"[File Cleanup] Failed to delete temporary video: {ex.Message}");
|
|
||||||
}
|
|
||||||
CurrentOutPath = null;
|
CurrentOutPath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ using MelonLoader.Utils;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.7-INDEV", "ADOFAI Editor Forum")]
|
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.8-INDEV", "ADOFAI Editor Forum")]
|
||||||
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
||||||
|
|
||||||
namespace ADOFAIRenderer;
|
namespace ADOFAIRenderer;
|
||||||
|
|||||||
@@ -824,16 +824,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
if (_pSrcFrame == null || _pDstFrame == null || _pPacket == null)
|
if (_pSrcFrame == null || _pDstFrame == null || _pPacket == null)
|
||||||
throw new Exception("Failed to allocate frame or packet structure.");
|
throw new Exception("Failed to allocate frame or packet structure.");
|
||||||
|
|
||||||
// Set format and size of destination frame, then allocate actual pixel buffer.
|
AllocateDstFrameBuffer();
|
||||||
// av_frame_get_buffer: Allocates memory matching format, width, height and
|
|
||||||
// sets data[]/linesize[] pointers. Second argument 0 means default alignment.
|
|
||||||
_pDstFrame->format = (int)_dstPixFmt;
|
|
||||||
_pDstFrame->width = _settings.Width;
|
|
||||||
_pDstFrame->height = _settings.Height;
|
|
||||||
_pDstFrame->color_range = AVColorRange.AVCOL_RANGE_JPEG;
|
|
||||||
|
|
||||||
if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
|
|
||||||
throw new Exception($"Failed to allocate {_dstPixFmt} frame buffer.");
|
|
||||||
|
|
||||||
// sws_getContext: Initialize software scaler (colorspace converter).
|
// sws_getContext: Initialize software scaler (colorspace converter).
|
||||||
// Input: RGBA (Unity capture format, 4 bytes/pixel)
|
// Input: RGBA (Unity capture format, 4 bytes/pixel)
|
||||||
@@ -1201,12 +1192,50 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the format, size and color range of the destination frame (_pDstFrame) and allocates a new pixel buffer for it.
|
||||||
|
/// av_frame_get_buffer allocates memory matching format, width and height and sets the data[]/linesize[] pointers.
|
||||||
|
/// Second argument 0 means default alignment.
|
||||||
|
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html (av_frame_get_buffer)
|
||||||
|
/// </summary>
|
||||||
|
private void AllocateDstFrameBuffer()
|
||||||
|
{
|
||||||
|
_pDstFrame->format = (int)_dstPixFmt;
|
||||||
|
_pDstFrame->width = _settings.Width;
|
||||||
|
_pDstFrame->height = _settings.Height;
|
||||||
|
_pDstFrame->color_range = AVColorRange.AVCOL_RANGE_JPEG;
|
||||||
|
|
||||||
|
var ret = ffmpeg.av_frame_get_buffer(_pDstFrame, 0);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception($"Failed to allocate {_dstPixFmt} frame buffer — {GetFFmpegErrorString(ret)} (Code: {ret})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes the pixel buffer of the destination frame (_pDstFrame) writable before the next frame is written into it.
|
||||||
|
///
|
||||||
|
/// After avcodec_send_frame, the encoder may keep a reference to the buffer of the submitted frame
|
||||||
|
/// until that frame is encoded (e.g. QSV in system memory input mode). The buffer is then shared and not writable
|
||||||
|
/// (av_frame_is_writable returns 0).
|
||||||
|
/// In that case the frame releases its reference (av_frame_unref) and a new buffer is allocated.
|
||||||
|
/// The encoder keeps the previous buffer until it no longer needs it. The previous contents are not copied,
|
||||||
|
/// because every plane is overwritten by the next frame.
|
||||||
|
///
|
||||||
|
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html (av_frame_is_writable, av_frame_unref)
|
||||||
|
/// </summary>
|
||||||
|
private void PrepareDstFrameForWrite()
|
||||||
|
{
|
||||||
|
if (ffmpeg.av_frame_is_writable(_pDstFrame) != 0) return;
|
||||||
|
|
||||||
|
ffmpeg.av_frame_unref(_pDstFrame);
|
||||||
|
AllocateDstFrameBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Encodes a frame by directly copying already pre-converted NV12 or YUV420P frame data.
|
/// Encodes a frame by directly copying already pre-converted NV12 or YUV420P frame data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void WritePreConvertedFrame(byte* buffer)
|
private void WritePreConvertedFrame(byte* buffer)
|
||||||
{
|
{
|
||||||
ffmpeg.av_frame_make_writable(_pDstFrame);
|
PrepareDstFrameForWrite();
|
||||||
|
|
||||||
var width = _settings.Width;
|
var width = _settings.Width;
|
||||||
var height = _settings.Height;
|
var height = _settings.Height;
|
||||||
@@ -1320,20 +1349,17 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// Core logic for actual frame encoding.
|
/// Core logic for actual frame encoding.
|
||||||
///
|
///
|
||||||
/// Processing flow:
|
/// Processing flow:
|
||||||
/// 1. av_frame_make_writable: Check if destination frame buffer is writable
|
/// 1. PrepareDstFrameForWrite: Makes the destination frame buffer writable
|
||||||
/// (Allocates a new buffer if encoder is still referencing previous frame's buffer)
|
/// (Allocates a new buffer if encoder is still referencing previous frame's buffer)
|
||||||
/// 2. sws_scale: RGBA → YUV420P/NV12 colorspace conversion
|
/// 2. sws_scale: RGBA → YUV420P/NV12 colorspace conversion
|
||||||
/// 3. (QSV hw_frames mode) av_hwframe_transfer_data: Upload NV12 → QSV hardware surface
|
/// 3. (QSV hw_frames mode) av_hwframe_transfer_data: Upload NV12 → QSV hardware surface
|
||||||
/// 4. Pass to encoder after setting PTS
|
/// 4. Pass to encoder after setting PTS
|
||||||
///
|
///
|
||||||
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html (av_frame_make_writable)
|
|
||||||
/// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html (sws_scale)
|
/// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html (sws_scale)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void WriteFrame()
|
private void WriteFrame()
|
||||||
{
|
{
|
||||||
// Ensure writable state since encoder might still be referencing previous frame's buffer.
|
PrepareDstFrameForWrite();
|
||||||
// Internally allocates a new buffer if referenced.
|
|
||||||
ffmpeg.av_frame_make_writable(_pDstFrame);
|
|
||||||
|
|
||||||
// ── Colorspace conversion: RGBA → YUV420P or NV12 ──
|
// ── Colorspace conversion: RGBA → YUV420P or NV12 ──
|
||||||
// sws_scale parameters explanation:
|
// sws_scale parameters explanation:
|
||||||
@@ -1494,8 +1520,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// 2. av_write_trailer: Writes container trailer (end-of-file metadata).
|
/// 2. av_write_trailer: Writes container trailer (end-of-file metadata).
|
||||||
/// For MP4, the moov atom (index info) is completed here.
|
/// For MP4, the moov atom (index info) is completed here.
|
||||||
/// ⚠️ If trailer is not written, the file becomes corrupted and unplayable!
|
/// ⚠️ If trailer is not written, the file becomes corrupted and unplayable!
|
||||||
|
/// av_write_trailer also flushes the output buffer and returns the write error of the output (AVIOContext.error), if any.
|
||||||
|
///
|
||||||
|
/// Throws an exception when a flush or the trailer fails (e.g. the disk is full), because the file is then incomplete.
|
||||||
|
/// End() runs once: after it returns or throws, further calls return immediately.
|
||||||
///
|
///
|
||||||
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html
|
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html
|
||||||
|
/// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libavformat/mux.c (av_write_trailer)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void End()
|
public void End()
|
||||||
{
|
{
|
||||||
@@ -1503,37 +1534,29 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
{
|
{
|
||||||
if (_disposed || !_isInitialized) return;
|
if (_disposed || !_isInitialized) return;
|
||||||
|
|
||||||
// 1. Flush delayed frames from video encoder (send null frame as "end of input" signal)
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 1. Flush delayed frames from video encoder (send null frame as "end of input" signal)
|
||||||
SendFrameAndWritePackets(null);
|
SendFrameAndWritePackets(null);
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log error occurring during flush process
|
|
||||||
Melon<RenderMod>.Logger.Error($"Error during video encoder flush: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.5. Flush remaining samples in audio FIFO + flush audio encoder
|
// 1.5. Flush remaining samples in audio FIFO + flush audio encoder
|
||||||
if (_audioEnabled)
|
if (_audioEnabled)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
FlushAudioEncoder();
|
FlushAudioEncoder();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
// 2. Write container trailer (moov atom and file termination metadata)
|
||||||
|
if (_pFormatContext != null)
|
||||||
{
|
{
|
||||||
Melon<RenderMod>.Logger.Error($"Error during audio encoder flush: {ex.Message}");
|
var ret = ffmpeg.av_write_trailer(_pFormatContext);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception($"Failed to write the container trailer — {GetFFmpegErrorString(ret)} (Code: {ret})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
// 2. Write container trailer (moov atom and file termination metadata)
|
|
||||||
if (_pFormatContext != null)
|
|
||||||
{
|
{
|
||||||
ffmpeg.av_write_trailer(_pFormatContext);
|
_isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isInitialized = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1544,11 +1567,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Actual resource cleanup logic. Cleanup order is important:
|
/// Actual resource cleanup logic. Cleanup order is important:
|
||||||
///
|
///
|
||||||
/// 1. Call End() (if not called yet) → flush delayed frames + write trailer
|
/// 1. SwsContext (scaler) → independent, can be freed first
|
||||||
/// 2. SwsContext (scaler) → independent, can be freed first
|
/// 2. AVFrame (src/dst) → freed before codec as codec may reference them
|
||||||
/// 3. AVFrame (src/dst) → freed before codec as codec may reference them
|
/// 3. AVCodecContext (codec) → hw_frames_ctx/hw_device_ctx freed together
|
||||||
/// 4. AVCodecContext (codec) → hw_frames_ctx/hw_device_ctx freed together
|
/// 4. AVFormatContext (format/IO) → includes closing file handle
|
||||||
/// 5. AVFormatContext (format/IO) → includes closing file handle
|
///
|
||||||
|
/// Dispose does not finish the file (no encoder flush, no trailer). A file is complete only after End() returned.
|
||||||
|
/// Without End(), the output file is incomplete and is left to the caller (the renderer deletes it).
|
||||||
///
|
///
|
||||||
/// Reference:
|
/// Reference:
|
||||||
/// av_frame_free: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html
|
/// av_frame_free: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html
|
||||||
@@ -1562,12 +1587,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
_isInitialized = false;
|
||||||
// Trigger End() safely in case Dispose is called without calling End() first
|
|
||||||
if (_isInitialized)
|
|
||||||
{
|
|
||||||
try { End(); } catch { /* Ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release Sws scaler
|
// Release Sws scaler
|
||||||
if (_pSwsContext != null)
|
if (_pSwsContext != null)
|
||||||
|
|||||||
Reference in new issue
Block a user