Update Color Conversion Shader

This commit is contained in:
copecone committed 2026-06-20 17:05:05 +09:00
1 parent 6a96434983
commit 9bb2b85010
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 System;
using FFmpeg.AutoGen; using FFmpeg.AutoGen;
using MelonLoader;
using Unity.Collections; using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe; using Unity.Collections.LowLevel.Unsafe;
@@ -10,7 +9,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// FFmpeg의 C 네이티브 API를 사용하여 비디오 파일을 스트리밍 방식으로 디코딩하는 클래스입니다. /// FFmpeg의 C 네이티브 API를 사용하여 비디오 파일을 스트리밍 방식으로 디코딩하는 클래스입니다.
/// 플레이어 형태가 아니며, 필요할 때 프레임을 순차적으로 읽거나 앞으로 탐색(Seek/Skip)할 수 있습니다. /// 플레이어 형태가 아니며, 필요할 때 프레임을 순차적으로 읽거나 앞으로 탐색(Seek/Skip)할 수 있습니다.
/// </summary> /// </summary>
public sealed unsafe class FFmpegVideoDecoder : IDisposable public sealed unsafe class FFmpegNativeDecoder : IDisposable
{ {
private AVFormatContext* _pFormatContext = null; private AVFormatContext* _pFormatContext = null;
private AVCodecContext* _pCodecContext = null; private AVCodecContext* _pCodecContext = null;
@@ -33,6 +32,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
// SwsContext 갱신 감지용 필드 // SwsContext 갱신 감지용 필드
private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE; private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private int _srcWidth; private int _srcWidth;
private int _srcHeight; private int _srcHeight;
@@ -57,7 +57,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// <summary>현재 재생 시간 (초).</summary> /// <summary>현재 재생 시간 (초).</summary>
public double CurrentTime => _currentFrameIndex / FPS; 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); 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; return;
} }
@@ -208,11 +211,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
_srcPixFmt = srcFormat; _srcPixFmt = srcFormat;
_srcWidth = srcWidth; _srcWidth = srcWidth;
_srcHeight = srcHeight; _srcHeight = srcHeight;
_dstPixFmt = dstFormat;
// Unity 표준인 RGBA32(AV_PIX_FMT_RGBA) 포맷으로 변환하도록 설정
_pSwsContext = ffmpeg.sws_getContext( _pSwsContext = ffmpeg.sws_getContext(
srcWidth, srcHeight, srcFormat, srcWidth, srcHeight, srcFormat,
Width, Height, AVPixelFormat.AV_PIX_FMT_RGBA, Width, Height, dstFormat,
(int)SwsFlags.SWS_BILINEAR, null, null, null (int)SwsFlags.SWS_BILINEAR, null, null, null
); );
@@ -262,7 +265,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (_hasCachedFrame) if (_hasCachedFrame)
{ {
timestamp = GetTimestampAndUpdateIndex(_pCachedFrame); 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) if (vflip)
{ {
@@ -293,7 +296,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
if (ret == 0) if (ret == 0)
{ {
timestamp = GetTimestampAndUpdateIndex(_pDecodedFrame); 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) if (vflip)
{ {
@@ -354,6 +357,277 @@ namespace ADOFAIRenderer.Util.FFmpeg
return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp); 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>
/// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용) /// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용)
/// </summary> /// </summary>
@@ -574,7 +848,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
} }
~FFmpegVideoDecoder() ~FFmpegNativeDecoder()
{ {
Dispose(); Dispose();
} }
+15 -14
View File
@@ -438,19 +438,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
var qsvReady = false; var qsvReady = false;
int err; 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" 딕셔너리를 전달하면 // av_hwdevice_ctx_create에 "child_device_type" 딕셔너리를 전달하면
// FFmpeg이 내부적으로 D3D11 디바이스를 생성하고 QSV 세션에 연결합니다. // FFmpeg이 내부적으로 적절한 하위 디바이스(Windows: D3D11, Linux: VAAPI)를 생성하고 QSV 세션에 연결합니다.
// 대부분의 최신 Intel 드라이버에서 가장 안정적으로 동작합니다.
{ {
AVDictionary* opts = null; 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); err = ffmpeg.av_hwdevice_ctx_create(&hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_QSV, null, opts, 0);
ffmpeg.av_dict_free(&opts); ffmpeg.av_dict_free(&opts);
if (err >= 0) if (err >= 0)
{ {
qsvReady = true; qsvReady = true;
Melon<RenderMod>.Logger.Msg($"{encoderName}: child_device_type=d3d11va 방식으로 QSV 디바이스 생성 성공"); Melon<RenderMod>.Logger.Msg($"{encoderName}: child_device_type={childDeviceName} 방식으로 QSV 디바이스 생성 성공");
} }
else else
{ {
@@ -458,24 +461,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
} }
// ── 전략 2: D3D11VA → QSV 파생(derive) ── // ── 전략 2: 하위 디바이스 → QSV 파생(derive) ──
// D3D11VA 디바이스를 먼저 생성한 뒤, // 하위 디바이스(D3D11VA / VAAPI)를 먼저 생성한 뒤,
// av_hwdevice_ctx_create_derived로 QSV 디바이스를 파생시킵니다. // av_hwdevice_ctx_create_derived로 QSV 디바이스를 파생시킵니다.
// 두 디바이스가 동일한 GPU를 공유하게 됩니다.
if (!qsvReady) if (!qsvReady)
{ {
// ReSharper disable once InconsistentNaming AVBufferRef* childDeviceCtx = null;
AVBufferRef* d3d11DeviceCtx = null; err = ffmpeg.av_hwdevice_ctx_create(&childDeviceCtx, childDeviceType, null, null, 0);
err = ffmpeg.av_hwdevice_ctx_create(&d3d11DeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA, null, null, 0);
if (err >= 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) if (err >= 0)
{ {
qsvReady = true; 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);
} }
} }