diff --git a/Util/FFmpeg/Codec.cs b/Util/FFmpeg/Codec.cs
index 9250a4a..e74de90 100644
--- a/Util/FFmpeg/Codec.cs
+++ b/Util/FFmpeg/Codec.cs
@@ -5,6 +5,6 @@ namespace ADOFAIRenderer.Util.FFmpeg
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum Codec
{
- H264, H265, AV1, VP9
+ H264, H265, VP9, AV1, VVC
}
}
\ No newline at end of file
diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs
index 0206549..a57b6ea 100644
--- a/Util/FFmpeg/FFmpegNativeEncoder.cs
+++ b/Util/FFmpeg/FFmpegNativeEncoder.cs
@@ -152,6 +152,18 @@ namespace ADOFAIRenderer.Util.FFmpeg
///
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
+ ///
+ /// 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체.
+ /// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
+ ///
+ private AVPacket* _pPacket = null;
+
+ ///
+ /// QSV 하드웨어 가속 모드일 때 사용할 GPU 프레임 구조체.
+ /// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
+ ///
+ private AVFrame* _pHwFrame = null;
+
/// 생성 시 전달받은 인코딩 설정값 (불변).
private readonly EncoderSettings _settings;
@@ -170,6 +182,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 실제 인코더 구현체(예: libx264, h264_nvenc)는 별도로 선택됩니다.
///
// ReSharper disable once InconsistentNaming
+ // ReSharper disable once UnusedMember.Local
private AVCodecID GetAVCodecID()
{
return _settings.Codec switch
@@ -178,6 +191,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
Codec.H265 => AVCodecID.AV_CODEC_ID_HEVC,
Codec.AV1 => AVCodecID.AV_CODEC_ID_AV1,
Codec.VP9 => AVCodecID.AV_CODEC_ID_VP9,
+ Codec.VVC => AVCodecID.AV_CODEC_ID_VVC,
_ => throw new ArgumentOutOfRangeException()
};
}
@@ -196,14 +210,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
///
private string[] GetEncoderCandidates()
{
- return _settings.Codec switch
- {
- Codec.H264 => new[] { "h264_nvenc", "h264_amf", "h264_qsv", "libx264" },
- Codec.H265 => new[] { "hevc_nvenc", "hevc_amf", "hevc_qsv", "libx265" },
- Codec.AV1 => new[] { "av1_nvenc", "av1_amf", "av1_qsv", "libsvtav1", "libaom-av1" },
- Codec.VP9 => new[] { "vp9_nvenc", "vp9_qsv", "libvpx-vp9" },
- _ => Array.Empty()
- };
+ return GPUUtil.GetPrioritizedEncoderCandidates(_settings.Codec).ToArray();
}
///
@@ -250,398 +257,410 @@ namespace ADOFAIRenderer.Util.FFmpeg
try
{
- // ── 1단계: 출력 컨테이너 포맷 컨텍스트 생성 ──
- // avformat_alloc_output_context2는 파일 확장자(outputPath)를 보고
- // 적합한 출력 포맷(mp4, mkv 등)을 자동으로 선택합니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
- fixed (AVFormatContext** ppFormatCtx = &_pFormatContext)
- {
- ffmpeg.avformat_alloc_output_context2(ppFormatCtx, null, null, outputPath);
- }
-
- if (_pFormatContext == null) throw new Exception("출력 포맷 컨텍스트를 생성하지 못했습니다.");
-
- // ── 2단계: 비디오 스트림 추가 ──
- // 컨테이너 안에 비디오 트랙(스트림)을 하나 생성합니다.
- // 오디오가 필요하면 여기서 오디오 스트림을 추가로 만들 수 있습니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
- _pStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
- if (_pStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
-
- // ── 3단계: 인코더 선택 및 열기 ──
- // GetEncoderCandidates()가 반환하는 우선순위 목록을 순회하며
- // 첫 번째로 사용 가능한 인코더를 찾습니다.
- var candidates = GetEncoderCandidates();
- bool openedSuccessfully = false;
-
- foreach (var encoderName in candidates)
- {
- // avcodec_find_encoder_by_name: 이름(예: "h264_nvenc")으로 인코더를 검색합니다.
- // 해당 인코더가 빌드에 포함되어 있지 않으면 null을 반환합니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__encoding.html
- var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoderName);
- if (pCodec == null) continue;
-
- // avcodec_alloc_context3: 선택된 인코더용 코덱 컨텍스트를 할당합니다.
- // 이 컨텍스트에 해상도, 프레임레이트, 품질 등 모든 인코딩 파라미터를 설정합니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
- _pCodecContext = ffmpeg.avcodec_alloc_context3(pCodec);
- if (_pCodecContext == null) continue;
-
- // ── 기본 비디오 파라미터 설정 ──
- _pCodecContext->width = _settings.Width;
- _pCodecContext->height = _settings.Height;
-
- // ── 픽셀 포맷 설정 ──
- // 비디오 인코더는 일반적으로 YUV420P 또는 NV12 형식의 입력을 기대합니다.
- //
- // • YUV420P: Y/U/V 3개의 독립 평면(plane). 대부분의 소프트웨어 인코더가 사용.
- // • NV12: Y 평면 + UV 인터리브 평면. Intel QSV 등 하드웨어 인코더가 선호.
- // • QSV 특수 사항: 코덱 컨텍스트의 pix_fmt은 AV_PIX_FMT_QSV(하드웨어 서피스 식별자)로,
- // 실제 sws_scale 출력은 NV12(소프트웨어 포맷)로 설정합니다.
- // FFmpeg이 내부적으로 NV12 ↔ QSV 서피스 간 매핑을 처리합니다.
- //
- // 참고: https://ffmpeg.org/doxygen/trunk/pixfmt_8h.html
- AVPixelFormat selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
- if (encoderName.Contains("qsv"))
+ // ── 1단계: 출력 컨테이너 포맷 컨텍스트 생성 ──
+ // avformat_alloc_output_context2는 파일 확장자(outputPath)를 보고
+ // 적합한 출력 포맷(mp4, mkv 등)을 자동으로 선택합니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
+ fixed (AVFormatContext** ppFormatCtx = &_pFormatContext)
{
- selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12;
- _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_QSV;
- }
- else
- {
- _pCodecContext->pix_fmt = selectedPixFmt;
+ ffmpeg.avformat_alloc_output_context2(ppFormatCtx, null, null, outputPath);
}
- // ── 프레임레이트 및 타임베이스 설정 ──
- // time_base: 프레임 간 시간 간격의 기본 단위.
- // 예) 60fps → time_base = 1/60초 (num=1, den=60)
- // PTS(Presentation Time Stamp) 값에 time_base를 곱하면 실제 시간(초)이 됩니다.
- // PTS=150, time_base=1/60 → 150 * (1/60) = 2.5초
- //
- // framerate: 인코더에 힌트를 주는 프레임레이트 값 (time_base의 역수).
- //
- // RationalUtil: FPS(double)를 분수 형태(분자/분모)로 변환합니다.
- // 예) 29.97 → 30000/1001 (NTSC 표준)
- //
- // 참고: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html
- 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 };
+ if (_pFormatContext == null) throw new Exception("출력 포맷 컨텍스트를 생성하지 못했습니다.");
- // ── GOP(Group of Pictures) 크기 설정 ──
- // GOP: 키프레임(I-frame) 간격. 예) gop_size=120, 60fps → 2초마다 키프레임.
- // 키프레임은 완전한 이미지를 담고 있어 탐색(seek) 지점이 됩니다.
- // 너무 크면 탐색 정밀도가 떨어지고, 너무 작으면 파일 크기가 증가합니다.
- _pCodecContext->gop_size = (int)Math.Round(_settings.FPS * 2);
+ // ── 2단계: 비디오 스트림 추가 ──
+ // 컨테이너 안에 비디오 트랙(스트림)을 하나 생성합니다.
+ // 오디오가 필요하면 여기서 오디오 스트림을 추가로 만들 수 있습니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
+ _pStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
+ if (_pStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
- // ── B-프레임 비활성화 ──
- // B-프레임(양방향 예측 프레임): 앞뒤 프레임을 참조하여 압축률을 높이지만,
- // 인코딩 지연(latency)이 증가하고 일부 인코더에서 호환성 문제가 발생할 수 있습니다.
- // 실시간 녹화 시나리오에서는 비활성화하여 지연을 최소화합니다.
- //
- // 참고: has_b_frames는 인코더가 내부적으로 설정하는 읽기 전용 필드이므로
- // 직접 세팅하지 않습니다.
- //
- // SVT-AV1 특수 사항: max_b_frames를 AVCodecContext에서 직접 설정하면
- // "bad parameter" 에러가 발생하므로, svtav1-params를 통해 제어합니다.
- // pred-struct=1: Low Delay (IPPP) 구조로 B-프레임 비활성화
- if (encoderName == "libsvtav1")
+ // ── 3단계: 인코더 선택 및 열기 ──
+ // GetEncoderCandidates()가 반환하는 우선순위 목록을 순회하며
+ // 첫 번째로 사용 가능한 인코더를 찾습니다.
+ var candidates = GetEncoderCandidates();
+ bool openedSuccessfully = false;
+
+ foreach (var encoderName in candidates)
{
- ffmpeg.av_opt_set(_pCodecContext->priv_data, "svtav1-params", "pred-struct=1", 0);
- }
- else
- {
- _pCodecContext->max_b_frames = 0;
- }
+ // avcodec_find_encoder_by_name: 이름(예: "h264_nvenc")으로 인코더를 검색합니다.
+ // 해당 인코더가 빌드에 포함되어 있지 않으면 null을 반환합니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__encoding.html
+ var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoderName);
+ if (pCodec == null) continue;
- // ── 글로벌 헤더 플래그 ──
- // 일부 컨테이너 포맷(mp4 등)은 코덱의 설정 정보(SPS/PPS 등)를
- // 각 패킷이 아닌 파일 헤더에 한 번만 기록하길 요구합니다.
- // AVFMT_GLOBALHEADER 플래그가 설정된 포맷이면 AV_CODEC_FLAG_GLOBAL_HEADER를 켜서
- // 인코더가 extradata에 설정 정보를 저장하도록 합니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/avformat_8h.html
- if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
- {
- _pCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
- }
+ // avcodec_alloc_context3: 선택된 인코더용 코덱 컨텍스트를 할당합니다.
+ // 이 컨텍스트에 해상도, 프레임레이트, 품질 등 모든 인코딩 파라미터를 설정합니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
+ _pCodecContext = ffmpeg.avcodec_alloc_context3(pCodec);
+ if (_pCodecContext == null) continue;
- // ── 품질/비트레이트 설정 적용 ──
- ApplyQualitySettings(encoderName);
+ // ── 기본 비디오 파라미터 설정 ──
+ _pCodecContext->width = _settings.Width;
+ _pCodecContext->height = _settings.Height;
+ _pCodecContext->thread_count = 0; // 멀티스레딩 활성화 (0 = 자동 설정)
- // ── QSV 인코더 하드웨어 디바이스 초기화 ──
- // QSV(Quick Sync Video)는 Intel GPU의 고정 기능 인코딩 하드웨어를 사용합니다.
- // FFmpeg에서 QSV를 사용하려면 하드웨어 디바이스 컨텍스트를 생성해야 합니다.
- //
- // 아래에서 3가지 전략을 순차적으로 시도합니다:
- // 전략 1) child_device_type=d3d11va 옵션으로 QSV 디바이스 직접 생성
- // → FFmpeg 내부에서 D3D11 디바이스를 자식으로 생성하여 QSV에 연결
- // 전략 2) D3D11VA 디바이스를 먼저 만들고, QSV 디바이스를 파생(derive)
- // → 일부 드라이버에서 전략 1이 실패할 때 유효
- // 전략 3) QSV 디바이스 직접 생성 (레거시 방식)
- // → 구형 드라이버/시스템에서 폴백
- //
- // 디바이스 확보 후, GPU 프레임 풀 활용을 시도합니다:
- // hw_frames_ctx: GPU VRAM에 프레임 풀을 사전 할당 (제로 카피, 최고 성능)
- // hw_device_ctx: 시스템 메모리에서 인코더가 직접 프레임을 읽음 (호환성 우수)
- //
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
- if (encoderName.Contains("qsv"))
- {
- AVBufferRef* hwDeviceCtx = null;
- bool qsvReady = false;
- int err;
-
- // ── 전략 1: child_device_type=d3d11va 옵션 ──
- // av_hwdevice_ctx_create에 "child_device_type" 딕셔너리를 전달하면
- // FFmpeg이 내부적으로 D3D11 디바이스를 생성하고 QSV 세션에 연결합니다.
- // 대부분의 최신 Intel 드라이버에서 가장 안정적으로 동작합니다.
+ // ── 픽셀 포맷 설정 ──
+ // 비디오 인코더는 일반적으로 YUV420P 또는 NV12 형식의 입력을 기대합니다.
+ //
+ // • YUV420P: Y/U/V 3개의 독립 평면(plane). 대부분의 소프트웨어 인코더가 사용.
+ // • NV12: Y 평면 + UV 인터리브 평면. 하드웨어 인코더(NVENC, AMF, QSV)가 선호.
+ // • QSV 특수 사항: 코덱 컨텍스트의 pix_fmt은 AV_PIX_FMT_QSV(하드웨어 서피스 식별자)로,
+ // 실제 sws_scale 출력은 NV12(소프트웨어 포맷)로 설정합니다.
+ // FFmpeg이 내부적으로 NV12 ↔ QSV 서피스 간 매핑을 처리합니다.
+ //
+ // 참고: https://ffmpeg.org/doxygen/trunk/pixfmt_8h.html
+ AVPixelFormat selectedPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
+ if (encoderName.Contains("qsv"))
{
- AVDictionary* opts = null;
- ffmpeg.av_dict_set(&opts, "child_device_type", "d3d11va", 0);
- err = ffmpeg.av_hwdevice_ctx_create(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, null, opts, 0);
- ffmpeg.av_dict_free(&opts);
- if (err >= 0)
- {
- qsvReady = true;
- Melon.Logger.Msg($"{encoderName}: child_device_type=d3d11va 방식으로 QSV 디바이스 생성 성공");
- }
- else
- {
- Melon.Logger.Warning($"{encoderName}: child_device_type 방식 실패 — {GetFFmpegErrorString(err)} (Code: {err})");
- }
+ selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12;
+ _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_QSV;
+ }
+ else if (encoderName.Contains("nvenc") || encoderName.Contains("amf"))
+ {
+ selectedPixFmt = AVPixelFormat.AV_PIX_FMT_NV12;
+ _pCodecContext->pix_fmt = selectedPixFmt;
+ }
+ else
+ {
+ _pCodecContext->pix_fmt = selectedPixFmt;
}
- // ── 전략 2: D3D11VA → QSV 파생(derive) ──
- // D3D11VA 디바이스를 먼저 생성한 뒤,
- // av_hwdevice_ctx_create_derived로 QSV 디바이스를 파생시킵니다.
- // 두 디바이스가 동일한 GPU를 공유하게 됩니다.
- if (!qsvReady)
+ // ── 프레임레이트 및 타임베이스 설정 ──
+ // time_base: 프레임 간 시간 간격의 기본 단위.
+ // 예) 60fps → time_base = 1/60초 (num=1, den=60)
+ // PTS(Presentation Time Stamp) 값에 time_base를 곱하면 실제 시간(초)이 됩니다.
+ // PTS=150, time_base=1/60 → 150 * (1/60) = 2.5초
+ //
+ // framerate: 인코더에 힌트를 주는 프레임레이트 값 (time_base의 역수).
+ //
+ // RationalUtil: FPS(double)를 분수 형태(분자/분모)로 변환합니다.
+ // 예) 29.97 → 30000/1001 (NTSC 표준)
+ //
+ // 참고: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html
+ 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 };
+
+ // ── GOP(Group of Pictures) 크기 설정 ──
+ // GOP: 키프레임(I-frame) 간격. 예) gop_size=120, 60fps → 2초마다 키프레임.
+ // 키프레임은 완전한 이미지를 담고 있어 탐색(seek) 지점이 됩니다.
+ // 너무 크면 탐색 정밀도가 떨어지고, 너무 작으면 파일 크기가 증가합니다.
+ _pCodecContext->gop_size = (int)Math.Round(_settings.FPS * 2);
+
+ // ── B-프레임 비활성화 ──
+ // B-프레임(양방향 예측 프레임): 앞뒤 프레임을 참조하여 압축률을 높이지만,
+ // 인코딩 지연(latency)이 증가하고 일부 인코더에서 호환성 문제가 발생할 수 있습니다.
+ // 실시간 녹화 시나리오에서는 비활성화하여 지연을 최소화합니다.
+ //
+ // 참고: has_b_frames는 인코더가 내부적으로 설정하는 읽기 전용 필드이므로
+ // 직접 세팅하지 않습니다.
+ //
+ // SVT-AV1 특수 사항: max_b_frames를 AVCodecContext에서 직접 설정하면
+ // "bad parameter" 에러가 발생하므로, svtav1-params를 통해 제어합니다.
+ // pred-struct=1: Low Delay (IPPP) 구조로 B-프레임 비활성화
+ if (encoderName == "libsvtav1")
{
- AVBufferRef* d3d11DeviceCtx = null;
- err = ffmpeg.av_hwdevice_ctx_create(&d3d11DeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA, null, null, 0);
- if (err >= 0)
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "svtav1-params", "pred-struct=1", 0);
+ }
+ else
+ {
+ _pCodecContext->max_b_frames = 0;
+ }
+
+ // ── 글로벌 헤더 플래그 ──
+ // 일부 컨테이너 포맷(mp4 등)은 코덱의 설정 정보(SPS/PPS 등)를
+ // 각 패킷이 아닌 파일 헤더에 한 번만 기록하길 요구합니다.
+ // AVFMT_GLOBALHEADER 플래그가 설정된 포맷이면 AV_CODEC_FLAG_GLOBAL_HEADER를 켜서
+ // 인코더가 extradata에 설정 정보를 저장하도록 합니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/avformat_8h.html
+ if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
+ {
+ _pCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ // ── 품질/비트레이트 설정 적용 ──
+ ApplyQualitySettings(encoderName);
+
+ // ── QSV 인코더 하드웨어 디바이스 초기화 ──
+ // QSV(Quick Sync Video)는 Intel GPU의 고정 기능 인코딩 하드웨어를 사용합니다.
+ // FFmpeg에서 QSV를 사용하려면 하드웨어 디바이스 컨텍스트를 생성해야 합니다.
+ //
+ // 아래에서 3가지 전략을 순차적으로 시도합니다:
+ // 전략 1) child_device_type=d3d11va 옵션으로 QSV 디바이스 직접 생성
+ // → FFmpeg 내부에서 D3D11 디바이스를 자식으로 생성하여 QSV에 연결
+ // 전략 2) D3D11VA 디바이스를 먼저 만들고, QSV 디바이스를 파생(derive)
+ // → 일부 드라이버에서 전략 1이 실패할 때 유효
+ // 전략 3) QSV 디바이스 직접 생성 (레거시 방식)
+ // → 구형 드라이버/시스템에서 폴백
+ //
+ // 디바이스 확보 후, GPU 프레임 풀 활용을 시도합니다:
+ // hw_frames_ctx: GPU VRAM에 프레임 풀을 사전 할당 (제로 카피, 최고 성능)
+ // hw_device_ctx: 시스템 메모리에서 인코더가 직접 프레임을 읽음 (호환성 우수)
+ //
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
+ if (encoderName.Contains("qsv"))
+ {
+ AVBufferRef* hwDeviceCtx = null;
+ bool qsvReady = false;
+ int err;
+
+ // ── 전략 1: child_device_type=d3d11va 옵션 ──
+ // av_hwdevice_ctx_create에 "child_device_type" 딕셔너리를 전달하면
+ // FFmpeg이 내부적으로 D3D11 디바이스를 생성하고 QSV 세션에 연결합니다.
+ // 대부분의 최신 Intel 드라이버에서 가장 안정적으로 동작합니다.
{
- err = ffmpeg.av_hwdevice_ctx_create_derived(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, d3d11DeviceCtx, 0);
+ AVDictionary* opts = null;
+ ffmpeg.av_dict_set(&opts, "child_device_type", "d3d11va", 0);
+ err = ffmpeg.av_hwdevice_ctx_create(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, null, opts, 0);
+ ffmpeg.av_dict_free(&opts);
if (err >= 0)
{
qsvReady = true;
- Melon.Logger.Msg($"{encoderName}: D3D11VA → QSV 파생 방식으로 디바이스 생성 성공");
+ Melon.Logger.Msg($"{encoderName}: child_device_type=d3d11va 방식으로 QSV 디바이스 생성 성공");
+ }
+ else
+ {
+ Melon.Logger.Warning($"{encoderName}: child_device_type 방식 실패 — {GetFFmpegErrorString(err)} (Code: {err})");
}
- ffmpeg.av_buffer_unref(&d3d11DeviceCtx);
}
- }
- // ── 전략 3: QSV 직접 생성 (레거시) ──
- if (!qsvReady)
- {
- err = ffmpeg.av_hwdevice_ctx_create(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, null, null, 0);
- if (err >= 0) qsvReady = true;
- }
-
- if (!qsvReady)
- {
- Melon.Logger.Warning($"{encoderName}: 모든 QSV 디바이스 생성 전략 실패. 다음 후보 코덱을 테스트합니다.");
+ // ── 전략 2: D3D11VA → QSV 파생(derive) ──
+ // D3D11VA 디바이스를 먼저 생성한 뒤,
+ // av_hwdevice_ctx_create_derived로 QSV 디바이스를 파생시킵니다.
+ // 두 디바이스가 동일한 GPU를 공유하게 됩니다.
+ if (!qsvReady)
{
- var pCtx = _pCodecContext;
- ffmpeg.avcodec_free_context(&pCtx);
+ // ReSharper disable once InconsistentNaming
+ AVBufferRef* d3d11DeviceCtx = null;
+ err = ffmpeg.av_hwdevice_ctx_create(&d3d11DeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA, null, null, 0);
+ if (err >= 0)
+ {
+ err = ffmpeg.av_hwdevice_ctx_create_derived(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, d3d11DeviceCtx, 0);
+ if (err >= 0)
+ {
+ qsvReady = true;
+ Melon.Logger.Msg($"{encoderName}: D3D11VA → QSV 파생 방식으로 디바이스 생성 성공");
+ }
+ ffmpeg.av_buffer_unref(&d3d11DeviceCtx);
+ }
}
- _pCodecContext = null;
- continue;
- }
- // ── hw_frames_ctx 시도: GPU 프레임 풀 사전 생성 ──
- // AVHWFramesContext는 GPU VRAM에 프레임 풀을 미리 할당하여
- // 매 프레임마다 메모리 할당/해제 오버헤드를 제거합니다.
- //
- // 장점: 제로 카피 가능, 최고 성능
- // 단점: GPU/드라이버에 따라 해상도/포맷 제약으로 실패할 수 있음
- // (예: Intel Arc에서 특정 텍스처 크기 제약 → E_INVALIDARG)
- //
- // initial_pool_size: 미리 할당할 프레임 수. 인코더 파이프라인 깊이에 맞춤.
- // 해상도를 32 배수로 정렬: QSV 하드웨어가 요구하는 텍스처 정렬 조건.
- //
- // 참고:
- // AVHWFramesContext: https://ffmpeg.org/doxygen/trunk/structAVHWFramesContext.html
- // av_hwframe_ctx_alloc: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
- bool framesCtxReady = false;
- AVBufferRef* hwFramesRef = ffmpeg.av_hwframe_ctx_alloc(hwDeviceCtx);
- if (hwFramesRef != null)
- {
- var framesCtx = (AVHWFramesContext*)hwFramesRef->data;
- framesCtx->format = AVPixelFormat.AV_PIX_FMT_QSV; // 하드웨어 서피스 포맷
- framesCtx->sw_format = AVPixelFormat.AV_PIX_FMT_NV12; // 소프트웨어 측 포맷
- framesCtx->width = (_settings.Width + 31) & ~31; // 32 배수 정렬 (비트 마스크)
- framesCtx->height = (_settings.Height + 31) & ~31; // 32 배수 정렬 (비트 마스크)
- framesCtx->initial_pool_size = 4; // 프레임 풀 크기
-
- err = ffmpeg.av_hwframe_ctx_init(hwFramesRef);
- if (err >= 0)
+ // ── 전략 3: QSV 직접 생성 (레거시) ──
+ if (!qsvReady)
{
- // 성공: 코덱 컨텍스트에 hw_frames_ctx를 연결합니다.
- // av_buffer_ref로 참조 카운트를 증가시켜 hwFramesRef 해제 후에도 유효하게 합니다.
- _pCodecContext->hw_frames_ctx = ffmpeg.av_buffer_ref(hwFramesRef);
- _isQsvEncoder = true;
- framesCtxReady = true;
- Melon.Logger.Msg($"{encoderName}: hw_frames_ctx 초기화 성공 (GPU 프레임 풀 모드)");
+ err = ffmpeg.av_hwdevice_ctx_create(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, null, null, 0);
+ if (err >= 0) qsvReady = true;
}
- else
+
+ if (!qsvReady)
{
- Melon.Logger.Warning($"{encoderName}: hw_frames_ctx 실패 — {GetFFmpegErrorString(err)} (Code: {err}). hw_device_ctx 모드로 폴백합니다.");
+ Melon.Logger.Warning($"{encoderName}: 모든 QSV 디바이스 생성 전략 실패. 다음 후보 코덱을 테스트합니다.");
+ {
+ var pCtx = _pCodecContext;
+ ffmpeg.avcodec_free_context(&pCtx);
+ }
+ _pCodecContext = null;
+ continue;
}
- ffmpeg.av_buffer_unref(&hwFramesRef);
+
+ // ── hw_frames_ctx 시도: GPU 프레임 풀 사전 생성 ──
+ // AVHWFramesContext는 GPU VRAM에 프레임 풀을 미리 할당하여
+ // 매 프레임마다 메모리 할당/해제 오버헤드를 제거합니다.
+ //
+ // 장점: 제로 카피 가능, 최고 성능
+ // 단점: GPU/드라이버에 따라 해상도/포맷 제약으로 실패할 수 있음
+ // (예: Intel Arc에서 특정 텍스처 크기 제약 → E_INVALIDARG)
+ //
+ // initial_pool_size: 미리 할당할 프레임 수. 인코더 파이프라인 깊이에 맞춤.
+ // 해상도를 32 배수로 정렬: QSV 하드웨어가 요구하는 텍스처 정렬 조건.
+ //
+ // 참고:
+ // AVHWFramesContext: https://ffmpeg.org/doxygen/trunk/structAVHWFramesContext.html
+ // av_hwframe_ctx_alloc: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
+ bool framesCtxReady = false;
+ AVBufferRef* hwFramesRef = ffmpeg.av_hwframe_ctx_alloc(hwDeviceCtx);
+ if (hwFramesRef != null)
+ {
+ var framesCtx = (AVHWFramesContext*)hwFramesRef->data;
+ framesCtx->format = AVPixelFormat.AV_PIX_FMT_QSV; // 하드웨어 서피스 포맷
+ framesCtx->sw_format = AVPixelFormat.AV_PIX_FMT_NV12; // 소프트웨어 측 포맷
+ framesCtx->width = (_settings.Width + 31) & ~31; // 32 배수 정렬 (비트 마스크)
+ framesCtx->height = (_settings.Height + 31) & ~31; // 32 배수 정렬 (비트 마스크)
+ framesCtx->initial_pool_size = 4; // 프레임 풀 크기
+
+ err = ffmpeg.av_hwframe_ctx_init(hwFramesRef);
+ if (err >= 0)
+ {
+ // 성공: 코덱 컨텍스트에 hw_frames_ctx를 연결합니다.
+ // av_buffer_ref로 참조 카운트를 증가시켜 hwFramesRef 해제 후에도 유효하게 합니다.
+ _pCodecContext->hw_frames_ctx = ffmpeg.av_buffer_ref(hwFramesRef);
+ _isQsvEncoder = true;
+ framesCtxReady = true;
+ Melon.Logger.Msg($"{encoderName}: hw_frames_ctx 초기화 성공 (GPU 프레임 풀 모드)");
+
+ _pHwFrame = ffmpeg.av_frame_alloc();
+ if (_pHwFrame == null)
+ throw new Exception("QSV 하드웨어 프레임 구조체를 할당하지 못했습니다.");
+ }
+ else
+ {
+ Melon.Logger.Warning($"{encoderName}: hw_frames_ctx 실패 — {GetFFmpegErrorString(err)} (Code: {err}). hw_device_ctx 모드로 폴백합니다.");
+ }
+ ffmpeg.av_buffer_unref(&hwFramesRef);
+ }
+
+ // ── hw_frames_ctx 실패 시: hw_device_ctx + NV12 시스템 메모리 폴백 ──
+ // hw_device_ctx 모드에서는 인코더가 시스템 메모리(RAM)의 NV12 프레임을
+ // 직접 받아서 내부적으로 GPU로 업로드합니다.
+ // hw_frames_ctx보다 약간 느리지만 호환성이 훨씬 좋습니다.
+ //
+ // pix_fmt을 NV12로 변경: 하드웨어 서피스(QSV) 대신 소프트웨어 포맷 직접 사용.
+ // _isQsvEncoder = false: WriteFrame()에서 수동 업로드를 건너뜁니다.
+ if (!framesCtxReady)
+ {
+ _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_NV12;
+ _pCodecContext->hw_device_ctx = ffmpeg.av_buffer_ref(hwDeviceCtx);
+ _isQsvEncoder = false; // 시스템 메모리 모드이므로 프레임 업로드 불필요
+ }
+
+ // hwDeviceCtx의 소유권은 코덱 컨텍스트로 이전했으므로
+ // 로컬 참조를 해제합니다 (참조 카운트 관리).
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__buffer.html
+ ffmpeg.av_buffer_unref(&hwDeviceCtx);
}
- // ── hw_frames_ctx 실패 시: hw_device_ctx + NV12 시스템 메모리 폴백 ──
- // hw_device_ctx 모드에서는 인코더가 시스템 메모리(RAM)의 NV12 프레임을
- // 직접 받아서 내부적으로 GPU로 업로드합니다.
- // hw_frames_ctx보다 약간 느리지만 호환성이 훨씬 좋습니다.
+ // ── 인코더별 속도 프리셋 지정 ──
+ // 프리셋은 인코딩 속도 ↔ 압축 효율 간의 트레이드오프를 결정합니다.
+ // 빠른 프리셋: 인코딩 빠르지만 파일 크기 증가
+ // 느린 프리셋: 인코딩 느리지만 같은 품질에서 파일 크기 감소
//
- // pix_fmt을 NV12로 변경: 하드웨어 서피스(QSV) 대신 소프트웨어 포맷 직접 사용.
- // _isQsvEncoder = false: WriteFrame()에서 수동 업로드를 건너뜁니다.
- if (!framesCtxReady)
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__opt__set__funcs.html
+ if (encoderName.Contains("nvenc"))
{
- _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_NV12;
- _pCodecContext->hw_device_ctx = ffmpeg.av_buffer_ref(hwDeviceCtx);
- _isQsvEncoder = false; // 시스템 메모리 모드이므로 프레임 업로드 불필요
+ // NVENC 프리셋 범위: p1(최고속) ~ p7(최고품질). p4 = 중간(default)
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "p4", 0);
+ }
+ else if (encoderName.Contains("libx264") || encoderName.Contains("libx265"))
+ {
+ // x264/x265 프리셋: ultrafast, superfast, veryfast, faster, fast, medium, slow, ...
+ // CPU 인코딩은 병목이 되기 쉬우므로 빠른 프리셋 사용
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "veryfast", 0);
+ }
+ else if (encoderName == "libsvtav1")
+ {
+ // SVT-AV1 프리셋: 0(최고 품질/최저 속도) ~ 13(최고 속도/최저 품질)
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "10", 0);
+ }
+ else if (encoderName == "libaom-av1")
+ {
+ // libaom cpu-used: 0(최고 품질) ~ 8(최고 속도)
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "cpu-used", "6", 0);
+ // "realtime" usage: 단일 패스, 저지연 인코딩 모드
+ ffmpeg.av_opt_set(_pCodecContext->priv_data, "usage", "realtime", 0);
+ }
+ // 다른 인코더들(AMF, QSV 등)은 기본 프리셋을 그대로 사용합니다.
+ // 지원하지 않는 옵션을 설정하면 에러가 발생할 수 있으므로 건너뜁니다.
+
+ // ── 인코더 열기 ──
+ // avcodec_open2: 위에서 설정한 모든 파라미터를 기반으로 인코더를 초기화합니다.
+ // 성공하면 0 이상을 반환하고, 이후 avcodec_send_frame/avcodec_receive_packet으로
+ // 프레임 인코딩이 가능해집니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
+ int openResult = ffmpeg.avcodec_open2(_pCodecContext, pCodec, null);
+ if (openResult >= 0)
+ {
+ bool isHardware = encoderName.Contains("nvenc") || encoderName.Contains("amf") || encoderName.Contains("qsv");
+ string accelStatus = isHardware ? "하드웨어 가속 활성화" : "소프트웨어 CPU 사용";
+ Melon.Logger.Msg($"비디오 인코더 구동 성공: {encoderName} ({accelStatus})");
+ _dstPixFmt = selectedPixFmt;
+ openedSuccessfully = true;
+ break;
}
- // hwDeviceCtx의 소유권은 코덱 컨텍스트로 이전했으므로
- // 로컬 참조를 해제합니다 (참조 카운트 관리).
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__buffer.html
- ffmpeg.av_buffer_unref(&hwDeviceCtx);
+ // 구동 실패 시 코덱 컨텍스트를 해제하고 다음 인코더를 시도합니다.
+ Melon.Logger.Warning($"{encoderName} 하드웨어 인코더 구동 실패: {GetFFmpegErrorString(openResult)} (Code: {openResult}). 다음 후보 코덱을 테스트합니다.");
+ var pCodecCtx = _pCodecContext;
+ ffmpeg.avcodec_free_context(&pCodecCtx);
+ _pCodecContext = null;
}
- // ── 인코더별 속도 프리셋 지정 ──
- // 프리셋은 인코딩 속도 ↔ 압축 효율 간의 트레이드오프를 결정합니다.
- // 빠른 프리셋: 인코딩 빠르지만 파일 크기 증가
- // 느린 프리셋: 인코딩 느리지만 같은 품질에서 파일 크기 감소
- //
- // 참고: https://ffmpeg.org/doxygen/trunk/group__opt__set__funcs.html
- if (encoderName.Contains("nvenc"))
+ if (!openedSuccessfully || _pCodecContext == null)
{
- // NVENC 프리셋 범위: p1(최고속) ~ p7(최고품질). p4 = 중간(default)
- ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "p4", 0);
+ throw new Exception("사용 가능한 비디오 인코더(하드웨어 가속 및 CPU 소프트웨어 전체)의 구동에 실패했습니다.");
}
- else if (encoderName.Contains("libx264") || encoderName.Contains("libx265"))
- {
- // x264/x265 프리셋: ultrafast, superfast, veryfast, faster, fast, medium, slow, ...
- // CPU 인코딩은 병목이 되기 쉬우므로 빠른 프리셋 사용
- ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "veryfast", 0);
- }
- else if (encoderName == "libsvtav1")
- {
- // SVT-AV1 프리셋: 0(최고 품질/최저 속도) ~ 13(최고 속도/최저 품질)
- ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "10", 0);
- }
- else if (encoderName == "libaom-av1")
- {
- // libaom cpu-used: 0(최고 품질) ~ 8(최고 속도)
- ffmpeg.av_opt_set(_pCodecContext->priv_data, "cpu-used", "6", 0);
- // "realtime" usage: 단일 패스, 저지연 인코딩 모드
- ffmpeg.av_opt_set(_pCodecContext->priv_data, "usage", "realtime", 0);
- }
- // 다른 인코더들(AMF, QSV 등)은 기본 프리셋을 그대로 사용합니다.
- // 지원하지 않는 옵션을 설정하면 에러가 발생할 수 있으므로 건너뜁니다.
- // ── 인코더 열기 ──
- // avcodec_open2: 위에서 설정한 모든 파라미터를 기반으로 인코더를 초기화합니다.
- // 성공하면 0 이상을 반환하고, 이후 avcodec_send_frame/avcodec_receive_packet으로
- // 프레임 인코딩이 가능해집니다.
+ // ── 4단계: 코덱 파라미터를 스트림에 복사 ──
+ // 코덱 컨텍스트에 설정된 파라미터(해상도, 코덱 ID, 프로파일 등)를
+ // 스트림의 codecpar에 복사합니다. 컨테이너 헤더에 기록될 정보입니다.
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
- int openResult = ffmpeg.avcodec_open2(_pCodecContext, pCodec, null);
- if (openResult >= 0)
+ if (ffmpeg.avcodec_parameters_from_context(_pStream->codecpar, _pCodecContext) < 0)
+ throw new Exception("코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
+
+ // ── 5단계: 출력 파일 I/O 열기 ──
+ // AVFMT_NOFILE 플래그가 없는 포맷은 실제 파일 I/O가 필요합니다.
+ // avio_open으로 파일을 쓰기 모드로 열고, 포맷 컨텍스트의 pb(I/O 컨텍스트)에 연결합니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__io.html
+ if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
{
- bool isHardware = encoderName.Contains("nvenc") || encoderName.Contains("amf") || encoderName.Contains("qsv");
- string accelStatus = isHardware ? "하드웨어 가속 활성화" : "소프트웨어 CPU 사용";
- Melon.Logger.Msg($"비디오 인코더 구동 성공: {encoderName} ({accelStatus})");
- _dstPixFmt = selectedPixFmt;
- openedSuccessfully = true;
- break;
+ if (ffmpeg.avio_open(&_pFormatContext->pb, outputPath, ffmpeg.AVIO_FLAG_WRITE) < 0)
+ throw new Exception($"출력 파일 {outputPath}를 열 수 없습니다.");
}
- // 구동 실패 시 코덱 컨텍스트를 해제하고 다음 인코더를 시도합니다.
- Melon.Logger.Warning($"{encoderName} 하드웨어 인코더 구동 실패: {GetFFmpegErrorString(openResult)} (Code: {openResult}). 다음 후보 코덱을 테스트합니다.");
- var pCodecCtx = _pCodecContext;
- ffmpeg.avcodec_free_context(&pCodecCtx);
- _pCodecContext = null;
- }
+ // 파일 헤더(ftyp, moov 등 컨테이너 메타데이터) 작성.
+ // 이 시점 이후부터 av_interleaved_write_frame으로 패킷을 쓸 수 있습니다.
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html
+ if (ffmpeg.avformat_write_header(_pFormatContext, null) < 0)
+ throw new Exception("파일 헤더를 작성하는 데 실패했습니다.");
- if (!openedSuccessfully || _pCodecContext == null)
+ // ── 6단계: 프레임 버퍼 및 색공간 변환기 초기화 ──
+ // av_frame_alloc: AVFrame 구조체를 힙에 할당합니다 (픽셀 데이터는 아직 없음).
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html
+ _pSrcFrame = ffmpeg.av_frame_alloc();
+ _pDstFrame = ffmpeg.av_frame_alloc();
+ _pPacket = ffmpeg.av_packet_alloc();
+ if (_pSrcFrame == null || _pDstFrame == null || _pPacket == null)
+ throw new Exception("프레임 또는 패킷 구조체를 할당하지 못했습니다.");
+
+ // 목적지 프레임의 형식과 크기를 설정한 뒤 실제 픽셀 버퍼를 할당합니다.
+ // av_frame_get_buffer: format, width, height에 맞는 메모리를 할당하고
+ // data[]/linesize[] 포인터를 설정합니다. 두 번째 인자 0은 기본 정렬을 의미합니다.
+ _pDstFrame->format = (int)_dstPixFmt;
+ _pDstFrame->width = _settings.Width;
+ _pDstFrame->height = _settings.Height;
+
+ if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
+ throw new Exception($"{_dstPixFmt} 프레임 버퍼를 할당하는 데 실패했습니다.");
+
+ // sws_getContext: 소프트웨어 스케일러(색공간 변환기) 초기화.
+ // 입력: RGBA (Unity 캡처 포맷, 4바이트/픽셀)
+ // 출력: YUV420P 또는 NV12 (인코더 요구 포맷)
+ //
+ // 여기서는 해상도 변환 없이 색공간만 변환하지만, sws_scale은
+ // 동시에 리사이즈도 수행할 수 있습니다.
+ //
+ // SWS_BILINEAR: 바이리니어 보간법. 색공간만 변환할 때는 품질 차이가 미미하며,
+ // 속도와 품질의 균형이 좋은 기본 옵션입니다.
+ //
+ // 참고: https://ffmpeg.org/doxygen/trunk/group__libsws.html
+ _pSwsContext = ffmpeg.sws_getContext(
+ _settings.Width, _settings.Height, AVPixelFormat.AV_PIX_FMT_RGBA,
+ _settings.Width, _settings.Height, _dstPixFmt,
+ (int)SwsFlags.SWS_BILINEAR, null, null, null);
+
+ if (_pSwsContext == null)
+ throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다.");
+
+ _isInitialized = true;
+ }
+ catch (Exception)
{
- throw new Exception("사용 가능한 비디오 인코더(하드웨어 가속 및 CPU 소프트웨어 전체)의 구동에 실패했습니다.");
+ // 초기화 도중 실패 시 부분 할당된 리소스를 모두 정리합니다.
+ Dispose();
+ throw;
}
-
- // ── 4단계: 코덱 파라미터를 스트림에 복사 ──
- // 코덱 컨텍스트에 설정된 파라미터(해상도, 코덱 ID, 프로파일 등)를
- // 스트림의 codecpar에 복사합니다. 컨테이너 헤더에 기록될 정보입니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
- if (ffmpeg.avcodec_parameters_from_context(_pStream->codecpar, _pCodecContext) < 0)
- throw new Exception("코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
-
- // ── 5단계: 출력 파일 I/O 열기 ──
- // AVFMT_NOFILE 플래그가 없는 포맷은 실제 파일 I/O가 필요합니다.
- // avio_open으로 파일을 쓰기 모드로 열고, 포맷 컨텍스트의 pb(I/O 컨텍스트)에 연결합니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__io.html
- if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
- {
- if (ffmpeg.avio_open(&_pFormatContext->pb, outputPath, ffmpeg.AVIO_FLAG_WRITE) < 0)
- throw new Exception($"출력 파일 {outputPath}를 열 수 없습니다.");
- }
-
- // 파일 헤더(ftyp, moov 등 컨테이너 메타데이터) 작성.
- // 이 시점 이후부터 av_interleaved_write_frame으로 패킷을 쓸 수 있습니다.
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html
- if (ffmpeg.avformat_write_header(_pFormatContext, null) < 0)
- throw new Exception("파일 헤더를 작성하는 데 실패했습니다.");
-
- // ── 6단계: 프레임 버퍼 및 색공간 변환기 초기화 ──
- // av_frame_alloc: AVFrame 구조체를 힙에 할당합니다 (픽셀 데이터는 아직 없음).
- // 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html
- _pSrcFrame = ffmpeg.av_frame_alloc();
- _pDstFrame = ffmpeg.av_frame_alloc();
- if (_pSrcFrame == null || _pDstFrame == null)
- throw new Exception("프레임 구조체를 할당하지 못했습니다.");
-
- // 목적지 프레임의 형식과 크기를 설정한 뒤 실제 픽셀 버퍼를 할당합니다.
- // av_frame_get_buffer: format, width, height에 맞는 메모리를 할당하고
- // data[]/linesize[] 포인터를 설정합니다. 두 번째 인자 0은 기본 정렬을 의미합니다.
- _pDstFrame->format = (int)_dstPixFmt;
- _pDstFrame->width = _settings.Width;
- _pDstFrame->height = _settings.Height;
-
- if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
- throw new Exception($"{_dstPixFmt} 프레임 버퍼를 할당하는 데 실패했습니다.");
-
- // sws_getContext: 소프트웨어 스케일러(색공간 변환기) 초기화.
- // 입력: RGBA (Unity 캡처 포맷, 4바이트/픽셀)
- // 출력: YUV420P 또는 NV12 (인코더 요구 포맷)
- //
- // 여기서는 해상도 변환 없이 색공간만 변환하지만, sws_scale은
- // 동시에 리사이즈도 수행할 수 있습니다.
- //
- // SWS_BILINEAR: 바이리니어 보간법. 색공간만 변환할 때는 품질 차이가 미미하며,
- // 속도와 품질의 균형이 좋은 기본 옵션입니다.
- //
- // 참고: https://ffmpeg.org/doxygen/trunk/group__libsws.html
- _pSwsContext = ffmpeg.sws_getContext(
- _settings.Width, _settings.Height, AVPixelFormat.AV_PIX_FMT_RGBA,
- _settings.Width, _settings.Height, _dstPixFmt,
- (int)SwsFlags.SWS_BILINEAR, null, null, null);
-
- if (_pSwsContext == null)
- throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다.");
-
- _isInitialized = true;
- }
- catch (Exception)
- {
- // 초기화 도중 실패 시 부분 할당된 리소스를 모두 정리합니다.
- Dispose();
- throw;
- }
}
}
@@ -700,7 +719,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// MaxBitrate가 TargetBitrate보다 큰 경우 그대로 사용,
// 그렇지 않으면 TargetBitrate의 1.5배를 최대값으로 설정합니다.
var maxRate = _settings.MaxBitrate > _settings.TargetBitrate
- ? _settings.MaxBitrate * 1000L : (long)(_settings.TargetBitrate * 1500L);
+ ? _settings.MaxBitrate * 1000L : _settings.TargetBitrate * 1500L;
_pCodecContext->rc_max_rate = maxRate;
// 버퍼 크기를 최대 비트레이트의 2배로 설정하여
@@ -869,35 +888,34 @@ namespace ADOFAIRenderer.Util.FFmpeg
// PCIe 대역폭 사용을 최소화할 수 있습니다.
//
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__hwcontext.html
- AVFrame* hwFrame = ffmpeg.av_frame_alloc();
+
+ ffmpeg.av_frame_unref(_pHwFrame);
try
{
- int ret = ffmpeg.av_hwframe_get_buffer(_pCodecContext->hw_frames_ctx, hwFrame, 0);
+ int ret = ffmpeg.av_hwframe_get_buffer(_pCodecContext->hw_frames_ctx, _pHwFrame, 0);
if (ret < 0)
throw new Exception($"QSV 하드웨어 프레임 버퍼 할당 실패 — {GetFFmpegErrorString(ret)} (Code: {ret})");
- ret = ffmpeg.av_hwframe_transfer_data(hwFrame, _pDstFrame, 0);
+ ret = ffmpeg.av_hwframe_transfer_data(_pHwFrame, _pDstFrame, 0);
if (ret < 0)
throw new Exception($"QSV 프레임 업로드 실패 — {GetFFmpegErrorString(ret)} (Code: {ret})");
// PTS(Presentation Time Stamp): 이 프레임이 표시되어야 하는 시점.
// _frameIndex를 1씩 증가시키며, 실제 시간은 PTS × time_base로 계산됩니다.
- hwFrame->pts = _frameIndex++;
- SendFrameAndWritePackets(hwFrame);
+ _pHwFrame->pts = _frameIndex++;
+ SendFrameAndWritePackets(_pHwFrame);
}
finally
{
- // 하드웨어 프레임의 참조를 해제합니다.
- // 프레임 풀에서 가져온 프레임이므로, 해제하면 풀로 반환됩니다.
- var pHw = hwFrame;
- ffmpeg.av_frame_free(&pHw);
+ // 사용이 끝났으므로 즉시 참조 해제하여 풀로 반환될 수 있게 합니다.
+ ffmpeg.av_frame_unref(_pHwFrame);
}
}
else
{
// ── 일반 인코더 (소프트웨어 또는 hw_device_ctx 모드) ──
// 소프트웨어 프레임을 직접 인코더에 전달합니다.
- // hw_device_ctx 모드인 경우, 인코더가 내부적으로 GPU 업로드를 처리합니다.
+ // hw_device_ctx 모인 경우, 인코더가 내부적으로 GPU 업로드를 처리합니다.
_pDstFrame->pts = _frameIndex++;
SendFrameAndWritePackets(_pDstFrame);
}
@@ -946,13 +964,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
}
// 인코딩 완료된 패킷을 모두 꺼내어 파일에 기록합니다.
- // av_packet_alloc: 패킷 구조체를 힙에 할당합니다.
- AVPacket* pPacket = ffmpeg.av_packet_alloc();
try
{
while (true)
{
- response = ffmpeg.avcodec_receive_packet(_pCodecContext, pPacket);
+ response = ffmpeg.avcodec_receive_packet(_pCodecContext, _pPacket);
// EAGAIN: 인코더가 더 많은 입력 프레임을 필요로 함 → 루프 종료
// EOF: 인코더 플러시 완료 (모든 지연 프레임 출력됨) → 루프 종료
@@ -966,26 +982,26 @@ namespace ADOFAIRenderer.Util.FFmpeg
}
// 코덱 타임베이스 → 스트림 타임베이스로 PTS/DTS 변환
- ffmpeg.av_packet_rescale_ts(pPacket, _pCodecContext->time_base, _pStream->time_base);
- pPacket->stream_index = _pStream->index;
+ ffmpeg.av_packet_rescale_ts(_pPacket, _pCodecContext->time_base, _pStream->time_base);
+ _pPacket->stream_index = _pStream->index;
// av_interleaved_write_frame: 패킷을 컨테이너에 기록합니다.
// "interleaved"는 여러 스트림(비디오+오디오)의 패킷을 시간 순서대로
// 인터리빙하여 쓴다는 의미입니다. 비디오만 있어도 이 함수를 사용합니다.
- response = ffmpeg.av_interleaved_write_frame(_pFormatContext, pPacket);
+ response = ffmpeg.av_interleaved_write_frame(_pFormatContext, _pPacket);
if (response < 0)
{
throw new Exception($"패킷을 파일에 쓰는 도중 에러가 발생했습니다. (Code: {response})");
}
// 패킷의 데이터 참조를 해제하여 다음 receive_packet에서 재사용할 수 있게 합니다.
- ffmpeg.av_packet_unref(pPacket);
+ ffmpeg.av_packet_unref(_pPacket);
}
}
finally
{
- // 패킷 구조체 자체를 힙에서 해제합니다.
- ffmpeg.av_packet_free(&pPacket);
+ // 비정상적인 탈출(예외 발생) 등에도 안전하게 패킷 참조 해제
+ ffmpeg.av_packet_unref(_pPacket);
}
}
@@ -1040,20 +1056,6 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 리소스 해제 (Dispose)
// ================================================================
- ///
- /// 모든 FFmpeg 네이티브 리소스를 해제합니다.
- /// IDisposable 패턴을 구현하여 using 문이나 수동 Dispose() 호출을 지원합니다.
- ///
- /// ⚠️ FFmpeg의 모든 리소스는 네이티브 메모리(힙)에 할당되므로
- /// C#의 GC가 자동으로 회수하지 않습니다. 반드시 명시적으로 해제해야 합니다.
- ///
- public void Dispose()
- {
- Dispose(true);
- // 파이널라이저가 이미 정리된 리소스를 다시 해제하지 않도록 방지합니다.
- GC.SuppressFinalize(this);
- }
-
///
/// 실제 리소스 해제 로직. 해제 순서가 중요합니다:
///
@@ -1069,7 +1071,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// avformat_free_context: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
/// avio_closep: https://ffmpeg.org/doxygen/trunk/group__lavf__io.html
///
- private void Dispose(bool disposing)
+ public void Dispose()
{
lock (_lock)
{
@@ -1105,6 +1107,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
_pDstFrame = null;
}
+ // QSV 하드웨어 프레임 메모리 해제
+ if (_pHwFrame != null)
+ {
+ var pFrame = _pHwFrame;
+ ffmpeg.av_frame_free(&pFrame);
+ _pHwFrame = null;
+ }
+
+ // 패킷 메모리 해제
+ if (_pPacket != null)
+ {
+ var pPacket = _pPacket;
+ ffmpeg.av_packet_free(&pPacket);
+ _pPacket = null;
+ }
+
// 코덱 컨텍스트 해제
// 이 함수는 내부적으로 hw_frames_ctx와 hw_device_ctx도 함께 해제합니다.
if (_pCodecContext != null)
@@ -1132,6 +1150,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
ffmpeg.avformat_free_context(_pFormatContext);
_pFormatContext = null;
}
+
+ GC.SuppressFinalize(this);
}
}
@@ -1141,9 +1161,6 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// ⚠️ 파이널라이저는 실행 시점이 보장되지 않으므로, 반드시 using 문이나
/// 명시적 Dispose() 호출로 리소스를 관리하세요.
///
- ~FFmpegNativeEncoder()
- {
- Dispose(false);
- }
+ ~FFmpegNativeEncoder() { Dispose(); }
}
}
\ No newline at end of file
diff --git a/Util/GPUUtil.cs b/Util/GPUUtil.cs
new file mode 100644
index 0000000..dec0bb4
--- /dev/null
+++ b/Util/GPUUtil.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using ADOFAIRenderer.Util.FFmpeg;
+using MelonLoader;
+
+namespace ADOFAIRenderer.Util
+{
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ public enum GPUVendor
+ {
+ Unknown, NVIDIA, AMD, Intel
+ }
+
+ public static class GPUUtil
+ {
+ private const string NvidiaPattern =
+ @"^NVIDIA GeForce [RG]TX [0-9]+(?>(?> Ti)|(?> SUPER)|(?> [0-9]+GB))?(?> Laptop GPU)?/PCIe/SSE2$";
+
+ private const string IntelPattern =
+ @"^Intel(?>\(R\))* (?>U*HD Graphics [0-9]+$|Arc\(TM\) [A-C][0-9]+ Graphics)";
+
+ private static Dictionary> _codecMap = new Dictionary>
+ {
+ { Codec.H264, new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "h264_nvenc"), (GPUVendor.Intel, "h264_qsv"), (GPUVendor.AMD, "h264_amf"), (GPUVendor.Unknown, "libx264") } },
+ { Codec.H265, new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "hevc_nvenc"), (GPUVendor.Intel, "hevc_qsv"), (GPUVendor.AMD, "hevc_amf"), (GPUVendor.Unknown, "libx265") } },
+ { Codec.VP9, new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "vp9_nvenc"), (GPUVendor.Intel, "vp9_qsv"), (GPUVendor.Unknown, "libvpx-vp9") } },
+ { Codec.AV1, new List<(GPUVendor, string)> { (GPUVendor.NVIDIA, "av1_nvenc"), (GPUVendor.Intel, "av1_qsv"), (GPUVendor.AMD, "av1_amf"), (GPUVendor.Unknown, "libsvtav1"), (GPUVendor.Unknown, "libaom-av1") } },
+ { Codec.VVC, new List<(GPUVendor, string)> { (GPUVendor.Unknown, "libvvenc") }}
+ };
+
+ public static GPUVendor GetActiveGPUVendor()
+ {
+ var gpuName = UnityEngine.SystemInfo.graphicsDeviceName;
+ Melon.Logger.Msg($"Active GPU: {gpuName}");
+
+ foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern))
+ {
+ return GPUVendor.NVIDIA;
+ }
+
+ foreach (Match unused in Regex.Matches(gpuName, IntelPattern))
+ {
+ return GPUVendor.Intel;
+ }
+
+ return GPUVendor.Unknown;
+ }
+
+ public static List GetPrioritizedEncoderCandidates(Codec codec)
+ {
+ if (!_codecMap.TryGetValue(codec, out var list)) return new List();
+
+ var vendor = GetActiveGPUVendor();
+ var prioritized = new List();
+ var others = new List();
+
+ foreach (var item in list)
+ {
+ if (item.Item1 == vendor) prioritized.Add(item.Item2);
+ else others.Add(item.Item2);
+ }
+
+ prioritized.AddRange(others);
+ return prioritized;
+ }
+ }
+}
\ No newline at end of file