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); } }