diff --git a/Bundle/Linux/shaders b/Bundle/Linux/shaders
index 899ccba..8955a15 100644
Binary files a/Bundle/Linux/shaders and b/Bundle/Linux/shaders differ
diff --git a/Bundle/Win64/shaders b/Bundle/Win64/shaders
index 2667963..bb65621 100644
Binary files a/Bundle/Win64/shaders and b/Bundle/Win64/shaders differ
diff --git a/Util/FFmpeg/FFmpegVideoDecoder.cs b/Util/FFmpeg/FFmpegNativeDecoder.cs
similarity index 67%
rename from Util/FFmpeg/FFmpegVideoDecoder.cs
rename to Util/FFmpeg/FFmpegNativeDecoder.cs
index efa699e..0413b9d 100644
--- a/Util/FFmpeg/FFmpegVideoDecoder.cs
+++ b/Util/FFmpeg/FFmpegNativeDecoder.cs
@@ -1,6 +1,5 @@
using System;
using FFmpeg.AutoGen;
-using MelonLoader;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
@@ -10,7 +9,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// FFmpeg의 C 네이티브 API를 사용하여 비디오 파일을 스트리밍 방식으로 디코딩하는 클래스입니다.
/// 플레이어 형태가 아니며, 필요할 때 프레임을 순차적으로 읽거나 앞으로 탐색(Seek/Skip)할 수 있습니다.
///
- public sealed unsafe class FFmpegVideoDecoder : IDisposable
+ public sealed unsafe class FFmpegNativeDecoder : IDisposable
{
private AVFormatContext* _pFormatContext = null;
private AVCodecContext* _pCodecContext = null;
@@ -33,6 +32,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// SwsContext 갱신 감지용 필드
private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
+ private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private int _srcWidth;
private int _srcHeight;
@@ -57,7 +57,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 현재 재생 시간 (초).
public double CurrentTime => _currentFrameIndex / FPS;
- public FFmpegVideoDecoder(string videoPath)
+ /// 비디오 원본 코덱의 픽셀 포맷.
+ public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE;
+
+ public FFmpegNativeDecoder(string videoPath)
{
Open(videoPath);
}
@@ -192,9 +195,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
}
}
- private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight)
+ private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight, AVPixelFormat dstFormat)
{
- if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight)
+ if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight && _dstPixFmt == dstFormat)
{
return;
}
@@ -208,11 +211,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
_srcPixFmt = srcFormat;
_srcWidth = srcWidth;
_srcHeight = srcHeight;
+ _dstPixFmt = dstFormat;
- // Unity 표준인 RGBA32(AV_PIX_FMT_RGBA) 포맷으로 변환하도록 설정
_pSwsContext = ffmpeg.sws_getContext(
srcWidth, srcHeight, srcFormat,
- Width, Height, AVPixelFormat.AV_PIX_FMT_RGBA,
+ Width, Height, dstFormat,
(int)SwsFlags.SWS_BILINEAR, null, null, null
);
@@ -262,7 +265,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (_hasCachedFrame)
{
timestamp = GetTimestampAndUpdateIndex(_pCachedFrame);
- InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pCachedFrame->width, _pCachedFrame->height);
+ InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pCachedFrame->width, _pCachedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA);
if (vflip)
{
@@ -293,7 +296,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (ret == 0)
{
timestamp = GetTimestampAndUpdateIndex(_pDecodedFrame);
- InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pDecodedFrame->width, _pDecodedFrame->height);
+ InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pDecodedFrame->width, _pDecodedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA);
if (vflip)
{
@@ -354,6 +357,277 @@ namespace ADOFAIRenderer.Util.FFmpeg
return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp);
}
+ ///
+ /// 다음 비디오 프레임을 읽어 Y, U, V 채널 데이터를 각각 복사합니다.
+ /// 비디오 원본이 YUV420p 계열(YUV420P, YUVJ420P)이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
+ /// 그 외 포맷일 경우 자동으로 YUV420p로 변환 후 복사합니다.
+ ///
+ public bool TryReadYUV420PFrame(
+ byte* yBuffer, int yStride,
+ byte* uBuffer, int uStride,
+ byte* vBuffer, int vStride,
+ out double timestamp)
+ {
+ timestamp = 0;
+ lock (_lock)
+ {
+ if (!_isOpened || _disposed) return false;
+
+ AVFrame* frame = null;
+
+ if (_hasCachedFrame)
+ {
+ frame = _pCachedFrame;
+ _hasCachedFrame = false;
+ }
+ else
+ {
+ while (true)
+ {
+ int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
+ if (ret == 0)
+ {
+ frame = _pDecodedFrame;
+ break;
+ }
+
+ if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
+ {
+ if (_isDraining) return false;
+
+ if (!ReadAndSendPacket())
+ {
+ _isDraining = true;
+ ffmpeg.avcodec_send_packet(_pCodecContext, null);
+ }
+ }
+ else if (ret == ffmpeg.AVERROR_EOF)
+ {
+ return false;
+ }
+ else
+ {
+ throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
+ }
+ }
+ }
+
+ if (frame == null) return false;
+
+ timestamp = GetTimestampAndUpdateIndex(frame);
+
+ if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUV420P && frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUVJ420P)
+ {
+ InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_YUV420P);
+
+ _pConvertedFrame->data[0] = yBuffer;
+ _pConvertedFrame->data[1] = uBuffer;
+ _pConvertedFrame->data[2] = vBuffer;
+ _pConvertedFrame->linesize[0] = yStride;
+ _pConvertedFrame->linesize[1] = uStride;
+ _pConvertedFrame->linesize[2] = vStride;
+
+ ffmpeg.sws_scale(
+ _pSwsContext,
+ frame->data, frame->linesize,
+ 0, frame->height,
+ _pConvertedFrame->data, _pConvertedFrame->linesize
+ );
+ }
+ else
+ {
+ int w = frame->width;
+ int h = frame->height;
+ int uvW = w / 2;
+ int uvH = h / 2;
+
+ // Y plane 복사
+ for (int y = 0; y < h; y++)
+ {
+ Buffer.MemoryCopy(
+ frame->data[0] + y * frame->linesize[0],
+ yBuffer + y * yStride,
+ w, w
+ );
+ }
+
+ // U plane 복사
+ for (int y = 0; y < uvH; y++)
+ {
+ Buffer.MemoryCopy(
+ frame->data[1] + y * frame->linesize[1],
+ uBuffer + y * uStride,
+ uvW, uvW
+ );
+ }
+
+ // V plane 복사
+ for (int y = 0; y < uvH; y++)
+ {
+ Buffer.MemoryCopy(
+ frame->data[2] + y * frame->linesize[2],
+ vBuffer + y * vStride,
+ uvW, uvW
+ );
+ }
+ }
+
+ ffmpeg.av_frame_unref(frame);
+ return true;
+ }
+ }
+
+ public bool TryReadYUV420PFrame(
+ NativeArray yBuffer,
+ NativeArray uBuffer,
+ NativeArray vBuffer,
+ out double timestamp)
+ {
+ int w = Width;
+ int h = Height;
+ int uvW = w / 2;
+ int uvH = h / 2;
+
+ if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH)
+ {
+ throw new ArgumentException("버퍼 크기가 비디오 YUV 평면 해상도보다 작습니다.");
+ }
+
+ return TryReadYUV420PFrame(
+ (byte*)yBuffer.GetUnsafePtr(), w,
+ (byte*)uBuffer.GetUnsafePtr(), uvW,
+ (byte*)vBuffer.GetUnsafePtr(), uvW,
+ out timestamp
+ );
+ }
+
+ ///
+ /// 다음 비디오 프레임을 읽어 NV12 형식의 Y, UV 채널 데이터를 각각 복사합니다.
+ /// 비디오 원본이 NV12 계열이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
+ /// 그 외 포맷일 경우 자동으로 NV12로 변환 후 복사합니다.
+ ///
+ public bool TryReadNV12Frame(
+ byte* yBuffer, int yStride,
+ byte* uvBuffer, int uvStride,
+ out double timestamp)
+ {
+ timestamp = 0;
+ lock (_lock)
+ {
+ if (!_isOpened || _disposed) return false;
+
+ AVFrame* frame = null;
+
+ if (_hasCachedFrame)
+ {
+ frame = _pCachedFrame;
+ _hasCachedFrame = false;
+ }
+ else
+ {
+ while (true)
+ {
+ int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
+ if (ret == 0)
+ {
+ frame = _pDecodedFrame;
+ break;
+ }
+
+ if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
+ {
+ if (_isDraining) return false;
+
+ if (!ReadAndSendPacket())
+ {
+ _isDraining = true;
+ ffmpeg.avcodec_send_packet(_pCodecContext, null);
+ }
+ }
+ else if (ret == ffmpeg.AVERROR_EOF)
+ {
+ return false;
+ }
+ else
+ {
+ throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
+ }
+ }
+ }
+
+ if (frame == null) return false;
+
+ timestamp = GetTimestampAndUpdateIndex(frame);
+
+ if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_NV12)
+ {
+ InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_NV12);
+
+ _pConvertedFrame->data[0] = yBuffer;
+ _pConvertedFrame->data[1] = uvBuffer;
+ _pConvertedFrame->linesize[0] = yStride;
+ _pConvertedFrame->linesize[1] = uvStride;
+
+ ffmpeg.sws_scale(
+ _pSwsContext,
+ frame->data, frame->linesize,
+ 0, frame->height,
+ _pConvertedFrame->data, _pConvertedFrame->linesize
+ );
+ }
+ else
+ {
+ int w = frame->width;
+ int h = frame->height;
+ int uvH = h / 2;
+
+ // Y plane 복사
+ for (int y = 0; y < h; y++)
+ {
+ Buffer.MemoryCopy(
+ frame->data[0] + y * frame->linesize[0],
+ yBuffer + y * yStride,
+ w, w
+ );
+ }
+
+ // UV plane 복사
+ for (int y = 0; y < uvH; y++)
+ {
+ Buffer.MemoryCopy(
+ frame->data[1] + y * frame->linesize[1],
+ uvBuffer + y * uvStride,
+ w, w
+ );
+ }
+ }
+
+ ffmpeg.av_frame_unref(frame);
+ return true;
+ }
+ }
+
+ public bool TryReadNV12Frame(
+ NativeArray yBuffer,
+ NativeArray uvBuffer,
+ out double timestamp)
+ {
+ int w = Width;
+ int h = Height;
+ int uvH = h / 2;
+
+ if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH)
+ {
+ throw new ArgumentException("버퍼 크기가 비디오 NV12 평면 해상도보다 작습니다.");
+ }
+
+ return TryReadNV12Frame(
+ (byte*)yBuffer.GetUnsafePtr(), w,
+ (byte*)uvBuffer.GetUnsafePtr(), w,
+ out timestamp
+ );
+ }
+
///
/// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용)
///
@@ -574,7 +848,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
}
}
- ~FFmpegVideoDecoder()
+ ~FFmpegNativeDecoder()
{
Dispose();
}
diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs
index d846ae6..c7c3477 100644
--- a/Util/FFmpeg/FFmpegNativeEncoder.cs
+++ b/Util/FFmpeg/FFmpegNativeEncoder.cs
@@ -438,19 +438,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
var qsvReady = false;
int err;
- // ── 전략 1: child_device_type=d3d11va 옵션 ──
+ bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
+ string childDeviceName = isWindows ? "d3d11va" : "vaapi";
+ AVHWDeviceType childDeviceType = isWindows ? AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA : AVHWDeviceType.AV_HWDEVICE_TYPE_VAAPI;
+
+ // ── 전략 1: child_device_type 옵션 ──
// av_hwdevice_ctx_create에 "child_device_type" 딕셔너리를 전달하면
- // FFmpeg이 내부적으로 D3D11 디바이스를 생성하고 QSV 세션에 연결합니다.
- // 대부분의 최신 Intel 드라이버에서 가장 안정적으로 동작합니다.
+ // FFmpeg이 내부적으로 적절한 하위 디바이스(Windows: D3D11, Linux: VAAPI)를 생성하고 QSV 세션에 연결합니다.
{
AVDictionary* opts = null;
- ffmpeg.av_dict_set(&opts, "child_device_type", "d3d11va", 0);
+ ffmpeg.av_dict_set(&opts, "child_device_type", childDeviceName, 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 디바이스 생성 성공");
+ Melon.Logger.Msg($"{encoderName}: child_device_type={childDeviceName} 방식으로 QSV 디바이스 생성 성공");
}
else
{
@@ -458,24 +461,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
}
}
- // ── 전략 2: D3D11VA → QSV 파생(derive) ──
- // D3D11VA 디바이스를 먼저 생성한 뒤,
+ // ── 전략 2: 하위 디바이스 → QSV 파생(derive) ──
+ // 하위 디바이스(D3D11VA / VAAPI)를 먼저 생성한 뒤,
// av_hwdevice_ctx_create_derived로 QSV 디바이스를 파생시킵니다.
- // 두 디바이스가 동일한 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);
+ AVBufferRef* childDeviceCtx = null;
+ err = ffmpeg.av_hwdevice_ctx_create(&childDeviceCtx, childDeviceType, null, null, 0);
if (err >= 0)
{
- err = ffmpeg.av_hwdevice_ctx_create_derived(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, d3d11DeviceCtx, 0);
+ err = ffmpeg.av_hwdevice_ctx_create_derived(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, childDeviceCtx, 0);
if (err >= 0)
{
qsvReady = true;
- Melon.Logger.Msg($"{encoderName}: D3D11VA → QSV 파생 방식으로 디바이스 생성 성공");
+ Melon.Logger.Msg($"{encoderName}: {childDeviceType} → QSV 파생 방식으로 디바이스 생성 성공");
}
- ffmpeg.av_buffer_unref(&d3d11DeviceCtx);
+ ffmpeg.av_buffer_unref(&childDeviceCtx);
}
}