diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj
index 1aa48be..a276d07 100644
--- a/ADOFAIRenderer.csproj
+++ b/ADOFAIRenderer.csproj
@@ -85,4 +85,8 @@
+
+
+
+
diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs
index b4b9692..cc0d710 100644
--- a/Core/RenderManager.cs
+++ b/Core/RenderManager.cs
@@ -53,14 +53,6 @@ namespace ADOFAIRenderer.Core
public int throttleCount;
public bool isThrottling;
- [SuppressMessage("ReSharper", "InconsistentNaming")]
- public enum BitrateControlMode
- {
- CBR,
- VBR,
- CQP
- }
-
public struct RenderSettings
{
public double TargetFPS;
@@ -69,7 +61,7 @@ namespace ADOFAIRenderer.Core
public int RenderWidth;
public int RenderHeight;
- public BitrateControlMode BitrateMode;
+ public RateControlMode BitrateMode;
public int Bitrate;
public int MaxBitrate;
public int QP;
@@ -80,7 +72,7 @@ namespace ADOFAIRenderer.Core
public int? RandomSeed;
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight,
- BitrateControlMode bitrateMode, int bitrate, int maxBitrate, int qp,
+ RateControlMode bitrateMode, int bitrate, int maxBitrate, int qp,
string codec, double endWaitTime, bool showHitText, int? randomSeed)
{
TargetFPS = targetFPS;
@@ -442,7 +434,6 @@ namespace ADOFAIRenderer.Core
DontDestroyOnLoad(_debugMonitorObj);
}
- FFmpegProcess.FFmpegPath = ffmpegPath;
lock (ImageQueue)
{
ImageQueue.Clear();
diff --git a/RenderMod.cs b/RenderMod.cs
index 38b7a3f..b73696a 100644
--- a/RenderMod.cs
+++ b/RenderMod.cs
@@ -1,7 +1,9 @@
using ADOFAIRenderer;
using ADOFAIRenderer.Core;
using ADOFAIRenderer.Util;
+using FFmpeg.AutoGen;
using MelonLoader;
+using MelonLoader.Utils;
using UnityEngine;
using UnityEngine.EventSystems;
@@ -19,6 +21,8 @@ namespace ADOFAIRenderer
public override void OnInitializeMelon()
{
+ ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory;
+
var renderManagerObj = new GameObject("Render Manager");
var updateHookerObj = new GameObject("Update Hooker");
Object.DontDestroyOnLoad(renderManagerObj);
diff --git a/UI/RenderButton.cs b/UI/RenderButton.cs
index 7cbd592..79514fc 100644
--- a/UI/RenderButton.cs
+++ b/UI/RenderButton.cs
@@ -45,7 +45,7 @@ namespace ADOFAIRenderer.UI
if (codecDropdown != null)
{
codecDropdown.ClearOptions();
- var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList();
+ var options = System.Enum.GetNames(typeof(Codec)).ToList();
codecDropdown.AddOptions(options);
}
@@ -53,7 +53,7 @@ namespace ADOFAIRenderer.UI
if (bitrateModeDropdown != null)
{
bitrateModeDropdown.ClearOptions();
- var modeOptions = System.Enum.GetNames(typeof(RenderManager.BitrateControlMode)).ToList();
+ var modeOptions = System.Enum.GetNames(typeof(RateControlMode)).ToList();
bitrateModeDropdown.AddOptions(modeOptions);
bitrateModeDropdown.onValueChanged.AddListener(delegate { UpdateUIByMode(); });
}
@@ -103,21 +103,21 @@ namespace ADOFAIRenderer.UI
if (bitrateModeDropdown == null) return;
if (_bitrateInput == null || _maxBitrateInput == null || _qpInput == null) return;
- var selectedMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value;
+ var selectedMode = (RateControlMode)bitrateModeDropdown.value;
switch (selectedMode)
{
- case RenderManager.BitrateControlMode.CBR:
+ case RateControlMode.CBR:
_bitrateInput.gameObject.SetActive(true);
_maxBitrateInput.gameObject.SetActive(false);
_qpInput.gameObject.SetActive(false);
break;
- case RenderManager.BitrateControlMode.VBR:
+ case RateControlMode.VBR:
_bitrateInput.gameObject.SetActive(true);
_maxBitrateInput.gameObject.SetActive(true);
_qpInput.gameObject.SetActive(false);
break;
- case RenderManager.BitrateControlMode.CQP:
+ case RateControlMode.CQP:
_bitrateInput.gameObject.SetActive(false);
_maxBitrateInput.gameObject.SetActive(false);
_qpInput.gameObject.SetActive(true);
@@ -165,17 +165,17 @@ namespace ADOFAIRenderer.UI
return;
}
- var bitrateMode = RenderManager.BitrateControlMode.CBR;
+ var bitrateMode = RateControlMode.CBR;
if (bitrateModeDropdown != null)
{
- bitrateMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value;
+ bitrateMode = (RateControlMode)bitrateModeDropdown.value;
}
var bitrate = 0;
var maxBitrate = 0;
var qp = 18;
- if (bitrateMode == RenderManager.BitrateControlMode.CBR || bitrateMode == RenderManager.BitrateControlMode.VBR)
+ if (bitrateMode == RateControlMode.CBR || bitrateMode == RateControlMode.VBR)
{
if (!int.TryParse(_bitrateInput.text, out bitrate) || bitrate <= 0)
{
@@ -184,7 +184,7 @@ namespace ADOFAIRenderer.UI
}
}
- if (bitrateMode == RenderManager.BitrateControlMode.VBR)
+ if (bitrateMode == RateControlMode.VBR)
{
if (!int.TryParse(_maxBitrateInput.text, out maxBitrate) || maxBitrate <= 0)
{
@@ -198,7 +198,7 @@ namespace ADOFAIRenderer.UI
}
}
- if (bitrateMode == RenderManager.BitrateControlMode.CQP)
+ if (bitrateMode == RateControlMode.CQP)
{
if (!int.TryParse(_qpInput.text, out qp) || qp < 0 || qp > 51)
{
diff --git a/Util/FFmpeg/Codec.cs b/Util/FFmpeg/Codec.cs
new file mode 100644
index 0000000..9250a4a
--- /dev/null
+++ b/Util/FFmpeg/Codec.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace ADOFAIRenderer.Util.FFmpeg
+{
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ public enum Codec
+ {
+ H264, H265, AV1, VP9
+ }
+}
\ No newline at end of file
diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs
new file mode 100644
index 0000000..7629f42
--- /dev/null
+++ b/Util/FFmpeg/FFmpegNativeEncoder.cs
@@ -0,0 +1,273 @@
+using System;
+using FFmpeg.AutoGen;
+using HarmonyLib;
+using Unity.Collections;
+
+namespace ADOFAIRenderer.Util.FFmpeg
+{
+ public unsafe class FFmpegNativeEncoder : IDisposable
+ {
+ public struct EncoderSettings
+ {
+ public int Width;
+ public int Height;
+ public double FPS;
+
+ public Codec Codec;
+ public RateControlMode Mode;
+
+ public int QualityValue; // CRF
+ public int TargetBitrate; // VBR
+ public int MaxBitrate; // VBR
+ }
+
+ // FFmpeg 네이티브 컨텍스트 및 구조체 포인터들
+ private AVFormatContext* _pFormatContext = null;
+ private AVCodecContext* _pCodecContext = null;
+ private AVStream* _pStream = null;
+
+ // 프레임 버퍼 및 스케일러 포인터들
+ private AVFrame* _pSrcFrame = null; // Unity에서 넘어온 원시 RGBA 프레임용
+ private AVFrame* _pDstFrame = null; // 인코더가 요구하는 YUV420P 프레임용
+ private SwsContext* _pSwsContext = null; // RGBA -> YUV420P 변환 스케일러
+
+ private long _frameIndex = 0;
+ private bool _isInitialized = false;
+ private bool _disposed = false;
+
+ private readonly EncoderSettings _settings;
+
+ public FFmpegNativeEncoder(EncoderSettings settings)
+ {
+ _settings = settings;
+ }
+
+ // ReSharper disable once InconsistentNaming
+ private AVCodecID GetAVCodecID()
+ {
+ return _settings.Codec switch
+ {
+ Codec.H264 => AVCodecID.AV_CODEC_ID_H264,
+ Codec.H265 => AVCodecID.AV_CODEC_ID_HEVC,
+ Codec.AV1 => AVCodecID.AV_CODEC_ID_AV1,
+ Codec.VP9 => AVCodecID.AV_CODEC_ID_VP9,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public void Start(string outputPath)
+ {
+ if (_isInitialized) return;
+
+ try
+ {
+ fixed (AVFormatContext** ppFormatCtx = &_pFormatContext)
+ {
+ ffmpeg.avformat_alloc_output_context2(ppFormatCtx, null, null, outputPath);
+ }
+
+ if (_pFormatContext == null) throw new Exception("출력 포맷 컨텍스트를 생성하지 못했습니다.");
+
+ var pCodec = ffmpeg.avcodec_find_encoder(GetAVCodecID());
+ if (pCodec == null) throw new Exception("코덱 인코더를 찾을 수 없습니다.");
+
+ _pStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
+ if (_pStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
+
+ _pCodecContext = ffmpeg.avcodec_alloc_context3(pCodec);
+ _pCodecContext->width = _settings.Width;
+ _pCodecContext->height = _settings.Height;
+ _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
+
+ var fpsRational = RationalUtil.Double2MaxPrecisionInt(_settings.FPS);
+ _pCodecContext->time_base = new AVRational
+ { num = fpsRational.Denominator, den = fpsRational.Numerator };
+ _pCodecContext->framerate = new AVRational
+ { num = fpsRational.Numerator, den = fpsRational.Denominator };
+ _pCodecContext->gop_size = (int)Math.Round(_settings.FPS * 2);
+
+ _pCodecContext->max_b_frames = 3;
+ _pCodecContext->has_b_frames = 1;
+
+ if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
+ {
+ _pCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ ApplyQualitySettings();
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "slow", 0);
+
+ if (ffmpeg.avcodec_open2(_pCodecContext, pCodec, null) < 0)
+ throw new Exception("지정된 설정으로 코덱을 여는 데 실패했습니다.");
+
+ if (ffmpeg.avcodec_parameters_from_context(_pStream->codecpar, _pCodecContext) < 0)
+ throw new Exception("코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
+
+ if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
+ {
+ if (ffmpeg.avio_open(&_pFormatContext->pb, outputPath, ffmpeg.AVIO_FLAG_WRITE) < 0)
+ throw new Exception($"출력 파일 {outputPath}를 열 수 없습니다.");
+ }
+
+ if (ffmpeg.avformat_write_header(_pFormatContext, null) < 0)
+ throw new Exception("파일 헤더를 작성하는 데 실패했습니다.");
+
+ _pSrcFrame = ffmpeg.av_frame_alloc();
+ _pDstFrame = ffmpeg.av_frame_alloc();
+ if (_pSrcFrame == null || _pDstFrame == null)
+ throw new Exception("프레임 구조체를 할당하지 못했습니다.");
+
+ _pDstFrame->format = (int)AVPixelFormat.AV_PIX_FMT_YUV420P;
+ _pDstFrame->width = _settings.Width;
+ _pDstFrame->height = _settings.Height;
+
+ if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
+ throw new Exception("YUV420P 프레임 버퍼를 할당하는 데 실패했습니다.");
+
+ _pSwsContext = ffmpeg.sws_getContext(
+ _settings.Width, _settings.Height, AVPixelFormat.AV_PIX_FMT_RGBA,
+ _settings.Width, _settings.Height, AVPixelFormat.AV_PIX_FMT_YUV420P,
+ (int)SwsFlags.SWS_BILINEAR, null, null, null);
+
+ if (_pSwsContext == null)
+ throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다.");
+
+ _isInitialized = true;
+ }
+ catch (Exception)
+ {
+ Dispose();
+ throw;
+ }
+ }
+
+ private void ApplyQualitySettings()
+ {
+ switch (_settings.Mode)
+ {
+ case RateControlMode.CBR:
+ _pCodecContext->bit_rate = _pCodecContext->rc_max_rate = _pCodecContext->rc_min_rate = _settings.TargetBitrate;
+ _pCodecContext->rc_buffer_size = _settings.TargetBitrate;
+
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "nal-hrd", "cbr", 0);
+ break;
+
+ case RateControlMode.VBR:
+ _pCodecContext->bit_rate = _settings.TargetBitrate;
+ var maxRate = _settings.MaxBitrate > _settings.TargetBitrate
+ ? _settings.MaxBitrate : (int)(_settings.TargetBitrate * 1.5);
+
+ _pCodecContext->rc_max_rate = maxRate;
+ _pCodecContext->rc_buffer_size = maxRate * 2;
+ break;
+
+ case RateControlMode.CQP:
+ var qpValue = _settings.QualityValue > 0 ? _settings.QualityValue : 18;
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public void WriteFrameFromBuffer(byte* buffer)
+ {
+
+ }
+
+ public void WriteFrameFromNativeBuffer(NativeArray buffer)
+ {
+
+ }
+
+ private void WriteFrame()
+ {
+ ffmpeg.sws_scale(
+ _pSwsContext,
+ _pSrcFrame->data, _pSrcFrame->linesize,
+ 0, _settings.Height,
+ _pDstFrame->data, _pDstFrame->linesize
+ );
+
+ // 3. 타임스탬프 설정
+ _pDstFrame->pts = _frameIndex++;
+
+ // 4. 인코더로 프레임 전달 및 패킷 쓰기
+ SendFrameAndWritePackets(_pDstFrame);
+ }
+
+ private void SendFrameAndWritePackets(AVFrame* pFrame)
+ {
+
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+
+ // 만약 End()를 호출하지 않고 명시적으로 Dispose가 되는 경우도 있으므로 안전하게 End() 트리거
+ if (_isInitialized)
+ {
+ try { End(); } catch { /* 무시 */ }
+ }
+
+ // Sws 스케일러 해제
+ if (_pSwsContext != null)
+ {
+ ffmpeg.sws_freeContext(_pSwsContext);
+ _pSwsContext = null;
+ }
+
+ // 프레임 메모리 해제
+ if (_pSrcFrame != null)
+ {
+ var pFrame = _pSrcFrame;
+ ffmpeg.av_frame_free(&pFrame);
+ _pSrcFrame = null;
+ }
+ if (_pDstFrame != null)
+ {
+ var pFrame = _pDstFrame;
+ ffmpeg.av_frame_free(&pFrame);
+ _pDstFrame = null;
+ }
+
+ // 코덱 컨텍스트 해제
+ if (_pCodecContext != null)
+ {
+ var pCodecCtx = _pCodecContext;
+ ffmpeg.avcodec_free_context(&pCodecCtx);
+ _pCodecContext = null;
+ }
+
+ // 포맷 컨텍스트 및 I/O 닫기
+ if (_pFormatContext != null)
+ {
+ if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
+ {
+ var pPb = _pFormatContext->pb;
+ if (pPb != null)
+ {
+ ffmpeg.avio_closep(&pPb);
+ _pFormatContext->pb = null;
+ }
+ }
+ ffmpeg.avformat_free_context(_pFormatContext);
+ _pFormatContext = null;
+ }
+
+ _disposed = true;
+ }
+
+ ~FFmpegNativeEncoder()
+ {
+ Dispose(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Util/FFmpeg/FFmpegProcess.cs b/Util/FFmpeg/FFmpegProcess.cs
deleted file mode 100644
index 43c6f1f..0000000
--- a/Util/FFmpeg/FFmpegProcess.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-using System;
-using System.Buffers;
-using System.Diagnostics;
-using System.IO;
-using Unity.Collections;
-
-namespace ADOFAIRenderer.Util.FFmpeg
-{
- public class FFmpegProcess : IDisposable
- {
- public static string FFmpegPath = "";
-
- private Process? _ffmpegProcess;
- private Stream? _inputStream;
-
- public bool IsEnd => _ffmpegProcess is { HasExited: true };
-
- protected void Execute(string command)
- {
- if (string.IsNullOrEmpty(command))
- {
- throw new ArgumentException("Command is empty");
- }
- if (string.IsNullOrEmpty(FFmpegPath) || !File.Exists(FFmpegPath))
- {
- throw new FileNotFoundException($"ffmpeg binary not found at '{FFmpegPath}'");
- }
-
- _ffmpegProcess = new Process();
- _ffmpegProcess.StartInfo.FileName = FFmpegPath;
- _ffmpegProcess.StartInfo.Arguments = command;
- _ffmpegProcess.StartInfo.UseShellExecute = false;
- _ffmpegProcess.StartInfo.RedirectStandardInput = true;
- _ffmpegProcess.StartInfo.CreateNoWindow = true;
- _ffmpegProcess.Start();
- _inputStream = _ffmpegProcess.StandardInput.BaseStream;
- }
-
- public void SendDataFromBuffer(byte[] buffer, int length = -1)
- {
- if (_inputStream == null) return;
- var writeLength = length < 0 ? buffer.Length : length;
- _inputStream.Write(buffer, 0, writeLength);
- _inputStream.Flush();
- }
-
- public void SendDataFromNativeBuffer(IntPtr nativePtr, int length)
- {
- if (_inputStream == null) return;
- StreamUtil.WriteNativePtrToStream(nativePtr, length, _inputStream);
- _inputStream.Flush();
- }
-
- public void Close()
- {
- if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return;
- try
- {
- _inputStream.Flush();
- _inputStream.Close();
-
- // Allow FFmpeg to finish encoding and exit gracefully without any timeout to prevent video corruption
- _ffmpegProcess.WaitForExit();
- }
- catch (Exception)
- {
- // Ignore any issues during termination
- }
- finally
- {
- _ffmpegProcess.Dispose();
- _ffmpegProcess = null;
- _inputStream = null;
- }
- }
-
- public void Kill(int timeoutMs = 0)
- {
- try
- {
- if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
- {
- _ffmpegProcess.Kill();
- if (timeoutMs > 0)
- {
- _ffmpegProcess.WaitForExit(timeoutMs);
- }
- }
- }
- catch (Exception)
- {
- // Ignore process already exited or access denied errors during cleanup
- }
- finally
- {
- try
- {
- _inputStream?.Close();
- }
- catch
- {
- // Ignore
- }
- _ffmpegProcess?.Dispose();
- _ffmpegProcess = null;
- _inputStream = null;
- }
- }
-
- public void Dispose()
- {
- Kill();
- }
-
- public static byte[] RentRawBytes(int size)
- {
- var array = ArrayPool.Shared.Rent(size);
- return array;
- }
-
- protected static byte[] ToRawBytes(NativeArray arr) where T : struct
- {
- var val = new NativeSlice(arr).SliceConvert();
- var array = new byte[val.Length];
- val.CopyTo(array);
- return array;
- }
- }
-}
diff --git a/Util/FFmpeg/RateControlMode.cs b/Util/FFmpeg/RateControlMode.cs
new file mode 100644
index 0000000..b790945
--- /dev/null
+++ b/Util/FFmpeg/RateControlMode.cs
@@ -0,0 +1,12 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace ADOFAIRenderer.Util.FFmpeg
+{
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ public enum RateControlMode
+ {
+ CBR,
+ VBR,
+ CQP
+ }
+}
\ No newline at end of file
diff --git a/Util/FFmpeg/VideoFFmpeg.cs b/Util/FFmpeg/VideoFFmpeg.cs
deleted file mode 100644
index 2cbf0e1..0000000
--- a/Util/FFmpeg/VideoFFmpeg.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using ADOFAIRenderer.Core;
-
-namespace ADOFAIRenderer.Util.FFmpeg
-{
- public class VideoFFmpeg : FFmpegProcess
- {
- [SuppressMessage("ReSharper", "InconsistentNaming")]
- public enum Codec
- {
- H264_Default,
- H264_NVIDIA,
- H264_QSV,
- HEVC_Default,
- HEVC_NVIDIA,
- HEVC_QSV,
- AV1_Default,
- AV1_NVIDIA,
- AV1_QSV,
- VP9_Default,
- VP9_NVIDIA
- }
-
- public int Width;
- public int Height;
- public double FrameRate;
- public double VideoFPS;
- public Codec VideoCodec;
-
- private static string Codec2String(Codec c)
- {
- return c switch
- {
- Codec.H264_Default => "-vf vflip",
- Codec.H264_NVIDIA => "-c:v h264_nvenc -vf vflip",
- Codec.H264_QSV => "-c:v h264_qsv -vf vflip",
- Codec.HEVC_Default => "-c:v libx265 -vf vflip",
- Codec.HEVC_NVIDIA => "-c:v hevc_nvenc -vf vflip",
- Codec.HEVC_QSV => "-c:v hevc_qsv -vf vflip",
- Codec.AV1_Default => "-c:v libaom-av1 -vf vflip",
- Codec.AV1_NVIDIA => "-c:v av1_nvenc -vf vflip",
- Codec.AV1_QSV => "-c:v av1_qsv -vf vflip",
- Codec.VP9_Default => "-c:v libvpx-vp9 -vf vflip",
- Codec.VP9_NVIDIA => "-c:v vp9_nvenc -vf vflip",
- _ => "",
- };
- }
-
- public VideoFFmpeg(int w, int h, double frame, double videoFrame, Codec codec)
- {
- Width = w;
- Height = h;
- FrameRate = frame;
- VideoFPS = videoFrame;
- VideoCodec = codec;
- }
-
- public void Start(string path, RenderManager.BitrateControlMode mode, int bitrate, int maxBitrate, int qp)
- {
- if (File.Exists(path))
- {
- File.Delete(path);
- }
-
- string bitrateArgs = mode switch
- {
- RenderManager.BitrateControlMode.CBR => $"-b:v {bitrate}k -minrate {bitrate}k -maxrate {bitrate}k -bufsize {bitrate * 2}k",
- RenderManager.BitrateControlMode.VBR => $"-b:v {bitrate}k -maxrate {maxBitrate}k -bufsize {maxBitrate * 2}k",
- RenderManager.BitrateControlMode.CQP => GetCqpArgs(VideoCodec, qp),
- _ => $"-b:v {bitrate}k"
- };
-
- var command = "-y -f rawvideo -vcodec rawvideo -pixel_format rgba -colorspace rgb -color_range 2 -video_size " +
- Width + "x" + Height + " -framerate " + FrameRate + " -threads 4 -loglevel quiet -i - " +
- Codec2String(VideoCodec) + " " + bitrateArgs + " -pix_fmt yuv420p -fps_mode cfr -r " +
- VideoFPS + " \"" + path + "\"";
-
- Execute(command);
- }
-
- private static string GetCqpArgs(Codec codec, int qp)
- {
- return codec switch
- {
- Codec.H264_NVIDIA => $"-rc constqp -qp {qp}",
- Codec.HEVC_NVIDIA => $"-rc constqp -qp {qp}",
- Codec.AV1_NVIDIA => $"-rc constqp -qp {qp}",
- Codec.VP9_NVIDIA => $"-rc constqp -qp {qp}",
-
- Codec.H264_QSV => $"-global_quality {qp}",
- Codec.HEVC_QSV => $"-global_quality {qp}",
- Codec.AV1_QSV => $"-global_quality {qp}",
-
- Codec.H264_Default => $"-crf {qp}",
- Codec.HEVC_Default => $"-crf {qp}",
- Codec.AV1_Default => $"-crf {qp}",
- Codec.VP9_Default => $"-crf {qp}",
-
- _ => $"-qp {qp}"
- };
- }
- }
-}
diff --git a/Util/RationalUtil.cs b/Util/RationalUtil.cs
new file mode 100644
index 0000000..4a6c293
--- /dev/null
+++ b/Util/RationalUtil.cs
@@ -0,0 +1,59 @@
+using System;
+
+namespace ADOFAIRenderer.Util
+{
+ public class RationalUtil
+ {
+ public static (int Numerator, int Denominator) Double2MaxPrecisionInt(double value,
+ int maxDenominator = 100000000)
+ {
+ // 1. 예외 처리 및 부호 판별
+ if (value == 0.0) return (0, 1);
+
+ var isNegative = value < 0;
+ var sign = isNegative ? -1.0 : 1.0;
+ var target = Math.Abs(value);
+ // 2. 스턴-브로코 / 연속분수 경계값 초기화
+ // 아래 두 행의 분수 (0/1, 1/0) 사이에서 시작하여 타겟에 가까워집니다.
+ long m00 = 0, m01 = 1;
+ long m10 = 1, m11 = 0;
+
+ var x = target;
+
+ // int 범위 오버플로우를 방지하면서 반복 처리
+ while (true)
+ {
+ var ai = (long)Math.Floor(x);
+
+ // 다음 단계의 분자, 분모 계산 (오버플로우 방지를 위해 long으로 임시 계산)
+ var nextNumerator = ai * m10 + m00;
+ var nextDenominator = ai * m11 + m01;
+
+ // int 범위를 벗어나거나, 설정한 최대 분모를 초과하면 이전 단계의 최적값을 반환
+ if (nextNumerator > int.MaxValue || nextDenominator > maxDenominator)
+ {
+ break;
+ }
+
+ // 행렬 업데이트
+ m00 = m10; m01 = m11;
+ m10 = nextNumerator; m11 = nextDenominator;
+
+ // 오차가 거의 없으면 (정밀도 도달) 종료
+ if (Math.Abs(x - ai) < 1e-15) break;
+
+ // 다음 연속분수 항 준비
+ x = 1.0 / (x - ai);
+ }
+
+ // 3. 최종 결정된 int 기반의 분자 분모
+ var finalNumerator = (int)m10;
+ var finalDenominator = (int)m11;
+
+ // 부호 복원
+ if (isNegative) finalNumerator = -finalNumerator;
+
+ return (finalNumerator, finalDenominator);
+ }
+ }
+}
\ No newline at end of file