SHA256
Compare commits
5
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
4e556a4632
|
||
|
|
f1e926f758 | ||
|
|
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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Legal Terms (Official)
|
|
||||||
|
|
||||||
Linking this Program statically or dynamically with other modules is making a combined work based on this Program. Thus, the terms and conditions of the GNU General Public License cover the whole combination.
|
Linking this Program statically or dynamically with other modules is making a combined work based on this Program. Thus, the terms and conditions of the GNU General Public License cover the whole combination.
|
||||||
|
|
||||||
As a special exception, the copyright holders of this Program give you permission to dynamically link this Program with independent modules that interact with or reference this Program solely through the public application programming interfaces (APIs), types, and data structures declared within the following namespaces:
|
As a special exception, the copyright holders of this Program give you permission to dynamically link this Program with independent modules that interact with or reference this Program solely through the public application programming interfaces (APIs), types, and data structures declared within the following namespaces:
|
||||||
@@ -22,21 +20,3 @@ An **"independent module"** is a separate and distinct software module that is n
|
|||||||
This exception does not grant any permission to modify the source code of this Program itself outside the terms of the GNU General Public License version 3.
|
This exception does not grant any permission to modify the source code of this Program itself outside the terms of the GNU General Public License version 3.
|
||||||
|
|
||||||
In accordance with section 7 of the GNU GPL version 3, if you modify this Program, you may extend this exception to your version of the Program, but you are not obligated to do so. If you do not wish to provide this exception in your derivative work, you may remove this exception statement from your version.
|
In accordance with section 7 of the GNU GPL version 3, if you modify this Program, you may extend this exception to your version of the Program, but you are not obligated to do so. If you do not wish to provide this exception in your derivative work, you may remove this exception statement from your version.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 조항 해설 (Korean Guide)
|
|
||||||
|
|
||||||
본 문서는 GNU General Public License 버전 3 (GPLv3) 제7조(추가적인 허가, Additional Permissions)에 따라 제공되는 공식 예외 조항입니다.
|
|
||||||
|
|
||||||
### 핵심 요약
|
|
||||||
|
|
||||||
- **동적 링킹(Dynamic Linking)만 허용**:
|
|
||||||
- 본 예외 조항은 **동적 링킹(런타임 참조 및 어셈블리 동적 로드)**에만 배타적으로 적용됩니다.
|
|
||||||
- ADOFAIRenderer 또는 그 일부 코드를 대상 모드/프로그램에 직접 병합하거나 포함(ILMerge, Costura, 바이너리 직접 내장/정적 컴파일 등)하는 **정적 링킹(Static Linking)은 본 예외 대상에서 제외**되며, 순수 GPLv3의 카피레프트(전염성) 의무가 그대로 적용됩니다.
|
|
||||||
- **외부 모드의 라이선스 자유**:
|
|
||||||
- 제3자 모드 또는 외부 프로그램이 동적 링킹을 통해 `ADOFAIRenderer.API` 및 `ADOFAIRenderer.Data` 네임스페이스의 공개 API, 클래스, 데이터 구조만을 참조·호출하여 연동하는 경우, 해당 모드는 **GPLv3의 카피레프트 의무를 적용받지 않습니다.**
|
|
||||||
- 외부 모드 개발자는 자신의 모드를 MIT, Apache-2.0, 독점/상용 라이선스 등 원하는 라이선스로 자유롭게 배포할 수 있습니다.
|
|
||||||
- **ADOFAIRenderer 코어 보호**:
|
|
||||||
- ADOFAIRenderer 자체의 소스 코드(렌더링 파이프라인, 코어 패치, 내부 유틸리티 등)를 수정하거나 재배포하는 경우에는 본 예외 조항이 적용되지 않으며, 순수 GPLv3 라이선스 조건이 그대로 적용됩니다.
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ namespace ADOFAIRenderer.Util.FFmpeg;
|
|||||||
/// 2. Codec & Hardware Device Discovery:
|
/// 2. Codec & Hardware Device Discovery:
|
||||||
/// a) avcodec_find_decoder: Locate codec implementation by codec ID.
|
/// a) avcodec_find_decoder: Locate codec implementation by codec ID.
|
||||||
/// b) avcodec_get_hw_config: Query supported hardware configurations for the decoder.
|
/// b) avcodec_get_hw_config: Query supported hardware configurations for the decoder.
|
||||||
/// c) Prioritized HW device creation (D3D11VA → DXVA2 → QSV → CUDA → D3D12VA → VAAPI → VideoToolbox).
|
/// c) HW device creation for the reported configurations, in the order of PreferredHwDeviceTypes.
|
||||||
/// d) Attach hw_device_ctx and register custom get_format callback.
|
/// d) Attach hw_device_ctx and register custom get_format callback.
|
||||||
/// e) If HW initialization fails or is unsupported, fall back to CPU software decoding
|
/// e) If HW initialization fails or is unsupported, fall back to CPU software decoding
|
||||||
/// with multi-threading enabled (thread_count = 0, FF_THREAD_FRAME).
|
/// with multi-threading enabled (thread_count = 0, FF_THREAD_FRAME).
|
||||||
@@ -35,8 +35,10 @@ namespace ADOFAIRenderer.Util.FFmpeg;
|
|||||||
/// c) (HW Transfer) For hardware frames, av_hwframe_transfer_data downloads GPU surface data
|
/// c) (HW Transfer) For hardware frames, av_hwframe_transfer_data downloads GPU surface data
|
||||||
/// into system RAM as NV12 (av_frame_copy_props preserves timestamps).
|
/// into system RAM as NV12 (av_frame_copy_props preserves timestamps).
|
||||||
/// d) (Output Dispatch):
|
/// d) (Output Dispatch):
|
||||||
/// - TryReadNV12Frame: Zero-copy direct memory transfer for NV12 frames, sws_scale fallback.
|
/// - TryReadNV12Frame: NV12 frames are copied row by row (Buffer.MemoryCopy) into the output buffers,
|
||||||
/// - TryReadYUV420PFrame: Zero-copy planar copy for YUV420P/YUVJ420P, sws_scale fallback.
|
/// other formats are converted with sws_scale.
|
||||||
|
/// - TryReadYUV420PFrame: YUV420P/YUVJ420P planes are copied row by row into the output buffers,
|
||||||
|
/// other formats are converted with sws_scale.
|
||||||
/// - TryReadFrame: sws_scale conversion to RGBA32 with optional vertical flip (vflip).
|
/// - TryReadFrame: sws_scale conversion to RGBA32 with optional vertical flip (vflip).
|
||||||
///
|
///
|
||||||
/// 4. Seeking & Scrubbing (SeekToTime / SeekToFrame):
|
/// 4. Seeking & Scrubbing (SeekToTime / SeekToFrame):
|
||||||
@@ -76,9 +78,12 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Preferred hardware device types in priority order.
|
/// Preferred hardware device types in priority order.
|
||||||
/// Modern Windows platforms prioritize D3D11VA for wide driver support and direct integration,
|
/// The candidates are the hardware configurations the selected decoder reports (avcodec_get_hw_config),
|
||||||
/// followed by DXVA2 (DirectX 9 / legacy), Intel QSV, NVIDIA CUDA, and DirectX 12 D3D12VA.
|
/// sorted by this list; configurations not in the list follow in reported order.
|
||||||
/// Non-Windows systems fall back to VAAPI or VideoToolbox.
|
/// Windows: D3D11VA, then DXVA2 (DirectX 9), CUDA and D3D12VA. Other platforms: VAAPI or VideoToolbox.
|
||||||
|
/// The QSV entry matches only decoders that report a QSV configuration. The native decoders returned by
|
||||||
|
/// avcodec_find_decoder report DXVA2/D3D11VA/D3D12VA/CUDA/VAAPI/Vulkan in the bundled FFmpeg 8 build;
|
||||||
|
/// the QSV decoders (h264_qsv, etc.) are separate decoders selected by name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly AVHWDeviceType[] PreferredHwDeviceTypes = new[]
|
private static readonly AVHWDeviceType[] PreferredHwDeviceTypes = new[]
|
||||||
{
|
{
|
||||||
@@ -661,9 +666,11 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable
|
|||||||
_getFormatDelegate = GetFormat;
|
_getFormatDelegate = GetFormat;
|
||||||
_pCodecContext->get_format = _getFormatDelegate;
|
_pCodecContext->get_format = _getFormatDelegate;
|
||||||
|
|
||||||
// Hardware decoding requires single-threaded decoding (thread_count = 1).
|
// Hardware decoding runs with one decoder thread (thread_count = 1).
|
||||||
// Multi-threading (thread_count = 0 / FF_THREAD_FRAME) is incompatible with fixed-function
|
// The GPU performs the decoding; the decoder thread only parses packets and submits them.
|
||||||
// GPU decoders and causes surface pool allocation exhaustion or crashes in get_format.
|
// With frame threading, libavcodec adds one hardware surface per thread to the frame pool
|
||||||
|
// (avcodec_get_hw_frames_parameters). The software fallback in GetFormat keeps this thread count.
|
||||||
|
// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libavcodec/decode.c
|
||||||
_pCodecContext->thread_count = 1;
|
_pCodecContext->thread_count = 1;
|
||||||
|
|
||||||
// Attempt opening codec context with hardware configuration
|
// Attempt opening codec context with hardware configuration
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// 2. Start(outputPath):
|
/// 2. Start(outputPath):
|
||||||
/// a) Create AVFormatContext for output container (mp4, etc.)
|
/// a) Create AVFormatContext for output container (mp4, etc.)
|
||||||
/// b) Add video stream (AVStream)
|
/// b) Add video stream (AVStream)
|
||||||
/// c) Try hardware encoders (NVENC → AMF → QSV) → software encoders (libx264, etc.)
|
/// c) Try the candidate encoders of the codec in order and select the first one that opens
|
||||||
/// in order to select an available encoder
|
/// (encoders of the active GPU vendor → other hardware encoders → software encoders; see GetEncoderCandidates)
|
||||||
/// d) Write file header and initialize frame/scaler buffers
|
/// d) Write file header and initialize frame/scaler buffers
|
||||||
///
|
///
|
||||||
/// 3. WriteFrame*(): Called for every frame
|
/// 3. WriteFrame*(): Called for every frame
|
||||||
/// a) Convert Unity RGBA original frame to NV12/YUV420P using sws_scale
|
/// a) Pre-converted NV12/YUV420P frame (GPU compute shader output): copied into the encoder frame
|
||||||
|
/// RGBA frame: converted to the encoder pixel format with sws_scale (CPU)
|
||||||
/// b) (In QSV hw_frames mode) Upload to GPU surface
|
/// b) (In QSV hw_frames mode) Upload to GPU surface
|
||||||
/// c) avcodec_send_frame → avcodec_receive_packet → Write to file
|
/// c) avcodec_send_frame → avcodec_receive_packet → Write to file
|
||||||
///
|
///
|
||||||
@@ -90,13 +91,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
public RateControlMode Mode;
|
public RateControlMode Mode;
|
||||||
|
|
||||||
/// <summary>Quality value used in CQP mode. Lower values mean higher quality. (e.g., H.264 CRF 18 ≈ visually lossless)</summary>
|
/// <summary>Quality value used in CQP mode. Lower values mean higher quality. (e.g., H.264 CRF 18 ≈ visually lossless)</summary>
|
||||||
public int QualityValue; // CRF
|
public int QualityValue;
|
||||||
|
|
||||||
/// <summary>Target bitrate in VBR/CBR mode (in kbps). Converted to bps internally for FFmpeg.</summary>
|
/// <summary>Target bitrate in VBR/CBR mode (in kbps). Converted to bps internally for FFmpeg.</summary>
|
||||||
public int TargetBitrate; // VBR
|
public int TargetBitrate;
|
||||||
|
|
||||||
/// <summary>Maximum bitrate in VBR mode (in kbps). Determines peak quality limit.</summary>
|
/// <summary>Maximum bitrate in VBR mode (in kbps). Determines peak quality limit.</summary>
|
||||||
public int MaxBitrate; // VBR
|
public int MaxBitrate;
|
||||||
|
|
||||||
/// <summary>GOP (Group of Pictures) size (in seconds). Represents keyframe interval.</summary>
|
/// <summary>GOP (Group of Pictures) size (in seconds). Represents keyframe interval.</summary>
|
||||||
public double GopSize;
|
public double GopSize;
|
||||||
@@ -165,7 +166,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// <summary>Accumulated index of encoded frames. Used as the PTS (Presentation Time Stamp) value.</summary>
|
/// <summary>Accumulated index of encoded frames. Used as the PTS (Presentation Time Stamp) value.</summary>
|
||||||
private long _frameIndex;
|
private long _frameIndex;
|
||||||
|
|
||||||
/// <summary>Whether Start() completed successfully and encoding is ready.</summary>
|
/// <summary>
|
||||||
|
/// Whether the encoder accepts frames: set to true when Start() completes,
|
||||||
|
/// and to false when End() runs (End() is not repeated) or Dispose() releases the resources.
|
||||||
|
/// </summary>
|
||||||
private bool _isInitialized;
|
private bool _isInitialized;
|
||||||
|
|
||||||
/// <summary>Whether Dispose() has already been called. Prevents duplicate cleanup.</summary>
|
/// <summary>Whether Dispose() has already been called. Prevents duplicate cleanup.</summary>
|
||||||
@@ -182,9 +186,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
private bool _isQsvEncoder;
|
private bool _isQsvEncoder;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Target pixel format for the encoder.
|
/// Pixel format of the frames passed to the encoder (output of the color conversion).
|
||||||
/// - General encoders (NVENC, AMF, Software): YUV420P (most common 4:2:0 chroma subsampling)
|
/// - Hardware encoders (NVENC, AMF, QSV): NV12 (Y plane + UV interleaved plane)
|
||||||
/// - QSV encoder: NV12 (Y plane + UV interleaved plane, preferred format for Intel hardware)
|
/// - libvvenc: YUV420P10LE (10-bit, 3 planes)
|
||||||
|
/// - Other software encoders: YUV420P (3 planes Y/U/V, 4:2:0 chroma subsampling)
|
||||||
///
|
///
|
||||||
/// Reference - Major pixel format relationships:
|
/// Reference - Major pixel format relationships:
|
||||||
/// RGBA (Unity source, 4-channel 8-bit)
|
/// RGBA (Unity source, 4-channel 8-bit)
|
||||||
@@ -285,7 +290,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of candidate encoders to try for the selected codec in priority order.
|
/// Returns a list of candidate encoders to try for the selected codec in priority order.
|
||||||
///
|
///
|
||||||
/// Priority: Hardware encoders (NVENC → AMF → QSV) → Software encoders (libx264, etc.)
|
/// Order (GPUUtil.GetPrioritizedEncoders):
|
||||||
|
/// 1. Hardware encoders of the active GPU vendor
|
||||||
|
/// 2. The remaining encoders in the declaration order of VideoEncoder.All
|
||||||
|
/// (per codec: NVENC → QSV → AMF → VideoToolbox → software, for the encoders the codec has)
|
||||||
|
/// With ForceSoftware, only the software encoders are returned.
|
||||||
///
|
///
|
||||||
/// - nvenc: Hardware encoder dedicated to NVIDIA GPUs (CUDA-based)
|
/// - nvenc: Hardware encoder dedicated to NVIDIA GPUs (CUDA-based)
|
||||||
/// - amf: Hardware encoder dedicated to AMD GPUs (Advanced Media Framework)
|
/// - amf: Hardware encoder dedicated to AMD GPUs (Advanced Media Framework)
|
||||||
@@ -416,11 +425,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
// ── Pixel format configuration ──
|
// ── Pixel format configuration ──
|
||||||
// Video encoders generally expect input in YUV420P or NV12 format.
|
// Video encoders generally expect input in YUV420P or NV12 format.
|
||||||
//
|
//
|
||||||
// • YUV420P: 3 independent planes Y/U/V. Used by most software encoders.
|
// • YUV420P: 3 independent planes Y/U/V. Used by the software encoders (libvvenc: YUV420P10LE).
|
||||||
// • NV12: Y plane + UV interleaved plane. Preferred by hardware encoders (NVENC, AMF, QSV).
|
// • NV12: Y plane + UV interleaved plane. Used by the hardware encoders (NVENC, AMF, QSV).
|
||||||
// • QSV Special Note: pix_fmt of codec context is AV_PIX_FMT_QSV (hardware surface identifier),
|
// • QSV: the converted frames are NV12 (selectedPixFmt). The codec context pix_fmt set here is
|
||||||
// while actual sws_scale output is set to NV12 (software format).
|
// AV_PIX_FMT_QSV and is replaced in the QSV device setup below according to the input mode:
|
||||||
// FFmpeg internally handles mapping between NV12 ↔ QSV surfaces.
|
// system memory input mode: pix_fmt = NV12, the encoder receives the NV12 frames directly.
|
||||||
|
// GPU frame pool input mode: pix_fmt stays AV_PIX_FMT_QSV, and SubmitDstFrame() uploads
|
||||||
|
// each NV12 frame to a QSV surface with av_hwframe_transfer_data.
|
||||||
//
|
//
|
||||||
// Reference: https://ffmpeg.org/doxygen/trunk/pixfmt_8h.html
|
// Reference: https://ffmpeg.org/doxygen/trunk/pixfmt_8h.html
|
||||||
var selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
|
var selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
|
||||||
@@ -479,17 +490,21 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
_pCodecContext->gop_size = calculatedGop;
|
_pCodecContext->gop_size = calculatedGop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Disable B-frames ──
|
// ── B-frame configuration ──
|
||||||
// B-frames (Bidirectional predictive frames): Increase compression ratio by referencing before/after frames,
|
// B-frames (bidirectional predictive frames) reference both earlier and later frames.
|
||||||
// but increase encoding latency and may cause compatibility issues on some encoders.
|
// They raise the compression ratio and make the encoder output packets out of presentation order
|
||||||
// In real-time recording scenarios, disable them to minimize latency.
|
// (dts < pts for reordered frames).
|
||||||
//
|
//
|
||||||
// Reference: has_b_frames is a read-only field internally set by the encoder,
|
// Configuration per encoder:
|
||||||
// so do not set it directly.
|
// SVT-AV1: svtav1-params "pred-struct=1" (low delay prediction structure).
|
||||||
|
// Setting max_b_frames on AVCodecContext makes SVT-AV1 report a "bad parameter" error.
|
||||||
|
// libvvenc: max_b_frames = -1 (encoder default GOP structure).
|
||||||
|
// Others: max_b_frames = 0 (no B-frames, frames are encoded in presentation order).
|
||||||
//
|
//
|
||||||
// SVT-AV1 Special Note: Setting max_b_frames directly on AVCodecContext
|
// has_b_frames is set by the encoder itself and is not written here.
|
||||||
// causes a "bad parameter" error, so control via svtav1-params.
|
//
|
||||||
// pred-struct=1: Low Delay (IPPP) structure disabling B-frames.
|
// Reference: https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Parameters.md
|
||||||
|
// (PredStructure: 0 = all intra, 1 = low delay, 2 = random access, default 2)
|
||||||
if (encoder == VideoEncoder.LibSvtAv1)
|
if (encoder == VideoEncoder.LibSvtAv1)
|
||||||
{
|
{
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "svtav1-params", "pred-struct=1", 0);
|
ffmpeg.av_opt_set(_pCodecContext->priv_data, "svtav1-params", "pred-struct=1", 0);
|
||||||
@@ -696,8 +711,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer ownership of hwDeviceCtx to codec context,
|
// Release the local reference of hwDeviceCtx.
|
||||||
// so release local reference (reference count management).
|
// The device stays alive through the references taken above:
|
||||||
|
// system memory input mode: codec context hw_device_ctx (av_buffer_ref)
|
||||||
|
// GPU frame pool input mode: the frames context (av_hwframe_ctx_alloc keeps a device reference),
|
||||||
|
// held by codec context hw_frames_ctx
|
||||||
// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__buffer.html
|
// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__buffer.html
|
||||||
ffmpeg.av_buffer_unref(&hwDeviceCtx);
|
ffmpeg.av_buffer_unref(&hwDeviceCtx);
|
||||||
}
|
}
|
||||||
@@ -833,8 +851,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
// Colorspace is converted without resolution change here, but sws_scale
|
// Colorspace is converted without resolution change here, but sws_scale
|
||||||
// can also perform resizing simultaneously.
|
// can also perform resizing simultaneously.
|
||||||
//
|
//
|
||||||
// SWS_POINT: Nearest neighbor interpolation. Since only colorspace is converted without resolution change,
|
// SWS_POINT: Nearest neighbor sampling. The luma plane keeps the source resolution (1:1).
|
||||||
// interpolation algorithm does not affect output quality, and SWS_POINT is fastest.
|
// The chroma planes are subsampled to half width and height (4:2:0) with the same filter,
|
||||||
|
// so each chroma sample takes one source pixel instead of averaging the 2x2 block.
|
||||||
|
// This scaler is used only by the CPU conversion path (WriteFrame with RGBA input).
|
||||||
//
|
//
|
||||||
// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html
|
// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html
|
||||||
_pSwsContext = ffmpeg.sws_getContext(
|
_pSwsContext = ffmpeg.sws_getContext(
|
||||||
@@ -1389,8 +1409,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
// av_hwframe_get_buffer: Get an empty hardware frame from hw_frames_ctx frame pool.
|
// av_hwframe_get_buffer: Get an empty hardware frame from hw_frames_ctx frame pool.
|
||||||
// av_hwframe_transfer_data: Copy (upload) NV12 software frame data to QSV surface.
|
// av_hwframe_transfer_data: Copy (upload) NV12 software frame data to QSV surface.
|
||||||
//
|
//
|
||||||
// Through this process, the encoder can read frames directly from GPU memory,
|
// The frame is copied from system memory to the GPU surface on every call;
|
||||||
// minimizing PCIe bandwidth usage.
|
// the encoder then reads the surface from GPU memory.
|
||||||
//
|
//
|
||||||
// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
|
// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
|
||||||
|
|
||||||
@@ -1953,8 +1973,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Flushes the audio encoder.
|
/// Flushes the audio encoder.
|
||||||
/// Pads and encodes remaining samples (less than frame_size) in FIFO,
|
/// Sends the samples remaining in the FIFO (fewer than frame_size) as one last short frame,
|
||||||
/// then sends null frame to output all delayed frames in encoder.
|
/// then sends a null frame to output all delayed packets of the encoder.
|
||||||
|
///
|
||||||
|
/// The short frame is handled by libavcodec (encode_send_frame_internal):
|
||||||
|
/// encoders with AV_CODEC_CAP_SMALL_LAST_FRAME or AV_CODEC_CAP_VARIABLE_FRAME_SIZE receive it as is,
|
||||||
|
/// other encoders receive it padded with silence up to frame_size (pad_last_frame).
|
||||||
|
/// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libavcodec/encode.c
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void FlushAudioEncoder()
|
private void FlushAudioEncoder()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ namespace ADOFAIRenderer.Util.OS;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends native desktop notifications on Windows, macOS, and Linux.
|
/// Sends native desktop notifications on Windows, macOS, and Linux.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// - <b>Windows</b>: Uses Win32 <c>Shell_NotifyIcon</c> (system tray balloon) via P/Invoke.
|
/// - <b>Windows</b>: Toast notification shown by a hidden <c>powershell.exe</c> process
|
||||||
/// No PowerShell, no UWP, no WinRT dependencies.
|
/// (WinRT <c>ToastNotificationManager</c>, script passed with <c>-EncodedCommand</c>).
|
||||||
|
/// With a folder path, the script also registers the <c>adofai-renderer://</c> URL protocol under
|
||||||
|
/// <c>HKCU\Software\Classes\adofai-renderer</c> (open command: <c>explorer.exe "<folder>"</c>)
|
||||||
|
/// so the toast button opens the folder; the key is overwritten on each notification and stays registered.
|
||||||
|
/// When PowerShell fails, a Win32 <c>Shell_NotifyIcon</c> balloon is shown via P/Invoke.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// - <b>macOS</b>: Uses <c>osascript</c> with AppleScript's <c>display notification</c>.
|
/// - <b>macOS</b>: Uses <c>osascript</c> with AppleScript's <c>display notification</c>.
|
||||||
@@ -88,7 +92,9 @@ internal static class OSNotification
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to show a Windows Toast notification via PowerShell.
|
/// Attempts to show a Windows Toast notification via PowerShell.
|
||||||
/// Returns true if the process launched successfully, false otherwise.
|
/// With a folder path, registers the adofai-renderer:// protocol in HKCU first (see class summary).
|
||||||
|
/// Waits up to 5 seconds for the process. Returns true when it exits with code 0,
|
||||||
|
/// false when it exits with another code, does not exit within 5 seconds, or fails to start.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TrySendToast(string title, string message, string? folderPath)
|
private static bool TrySendToast(string title, string message, string? folderPath)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user