SHA256
Add Compute Shader Color Conversion
This commit is contained in:
7 files changed
+368
-31
No files matched your search
@@ -78,6 +78,8 @@
|
|||||||
<EmbeddedResource Include="Bundle\Win64\fonts" />
|
<EmbeddedResource Include="Bundle\Win64\fonts" />
|
||||||
<None Remove="Bundle\Win64\scenes" />
|
<None Remove="Bundle\Win64\scenes" />
|
||||||
<EmbeddedResource Include="Bundle\Win64\scenes" />
|
<EmbeddedResource Include="Bundle\Win64\scenes" />
|
||||||
|
<None Remove="Bundle\Win64\shaders" />
|
||||||
|
<EmbeddedResource Include="Bundle\Win64\shaders" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -85,6 +87,8 @@
|
|||||||
<EmbeddedResource Include="Bundle\Linux\fonts" />
|
<EmbeddedResource Include="Bundle\Linux\fonts" />
|
||||||
<None Remove="Bundle\Linux\scenes" />
|
<None Remove="Bundle\Linux\scenes" />
|
||||||
<EmbeddedResource Include="Bundle\Linux\scenes" />
|
<EmbeddedResource Include="Bundle\Linux\scenes" />
|
||||||
|
<None Remove="Bundle\Linux\shaders" />
|
||||||
|
<EmbeddedResource Include="Bundle\Linux\shaders" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -17,6 +17,8 @@ namespace ADOFAIRenderer
|
|||||||
public static AssetBundle FontBundle = null!;
|
public static AssetBundle FontBundle = null!;
|
||||||
// ReSharper disable once MemberCanBePrivate.Global
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
public static AssetBundle SceneBundle = null!;
|
public static AssetBundle SceneBundle = null!;
|
||||||
|
public static AssetBundle ShaderBundle = null!;
|
||||||
|
public static ComputeShader ColorConversionShader = null!;
|
||||||
public static string[] ModScenes = {};
|
public static string[] ModScenes = {};
|
||||||
|
|
||||||
public override void OnInitializeMelon()
|
public override void OnInitializeMelon()
|
||||||
@@ -38,6 +40,16 @@ namespace ADOFAIRenderer
|
|||||||
FontBundle = BundleLoader.GetBundle("fonts");
|
FontBundle = BundleLoader.GetBundle("fonts");
|
||||||
SceneBundle = BundleLoader.GetBundle("scenes");
|
SceneBundle = BundleLoader.GetBundle("scenes");
|
||||||
ModScenes = SceneBundle.GetAllScenePaths();
|
ModScenes = SceneBundle.GetAllScenePaths();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ShaderBundle = BundleLoader.GetBundle("shaders");
|
||||||
|
ColorConversionShader = ShaderBundle.LoadAsset<ComputeShader>("ColorConversion");
|
||||||
|
Melon<RenderMod>.Logger.Msg("컬러 변환 Compute Shader 로드 완료.");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Warning($"Shader 에셋번들을 로드할 수 없습니다. GPU 컬러 변환이 비활성화되며 CPU로 폴백합니다. 에러: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void FixEventSystem()
|
public static void FixEventSystem()
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
|
/// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private AVFrame* _pHwFrame = null;
|
private AVFrame* _pHwFrame = null;
|
||||||
|
private bool _loggedConversionMode;
|
||||||
|
|
||||||
/// <summary>생성 시 전달받은 인코딩 설정값 (불변).</summary>
|
/// <summary>생성 시 전달받은 인코딩 설정값 (불변).</summary>
|
||||||
private readonly EncoderSettings _settings;
|
private readonly EncoderSettings _settings;
|
||||||
@@ -303,6 +304,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
// ── 기본 비디오 파라미터 설정 ──
|
// ── 기본 비디오 파라미터 설정 ──
|
||||||
_pCodecContext->width = _settings.Width;
|
_pCodecContext->width = _settings.Width;
|
||||||
_pCodecContext->height = _settings.Height;
|
_pCodecContext->height = _settings.Height;
|
||||||
|
_pCodecContext->color_range = AVColorRange.AVCOL_RANGE_JPEG;
|
||||||
// thread_count = 0: FFmpeg이 시스템 CPU 코어 수에 맞춰 자동으로 스레드를 할당합니다.
|
// thread_count = 0: FFmpeg이 시스템 CPU 코어 수에 맞춰 자동으로 스레드를 할당합니다.
|
||||||
// libvvenc의 경우에도 FFmpeg 래퍼가 thread_count를 VVenC 내부 스레드 수로 전달하므로,
|
// libvvenc의 경우에도 FFmpeg 래퍼가 thread_count를 VVenC 내부 스레드 수로 전달하므로,
|
||||||
// 0(자동)을 설정해야 모든 코어를 활용한 병렬 인코딩이 가능합니다.
|
// 0(자동)을 설정해야 모든 코어를 활용한 병렬 인코딩이 가능합니다.
|
||||||
@@ -665,6 +667,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
_pDstFrame->format = (int)_dstPixFmt;
|
_pDstFrame->format = (int)_dstPixFmt;
|
||||||
_pDstFrame->width = _settings.Width;
|
_pDstFrame->width = _settings.Width;
|
||||||
_pDstFrame->height = _settings.Height;
|
_pDstFrame->height = _settings.Height;
|
||||||
|
_pDstFrame->color_range = AVColorRange.AVCOL_RANGE_JPEG;
|
||||||
|
|
||||||
if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
|
if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0)
|
||||||
throw new Exception($"{_dstPixFmt} 프레임 버퍼를 할당하는 데 실패했습니다.");
|
throw new Exception($"{_dstPixFmt} 프레임 버퍼를 할당하는 데 실패했습니다.");
|
||||||
@@ -689,6 +692,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다.");
|
throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다.");
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
|
_loggedConversionMode = false;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -873,7 +877,24 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_disposed || !_isInitialized) return;
|
if (_disposed || !_isInitialized) return;
|
||||||
WriteFrameFromBuffer((byte*)pointer.ToPointer());
|
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
|
||||||
|
|
||||||
|
if (!_loggedConversionMode)
|
||||||
|
{
|
||||||
|
_loggedConversionMode = true;
|
||||||
|
Melon<RenderMod>.Logger.Msg(length == preConvertedSize
|
||||||
|
? "[인코더] 고속 GPU 컬러 변환 활성화 (NV12/YUV420P Direct Write, sws_scale 우회)"
|
||||||
|
: "[인코더] CPU 컬러 변환 폴백 활성화 (RGBA Input, sws_scale 사용)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == preConvertedSize)
|
||||||
|
{
|
||||||
|
WritePreConvertedFrame((byte*)pointer.ToPointer());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteFrameFromBuffer((byte*)pointer.ToPointer());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,11 +909,169 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
if (_disposed || !_isInitialized) return;
|
if (_disposed || !_isInitialized) return;
|
||||||
fixed (byte* pBuffer = buffer)
|
fixed (byte* pBuffer = buffer)
|
||||||
{
|
{
|
||||||
WriteFrameFromBuffer(pBuffer);
|
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
|
||||||
|
|
||||||
|
if (!_loggedConversionMode)
|
||||||
|
{
|
||||||
|
_loggedConversionMode = true;
|
||||||
|
Melon<RenderMod>.Logger.Msg(length == preConvertedSize
|
||||||
|
? "[인코더] 고속 GPU 컬러 변환 활성화 (NV12/YUV420P Direct Write, sws_scale 우회)"
|
||||||
|
: "[인코더] CPU 컬러 변환 폴백 활성화 (RGBA Input, sws_scale 사용)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == preConvertedSize)
|
||||||
|
{
|
||||||
|
WritePreConvertedFrame(pBuffer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteFrameFromBuffer(pBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이미 NV12 혹은 YUV420P로 변환된 프레임 데이터를 받아서 다이렉트로 복사하여 인코딩합니다.
|
||||||
|
/// </summary>
|
||||||
|
private void WritePreConvertedFrame(byte* buffer)
|
||||||
|
{
|
||||||
|
ffmpeg.av_frame_make_writable(_pDstFrame);
|
||||||
|
|
||||||
|
var width = _settings.Width;
|
||||||
|
var height = _settings.Height;
|
||||||
|
|
||||||
|
switch (_dstPixFmt)
|
||||||
|
{
|
||||||
|
case AVPixelFormat.AV_PIX_FMT_NV12 or AVPixelFormat.AV_PIX_FMT_QSV:
|
||||||
|
{
|
||||||
|
// Y Plane 복사
|
||||||
|
var pSrcY = buffer;
|
||||||
|
var pDstY = _pDstFrame->data[0];
|
||||||
|
var dstStrideY = _pDstFrame->linesize[0];
|
||||||
|
|
||||||
|
if (width == dstStrideY)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcY, pDstY, dstStrideY * height, width * height);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcY + y * width, pDstY + y * dstStrideY, width, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UV Plane 복사 (Y 평면 크기 이후 위치)
|
||||||
|
var pSrcUV = buffer + width * height;
|
||||||
|
var pDstUV = _pDstFrame->data[1];
|
||||||
|
var dstStrideUV = _pDstFrame->linesize[1];
|
||||||
|
var uvHeight = height / 2;
|
||||||
|
|
||||||
|
if (width == dstStrideUV)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcUV, pDstUV, dstStrideUV * uvHeight, width * uvHeight);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var y = 0; y < uvHeight; y++)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcUV + y * width, pDstUV + y * dstStrideUV, width, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AVPixelFormat.AV_PIX_FMT_YUV420P:
|
||||||
|
{
|
||||||
|
// Y Plane 복사
|
||||||
|
var pSrcY = buffer;
|
||||||
|
var pDstY = _pDstFrame->data[0];
|
||||||
|
var dstStrideY = _pDstFrame->linesize[0];
|
||||||
|
|
||||||
|
if (width == dstStrideY)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcY, pDstY, dstStrideY * height, width * height);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcY + y * width, pDstY + y * dstStrideY, width, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// U Plane 복사
|
||||||
|
var pSrcU = buffer + width * height;
|
||||||
|
var pDstU = _pDstFrame->data[1];
|
||||||
|
var dstStrideU = _pDstFrame->linesize[1];
|
||||||
|
var chromaWidth = width / 2;
|
||||||
|
var chromaHeight = height / 2;
|
||||||
|
|
||||||
|
if (chromaWidth == dstStrideU)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcU, pDstU, dstStrideU * chromaHeight, chromaWidth * chromaHeight);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var y = 0; y < chromaHeight; y++)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcU + y * chromaWidth, pDstU + y * dstStrideU, chromaWidth, chromaWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V Plane 복사
|
||||||
|
var pSrcV = pSrcU + chromaWidth * chromaHeight;
|
||||||
|
var pDstV = _pDstFrame->data[2];
|
||||||
|
var dstStrideV = _pDstFrame->linesize[2];
|
||||||
|
|
||||||
|
if (chromaWidth == dstStrideV)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcV, pDstV, dstStrideV * chromaHeight, chromaWidth * chromaHeight);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var y = 0; y < chromaHeight; y++)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pSrcV + y * chromaWidth, pDstV + y * dstStrideV, chromaWidth, chromaWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"이 픽셀 포맷({_dstPixFmt})은 사전 변환된 버퍼 다이렉트 복사를 지원하지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// QSV 가속 처리 또는 인코더 전송
|
||||||
|
if (_isQsvEncoder)
|
||||||
|
{
|
||||||
|
ffmpeg.av_frame_unref(_pHwFrame);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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(_pHwFrame, _pDstFrame, 0);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception($"QSV 프레임 업로드 실패 — {GetFFmpegErrorString(ret)} (Code: {ret})");
|
||||||
|
|
||||||
|
_pHwFrame->pts = _frameIndex++;
|
||||||
|
SendFrameAndWritePackets(_pHwFrame);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ffmpeg.av_frame_unref(_pHwFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pDstFrame->pts = _frameIndex++;
|
||||||
|
SendFrameAndWritePackets(_pDstFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 실제 프레임 인코딩의 핵심 로직.
|
/// 실제 프레임 인코딩의 핵심 로직.
|
||||||
///
|
///
|
||||||
@@ -1008,7 +1187,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
private void SendFrameAndWritePackets(AVFrame* pFrame)
|
private void SendFrameAndWritePackets(AVFrame* pFrame)
|
||||||
{
|
{
|
||||||
// 인코더 내부 큐에 프레임 전달
|
// 인코더 내부 큐에 프레임 전달
|
||||||
int response = ffmpeg.avcodec_send_frame(_pCodecContext, pFrame);
|
var response = ffmpeg.avcodec_send_frame(_pCodecContext, pFrame);
|
||||||
if (response < 0)
|
if (response < 0)
|
||||||
{
|
{
|
||||||
throw new Exception($"인코더에 프레임을 전송하는 도중 에러가 발생했습니다. (Code: {response})");
|
throw new Exception($"인코더에 프레임을 전송하는 도중 에러가 발생했습니다. (Code: {response})");
|
||||||
@@ -1027,7 +1206,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else if (response < 0)
|
|
||||||
|
if (response < 0)
|
||||||
{
|
{
|
||||||
throw new Exception($"인코더에서 패킷을 수신하는 도중 에러가 발생했습니다. (Code: {response})");
|
throw new Exception($"인코더에서 패킷을 수신하는 도중 에러가 발생했습니다. (Code: {response})");
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-21
@@ -3,6 +3,8 @@ using System.Buffers;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ADOFAIRenderer.Core;
|
using ADOFAIRenderer.Core;
|
||||||
|
using ADOFAIRenderer.Util.FFmpeg;
|
||||||
|
using FFmpeg.AutoGen;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
using Unity.Collections.LowLevel.Unsafe;
|
using Unity.Collections.LowLevel.Unsafe;
|
||||||
@@ -13,6 +15,10 @@ namespace ADOFAIRenderer.Util
|
|||||||
{
|
{
|
||||||
public class FrameReader : IDisposable
|
public class FrameReader : IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly int Height = Shader.PropertyToID("Height");
|
||||||
|
private static readonly int Width = Shader.PropertyToID("Width");
|
||||||
|
private static readonly int DestinationBuffer = Shader.PropertyToID("DestinationBuffer");
|
||||||
|
private static readonly int SourceTexture = Shader.PropertyToID("SourceTexture");
|
||||||
private const int InitialBufferSize = 16;
|
private const int InitialBufferSize = 16;
|
||||||
|
|
||||||
private readonly Stack<ReadbackRequestState> _statePool = new();
|
private readonly Stack<ReadbackRequestState> _statePool = new();
|
||||||
@@ -42,6 +48,24 @@ namespace ADOFAIRenderer.Util
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static unsafe AVPixelFormat PredictPixelFormat(Codec codec)
|
||||||
|
{
|
||||||
|
var candidates = GPUUtil.GetPrioritizedEncoderCandidates(codec);
|
||||||
|
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||||
|
foreach (var encoderName in candidates)
|
||||||
|
{
|
||||||
|
var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoderName);
|
||||||
|
if (pCodec == null) continue;
|
||||||
|
|
||||||
|
if (encoderName.Contains("qsv") || encoderName.Contains("nvenc") || encoderName.Contains("amf"))
|
||||||
|
{
|
||||||
|
return AVPixelFormat.AV_PIX_FMT_NV12;
|
||||||
|
}
|
||||||
|
return encoderName == "libvvenc" ? AVPixelFormat.AV_PIX_FMT_YUV420P10LE : AVPixelFormat.AV_PIX_FMT_YUV420P;
|
||||||
|
}
|
||||||
|
return AVPixelFormat.AV_PIX_FMT_YUV420P;
|
||||||
|
}
|
||||||
|
|
||||||
public void RequestFrameCapture(Texture texture, long currentFrame)
|
public void RequestFrameCapture(Texture texture, long currentFrame)
|
||||||
{
|
{
|
||||||
if (!RenderManager.IsCapturingActive) return;
|
if (!RenderManager.IsCapturingActive) return;
|
||||||
@@ -62,15 +86,74 @@ namespace ADOFAIRenderer.Util
|
|||||||
state.IsInFlight = true;
|
state.IsInFlight = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel
|
// GPU 컬러 변환 활성화 여부 검사
|
||||||
state.Setup(currentFrame, bufferSize);
|
bool useGPUConversion = RenderMod.ColorConversionShader;
|
||||||
|
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
|
||||||
|
|
||||||
AsyncGPUReadback.RequestIntoNativeArray(
|
if (useGPUConversion && RenderManager.Instance)
|
||||||
ref state.NativeBuffer,
|
{
|
||||||
texture,
|
expectedFmt = PredictPixelFormat(RenderManager.Instance.Configuration.Codec);
|
||||||
0,
|
// Compute Shader는 NV12와 YUV420P만 지원하므로 그 외 포맷은 CPU 폴백 처리
|
||||||
state.CachedCallback
|
if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P)
|
||||||
);
|
{
|
||||||
|
useGPUConversion = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel
|
||||||
|
state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height);
|
||||||
|
|
||||||
|
if (useGPUConversion && state.GPUBuffer != null && RenderMod.ColorConversionShader)
|
||||||
|
{
|
||||||
|
int kernelIndex = -1;
|
||||||
|
if (expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12)
|
||||||
|
{
|
||||||
|
kernelIndex = RenderMod.ColorConversionShader.FindKernel("CSMain_NV12");
|
||||||
|
}
|
||||||
|
else if (expectedFmt == AVPixelFormat.AV_PIX_FMT_YUV420P)
|
||||||
|
{
|
||||||
|
kernelIndex = RenderMod.ColorConversionShader.FindKernel("CSMain_YUV420P");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kernelIndex >= 0)
|
||||||
|
{
|
||||||
|
RenderMod.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture);
|
||||||
|
RenderMod.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer);
|
||||||
|
RenderMod.ColorConversionShader.SetInt(Width, texture.width);
|
||||||
|
RenderMod.ColorConversionShader.SetInt(Height, texture.height);
|
||||||
|
|
||||||
|
var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12
|
||||||
|
? (texture.width + 31) / 32
|
||||||
|
: (texture.width + 63) / 64;
|
||||||
|
var threadGroupsY = (texture.height + 15) / 16;
|
||||||
|
|
||||||
|
RenderMod.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||||||
|
|
||||||
|
AsyncGPUReadback.Request(
|
||||||
|
state.GPUBuffer,
|
||||||
|
state.CachedCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 커널을 못 찾은 경우 기존 RGBA 텍스처 백업 기법으로 폴백
|
||||||
|
AsyncGPUReadback.RequestIntoNativeArray(
|
||||||
|
ref state.NativeBuffer,
|
||||||
|
texture,
|
||||||
|
0,
|
||||||
|
state.CachedCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AsyncGPUReadback.RequestIntoNativeArray(
|
||||||
|
ref state.NativeBuffer,
|
||||||
|
texture,
|
||||||
|
0,
|
||||||
|
state.CachedCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void OnReadbackComplete(ReadbackRequestState state, AsyncGPUReadbackRequest request)
|
internal void OnReadbackComplete(ReadbackRequestState state, AsyncGPUReadbackRequest request)
|
||||||
@@ -101,7 +184,9 @@ namespace ADOFAIRenderer.Util
|
|||||||
}
|
}
|
||||||
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame.");
|
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame.");
|
||||||
|
|
||||||
var byteSize = state.NativeBuffer.Length;
|
var byteSize = state.IsGPUConversion
|
||||||
|
? state.Width * state.Height * 3 / 2
|
||||||
|
: state.Width * state.Height * 4;
|
||||||
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
|
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
|
||||||
Array.Clear(blackFrame, 0, byteSize);
|
Array.Clear(blackFrame, 0, byteSize);
|
||||||
|
|
||||||
@@ -117,14 +202,32 @@ namespace ADOFAIRenderer.Util
|
|||||||
{
|
{
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
|
if (state.IsGPUConversion)
|
||||||
var capFrame = new RenderManager.CapturedFrame
|
|
||||||
{
|
{
|
||||||
NativePointer = ptr,
|
var data = request.GetData<byte>();
|
||||||
Length = state.NativeBuffer.Length,
|
// 비동기 요청 버퍼 데이터를 지속성(NativeBuffer) 메모리로 복사
|
||||||
ReadbackState = state
|
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
|
||||||
};
|
|
||||||
PushToQueue(frame, capFrame);
|
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
|
||||||
|
var capFrame = new RenderManager.CapturedFrame
|
||||||
|
{
|
||||||
|
NativePointer = ptr,
|
||||||
|
Length = state.NativeBuffer.Length,
|
||||||
|
ReadbackState = state
|
||||||
|
};
|
||||||
|
PushToQueue(frame, capFrame);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
|
||||||
|
var capFrame = new RenderManager.CapturedFrame
|
||||||
|
{
|
||||||
|
NativePointer = ptr,
|
||||||
|
Length = state.NativeBuffer.Length,
|
||||||
|
ReadbackState = state
|
||||||
|
};
|
||||||
|
PushToQueue(frame, capFrame);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +281,11 @@ namespace ADOFAIRenderer.Util
|
|||||||
public NativeArray<byte> NativeBuffer;
|
public NativeArray<byte> NativeBuffer;
|
||||||
public bool IsInFlight { get; internal set; }
|
public bool IsInFlight { get; internal set; }
|
||||||
|
|
||||||
|
public ComputeBuffer? GPUBuffer;
|
||||||
|
public bool IsGPUConversion { get; private set; }
|
||||||
|
public int Width { get; private set; }
|
||||||
|
public int Height { get; private set; }
|
||||||
|
|
||||||
public readonly Action<AsyncGPUReadbackRequest> CachedCallback;
|
public readonly Action<AsyncGPUReadbackRequest> CachedCallback;
|
||||||
|
|
||||||
public ReadbackRequestState(FrameReader owner)
|
public ReadbackRequestState(FrameReader owner)
|
||||||
@@ -187,19 +295,34 @@ namespace ADOFAIRenderer.Util
|
|||||||
FrameIndex = -1;
|
FrameIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Setup(long frameIndex, int bufferSize)
|
public void Setup(long frameIndex, int bufferSize, bool isGPUConversion, AVPixelFormat pixelFormat, int width, int height)
|
||||||
{
|
{
|
||||||
FrameIndex = frameIndex;
|
FrameIndex = frameIndex;
|
||||||
|
IsGPUConversion = isGPUConversion;
|
||||||
|
Width = width; Height = height;
|
||||||
|
|
||||||
// 버퍼가 생성되지 않았거나 크기가 다른 경우 재생성
|
var readbackSize = isGPUConversion ? (width * height * 3 / 2) : bufferSize;
|
||||||
if (!NativeBuffer.IsCreated || NativeBuffer.Length != bufferSize)
|
if (!NativeBuffer.IsCreated || NativeBuffer.Length != readbackSize)
|
||||||
{
|
{
|
||||||
// 이전 메모리 해제
|
|
||||||
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
||||||
|
|
||||||
NativeBuffer = new NativeArray<byte>(bufferSize, Allocator.Persistent,
|
NativeBuffer = new NativeArray<byte>(readbackSize, Allocator.Persistent,
|
||||||
NativeArrayOptions.UninitializedMemory);
|
NativeArrayOptions.UninitializedMemory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGPUConversion)
|
||||||
|
{
|
||||||
|
var computeBufferSize = readbackSize / 4;
|
||||||
|
if (GPUBuffer != null && GPUBuffer.count == computeBufferSize) return;
|
||||||
|
|
||||||
|
GPUBuffer?.Dispose();
|
||||||
|
GPUBuffer = new ComputeBuffer(computeBufferSize, 4, ComputeBufferType.Raw);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GPUBuffer?.Dispose();
|
||||||
|
GPUBuffer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnComplete(AsyncGPUReadbackRequest request)
|
private void OnComplete(AsyncGPUReadbackRequest request)
|
||||||
@@ -215,6 +338,8 @@ namespace ADOFAIRenderer.Util
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
||||||
|
GPUBuffer?.Dispose();
|
||||||
|
GPUBuffer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-6
@@ -33,27 +33,43 @@ namespace ADOFAIRenderer.Util
|
|||||||
[Codec.VVC] = new List<(GPUVendor, string)> { (GPUVendor.Unknown, "libvvenc") }
|
[Codec.VVC] = new List<(GPUVendor, string)> { (GPUVendor.Unknown, "libvvenc") }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static GPUVendor _cachedVendor = GPUVendor.Unknown;
|
||||||
|
private static bool _hasCachedVendor = false;
|
||||||
|
|
||||||
private static GPUVendor GetActiveGPUVendor()
|
private static GPUVendor GetActiveGPUVendor()
|
||||||
{
|
{
|
||||||
|
if (_hasCachedVendor) return _cachedVendor;
|
||||||
|
|
||||||
var gpuName = UnityEngine.SystemInfo.graphicsDeviceName;
|
var gpuName = UnityEngine.SystemInfo.graphicsDeviceName;
|
||||||
Melon<RenderMod>.Logger.Msg($"Active GPU: {gpuName}");
|
Melon<RenderMod>.Logger.Msg($"Active GPU: {gpuName}");
|
||||||
|
|
||||||
|
_cachedVendor = GPUVendor.Unknown;
|
||||||
foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern))
|
foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern))
|
||||||
{
|
{
|
||||||
return GPUVendor.NVIDIA;
|
_cachedVendor = GPUVendor.NVIDIA;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Match unused in Regex.Matches(gpuName, IntelPattern))
|
if (_cachedVendor == GPUVendor.Unknown)
|
||||||
{
|
{
|
||||||
return GPUVendor.Intel;
|
foreach (Match unused in Regex.Matches(gpuName, IntelPattern))
|
||||||
|
{
|
||||||
|
_cachedVendor = GPUVendor.Intel;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Match unused in Regex.Matches(gpuName, AMDPattern))
|
if (_cachedVendor == GPUVendor.Unknown)
|
||||||
{
|
{
|
||||||
return GPUVendor.AMD;
|
foreach (Match unused in Regex.Matches(gpuName, AMDPattern))
|
||||||
|
{
|
||||||
|
_cachedVendor = GPUVendor.AMD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GPUVendor.Unknown;
|
_hasCachedVendor = true;
|
||||||
|
return _cachedVendor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<string> GetPrioritizedEncoderCandidates(Codec codec)
|
public static List<string> GetPrioritizedEncoderCandidates(Codec codec)
|
||||||
|
|||||||
Reference in new issue
Block a user