SHA256
Update Color Conversion Shader
This commit is contained in:
4 files changed
+299
-24
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -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)할 수 있습니다.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>현재 재생 시간 (초).</summary>
|
||||
public double CurrentTime => _currentFrameIndex / FPS;
|
||||
|
||||
public FFmpegVideoDecoder(string videoPath)
|
||||
/// <summary>비디오 원본 코덱의 픽셀 포맷.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 비디오 프레임을 읽어 Y, U, V 채널 데이터를 각각 복사합니다.
|
||||
/// 비디오 원본이 YUV420p 계열(YUV420P, YUVJ420P)이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
|
||||
/// 그 외 포맷일 경우 자동으로 YUV420p로 변환 후 복사합니다.
|
||||
/// </summary>
|
||||
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<byte> yBuffer,
|
||||
NativeArray<byte> uBuffer,
|
||||
NativeArray<byte> 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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 비디오 프레임을 읽어 NV12 형식의 Y, UV 채널 데이터를 각각 복사합니다.
|
||||
/// 비디오 원본이 NV12 계열이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
|
||||
/// 그 외 포맷일 경우 자동으로 NV12로 변환 후 복사합니다.
|
||||
/// </summary>
|
||||
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<byte> yBuffer,
|
||||
NativeArray<byte> 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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용)
|
||||
/// </summary>
|
||||
@@ -574,7 +848,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
}
|
||||
|
||||
~FFmpegVideoDecoder()
|
||||
~FFmpegNativeDecoder()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
@@ -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<RenderMod>.Logger.Msg($"{encoderName}: child_device_type=d3d11va 방식으로 QSV 디바이스 생성 성공");
|
||||
Melon<RenderMod>.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<RenderMod>.Logger.Msg($"{encoderName}: D3D11VA → QSV 파생 방식으로 디바이스 생성 성공");
|
||||
Melon<RenderMod>.Logger.Msg($"{encoderName}: {childDeviceType} → QSV 파생 방식으로 디바이스 생성 성공");
|
||||
}
|
||||
ffmpeg.av_buffer_unref(&d3d11DeviceCtx);
|
||||
ffmpeg.av_buffer_unref(&childDeviceCtx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user