Add GPU Detection

This commit is contained in:
copecone committed 2026-06-08 22:46:32 +09:00
1 parent 32dccd4298
commit c8755eaca7
3 files changed
+136 -51

No files matched your search

+1 -1
View File
@@ -5,6 +5,6 @@ namespace ADOFAIRenderer.Util.FFmpeg
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum Codec
{
H264, H265, AV1, VP9
H264, H265, VP9, AV1, VVC
}
}
+67 -50
View File
@@ -152,6 +152,18 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// </summary>
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
/// <summary>
/// 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체.
/// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
/// </summary>
private AVPacket* _pPacket = null;
/// <summary>
/// QSV 하드웨어 가속 모드일 때 사용할 GPU 프레임 구조체.
/// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
/// </summary>
private AVFrame* _pHwFrame = null;
/// <summary>생성 시 전달받은 인코딩 설정값 (불변).</summary>
private readonly EncoderSettings _settings;
@@ -170,6 +182,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 실제 인코더 구현체(예: libx264, h264_nvenc)는 별도로 선택됩니다.
/// </summary>
// 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
/// </summary>
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<string>()
};
return GPUUtil.GetPrioritizedEncoderCandidates(_settings.Codec).ToArray();
}
/// <summary>
@@ -291,12 +298,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
// ── 기본 비디오 파라미터 설정 ──
_pCodecContext->width = _settings.Width;
_pCodecContext->height = _settings.Height;
_pCodecContext->thread_count = 0; // 멀티스레딩 활성화 (0 = 자동 설정)
// ── 픽셀 포맷 설정 ──
// 비디오 인코더는 일반적으로 YUV420P 또는 NV12 형식의 입력을 기대합니다.
//
// • YUV420P: Y/U/V 3개의 독립 평면(plane). 대부분의 소프트웨어 인코더가 사용.
// • NV12: Y 평면 + UV 인터리브 평면. Intel QSV 등 하드웨어 인코더가 선호.
// • NV12: Y 평면 + UV 인터리브 평면. 하드웨어 인코더(NVENC, AMF, QSV)가 선호.
// • QSV 특수 사항: 코덱 컨텍스트의 pix_fmt은 AV_PIX_FMT_QSV(하드웨어 서피스 식별자)로,
// 실제 sws_scale 출력은 NV12(소프트웨어 포맷)로 설정합니다.
// FFmpeg이 내부적으로 NV12 ↔ QSV 서피스 간 매핑을 처리합니다.
@@ -308,6 +316,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
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;
@@ -420,6 +433,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 두 디바이스가 동일한 GPU를 공유하게 됩니다.
if (!qsvReady)
{
// 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)
@@ -486,6 +500,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
_isQsvEncoder = true;
framesCtxReady = true;
Melon<RenderMod>.Logger.Msg($"{encoderName}: hw_frames_ctx 초기화 성공 (GPU 프레임 풀 모드)");
_pHwFrame = ffmpeg.av_frame_alloc();
if (_pHwFrame == null)
throw new Exception("QSV 하드웨어 프레임 구조체를 할당하지 못했습니다.");
}
else
{
@@ -602,8 +620,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 참고: 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("프레임 구조체를 할당하지 못했습니다.");
_pPacket = ffmpeg.av_packet_alloc();
if (_pSrcFrame == null || _pDstFrame == null || _pPacket == null)
throw new Exception("프레임 또는 패킷 구조체를 할당하지 못했습니다.");
// 목적지 프레임의 형식과 크기를 설정한 뒤 실제 픽셀 버퍼를 할당합니다.
// av_frame_get_buffer: format, width, height에 맞는 메모리를 할당하고
@@ -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)
// ================================================================
/// <summary>
/// 모든 FFmpeg 네이티브 리소스를 해제합니다.
/// IDisposable 패턴을 구현하여 using 문이나 수동 Dispose() 호출을 지원합니다.
///
/// ⚠️ FFmpeg의 모든 리소스는 네이티브 메모리(힙)에 할당되므로
/// C#의 GC가 자동으로 회수하지 않습니다. 반드시 명시적으로 해제해야 합니다.
/// </summary>
public void Dispose()
{
Dispose(true);
// 파이널라이저가 이미 정리된 리소스를 다시 해제하지 않도록 방지합니다.
GC.SuppressFinalize(this);
}
/// <summary>
/// 실제 리소스 해제 로직. 해제 순서가 중요합니다:
///
@@ -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
/// </summary>
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() 호출로 리소스를 관리하세요.
/// </summary>
~FFmpegNativeEncoder()
{
Dispose(false);
}
~FFmpegNativeEncoder() { Dispose(); }
}
}
+68
View File
@@ -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<Codec, List<(GPUVendor, string)>> _codecMap = new Dictionary<Codec, List<(GPUVendor, string)>>
{
{ 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<RenderMod>.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<string> GetPrioritizedEncoderCandidates(Codec codec)
{
if (!_codecMap.TryGetValue(codec, out var list)) return new List<string>();
var vendor = GetActiveGPUVendor();
var prioritized = new List<string>();
var others = new List<string>();
foreach (var item in list)
{
if (item.Item1 == vendor) prioritized.Add(item.Item2);
else others.Add(item.Item2);
}
prioritized.AddRange(others);
return prioritized;
}
}
}