Add Basic Native Encoder Object Structure (Not Working)

This commit is contained in:
copecone committed 2026-06-02 15:59:51 +09:00
1 parent c6356a28f0
commit bfdfa9beb7
10 files changed
+375 -255

No files matched your search

+4
View File
@@ -85,4 +85,8 @@
<EmbeddedResource Include="Bundle\Linux\scenes" /> <EmbeddedResource Include="Bundle\Linux\scenes" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="FFmpeg.AutoGen" Version="8.1.0" />
</ItemGroup>
</Project> </Project>
+2 -11
View File
@@ -53,14 +53,6 @@ namespace ADOFAIRenderer.Core
public int throttleCount; public int throttleCount;
public bool isThrottling; public bool isThrottling;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum BitrateControlMode
{
CBR,
VBR,
CQP
}
public struct RenderSettings public struct RenderSettings
{ {
public double TargetFPS; public double TargetFPS;
@@ -69,7 +61,7 @@ namespace ADOFAIRenderer.Core
public int RenderWidth; public int RenderWidth;
public int RenderHeight; public int RenderHeight;
public BitrateControlMode BitrateMode; public RateControlMode BitrateMode;
public int Bitrate; public int Bitrate;
public int MaxBitrate; public int MaxBitrate;
public int QP; public int QP;
@@ -80,7 +72,7 @@ namespace ADOFAIRenderer.Core
public int? RandomSeed; public int? RandomSeed;
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, 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) string codec, double endWaitTime, bool showHitText, int? randomSeed)
{ {
TargetFPS = targetFPS; TargetFPS = targetFPS;
@@ -442,7 +434,6 @@ namespace ADOFAIRenderer.Core
DontDestroyOnLoad(_debugMonitorObj); DontDestroyOnLoad(_debugMonitorObj);
} }
FFmpegProcess.FFmpegPath = ffmpegPath;
lock (ImageQueue) lock (ImageQueue)
{ {
ImageQueue.Clear(); ImageQueue.Clear();
+4
View File
@@ -1,7 +1,9 @@
using ADOFAIRenderer; using ADOFAIRenderer;
using ADOFAIRenderer.Core; using ADOFAIRenderer.Core;
using ADOFAIRenderer.Util; using ADOFAIRenderer.Util;
using FFmpeg.AutoGen;
using MelonLoader; using MelonLoader;
using MelonLoader.Utils;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
@@ -19,6 +21,8 @@ namespace ADOFAIRenderer
public override void OnInitializeMelon() public override void OnInitializeMelon()
{ {
ffmpeg.RootPath = MelonEnvironment.UserLibsDirectory;
var renderManagerObj = new GameObject("Render Manager"); var renderManagerObj = new GameObject("Render Manager");
var updateHookerObj = new GameObject("Update Hooker"); var updateHookerObj = new GameObject("Update Hooker");
Object.DontDestroyOnLoad(renderManagerObj); Object.DontDestroyOnLoad(renderManagerObj);
+11 -11
View File
@@ -45,7 +45,7 @@ namespace ADOFAIRenderer.UI
if (codecDropdown != null) if (codecDropdown != null)
{ {
codecDropdown.ClearOptions(); codecDropdown.ClearOptions();
var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList(); var options = System.Enum.GetNames(typeof(Codec)).ToList();
codecDropdown.AddOptions(options); codecDropdown.AddOptions(options);
} }
@@ -53,7 +53,7 @@ namespace ADOFAIRenderer.UI
if (bitrateModeDropdown != null) if (bitrateModeDropdown != null)
{ {
bitrateModeDropdown.ClearOptions(); bitrateModeDropdown.ClearOptions();
var modeOptions = System.Enum.GetNames(typeof(RenderManager.BitrateControlMode)).ToList(); var modeOptions = System.Enum.GetNames(typeof(RateControlMode)).ToList();
bitrateModeDropdown.AddOptions(modeOptions); bitrateModeDropdown.AddOptions(modeOptions);
bitrateModeDropdown.onValueChanged.AddListener(delegate { UpdateUIByMode(); }); bitrateModeDropdown.onValueChanged.AddListener(delegate { UpdateUIByMode(); });
} }
@@ -103,21 +103,21 @@ namespace ADOFAIRenderer.UI
if (bitrateModeDropdown == null) return; if (bitrateModeDropdown == null) return;
if (_bitrateInput == null || _maxBitrateInput == null || _qpInput == null) return; if (_bitrateInput == null || _maxBitrateInput == null || _qpInput == null) return;
var selectedMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value; var selectedMode = (RateControlMode)bitrateModeDropdown.value;
switch (selectedMode) switch (selectedMode)
{ {
case RenderManager.BitrateControlMode.CBR: case RateControlMode.CBR:
_bitrateInput.gameObject.SetActive(true); _bitrateInput.gameObject.SetActive(true);
_maxBitrateInput.gameObject.SetActive(false); _maxBitrateInput.gameObject.SetActive(false);
_qpInput.gameObject.SetActive(false); _qpInput.gameObject.SetActive(false);
break; break;
case RenderManager.BitrateControlMode.VBR: case RateControlMode.VBR:
_bitrateInput.gameObject.SetActive(true); _bitrateInput.gameObject.SetActive(true);
_maxBitrateInput.gameObject.SetActive(true); _maxBitrateInput.gameObject.SetActive(true);
_qpInput.gameObject.SetActive(false); _qpInput.gameObject.SetActive(false);
break; break;
case RenderManager.BitrateControlMode.CQP: case RateControlMode.CQP:
_bitrateInput.gameObject.SetActive(false); _bitrateInput.gameObject.SetActive(false);
_maxBitrateInput.gameObject.SetActive(false); _maxBitrateInput.gameObject.SetActive(false);
_qpInput.gameObject.SetActive(true); _qpInput.gameObject.SetActive(true);
@@ -165,17 +165,17 @@ namespace ADOFAIRenderer.UI
return; return;
} }
var bitrateMode = RenderManager.BitrateControlMode.CBR; var bitrateMode = RateControlMode.CBR;
if (bitrateModeDropdown != null) if (bitrateModeDropdown != null)
{ {
bitrateMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value; bitrateMode = (RateControlMode)bitrateModeDropdown.value;
} }
var bitrate = 0; var bitrate = 0;
var maxBitrate = 0; var maxBitrate = 0;
var qp = 18; 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) 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) 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) if (!int.TryParse(_qpInput.text, out qp) || qp < 0 || qp > 51)
{ {
+10
View File
@@ -0,0 +1,10 @@
using System.Diagnostics.CodeAnalysis;
namespace ADOFAIRenderer.Util.FFmpeg
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum Codec
{
H264, H265, AV1, VP9
}
}
+273
View File
@@ -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<byte> 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);
}
}
}
-129
View File
@@ -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<byte>.Shared.Rent(size);
return array;
}
protected static byte[] ToRawBytes<T>(NativeArray<T> arr) where T : struct
{
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
var array = new byte[val.Length];
val.CopyTo(array);
return array;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
namespace ADOFAIRenderer.Util.FFmpeg
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum RateControlMode
{
CBR,
VBR,
CQP
}
}
-104
View File
@@ -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}"
};
}
}
}
+59
View File
@@ -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);
}
}
}