From bfdfa9beb784580d7268763e15aba9a240c4285d820b598b8e8973dc7e7c7fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4DELTA?= Date: Tue, 2 Jun 2026 15:59:51 +0900 Subject: [PATCH] Add Basic Native Encoder Object Structure (Not Working) --- ADOFAIRenderer.csproj | 4 + Core/RenderManager.cs | 13 +- RenderMod.cs | 4 + UI/RenderButton.cs | 22 +-- Util/FFmpeg/Codec.cs | 10 ++ Util/FFmpeg/FFmpegNativeEncoder.cs | 273 +++++++++++++++++++++++++++++ Util/FFmpeg/FFmpegProcess.cs | 129 -------------- Util/FFmpeg/RateControlMode.cs | 12 ++ Util/FFmpeg/VideoFFmpeg.cs | 104 ----------- Util/RationalUtil.cs | 59 +++++++ 10 files changed, 375 insertions(+), 255 deletions(-) create mode 100644 Util/FFmpeg/Codec.cs create mode 100644 Util/FFmpeg/FFmpegNativeEncoder.cs delete mode 100644 Util/FFmpeg/FFmpegProcess.cs create mode 100644 Util/FFmpeg/RateControlMode.cs delete mode 100644 Util/FFmpeg/VideoFFmpeg.cs create mode 100644 Util/RationalUtil.cs 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