SHA256
Add Renderer Feature
This commit is contained in:
1 parent
c9314c264d
commit
2556bc7f62
13 files changed
+727
-21
No files matched your search
@@ -35,3 +35,6 @@ desktop.ini
|
|||||||
.antigravitycli/
|
.antigravitycli/
|
||||||
.agent/
|
.agent/
|
||||||
GEMINI.md
|
GEMINI.md
|
||||||
|
|
||||||
|
# Decompiled Game Code
|
||||||
|
*.cs.txt
|
||||||
@@ -41,6 +41,12 @@
|
|||||||
<Reference Include="UnityEngine.CoreModule">
|
<Reference Include="UnityEngine.CoreModule">
|
||||||
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
|
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.IMGUIModule">
|
||||||
|
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.IMGUIModule.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.TextRenderingModule">
|
||||||
|
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="UnityEngine.InputLegacyModule">
|
<Reference Include="UnityEngine.InputLegacyModule">
|
||||||
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.InputLegacyModule.dll</HintPath>
|
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.InputLegacyModule.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.Component
|
||||||
|
{
|
||||||
|
public class DebugMonitorUI : MonoBehaviour
|
||||||
|
{
|
||||||
|
private Text _text = null!;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
// UI 레이어(5) 설정
|
||||||
|
gameObject.layer = 5;
|
||||||
|
|
||||||
|
// Canvas 설정
|
||||||
|
Canvas canvas = gameObject.AddComponent<Canvas>();
|
||||||
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||||
|
canvas.sortingOrder = 9999;
|
||||||
|
|
||||||
|
// 해상도 다이내믹 스케일링 설정 (Reference: 1920x1080)
|
||||||
|
CanvasScaler scaler = gameObject.AddComponent<CanvasScaler>();
|
||||||
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||||
|
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||||
|
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||||
|
scaler.matchWidthOrHeight = 0.5f;
|
||||||
|
|
||||||
|
gameObject.AddComponent<GraphicRaycaster>();
|
||||||
|
|
||||||
|
// 배경 패널 생성
|
||||||
|
GameObject panelObj = new GameObject("BackgroundPanel");
|
||||||
|
panelObj.transform.SetParent(transform, false);
|
||||||
|
panelObj.layer = 5;
|
||||||
|
|
||||||
|
Image image = panelObj.AddComponent<Image>();
|
||||||
|
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 약간 더 고급스러운 다크톤 아크릴 감성
|
||||||
|
|
||||||
|
RectTransform panelRect = panelObj.GetComponent<RectTransform>();
|
||||||
|
panelRect.anchorMin = new Vector2(1, 1);
|
||||||
|
panelRect.anchorMax = new Vector2(1, 1);
|
||||||
|
panelRect.pivot = new Vector2(1, 1);
|
||||||
|
panelRect.anchoredPosition = new Vector2(-30, -30);
|
||||||
|
panelRect.sizeDelta = new Vector2(340, 250);
|
||||||
|
|
||||||
|
// 텍스트 생성
|
||||||
|
GameObject textObj = new GameObject("MonitorText");
|
||||||
|
textObj.transform.SetParent(panelObj.transform, false);
|
||||||
|
textObj.layer = 5;
|
||||||
|
|
||||||
|
_text = textObj.AddComponent<Text>();
|
||||||
|
_text.supportRichText = true;
|
||||||
|
_text.fontSize = 13;
|
||||||
|
_text.lineSpacing = 1.3f;
|
||||||
|
_text.alignment = TextAnchor.UpperLeft;
|
||||||
|
|
||||||
|
// uGUI 특유의 텍스트 잘림 현상 원천 예방
|
||||||
|
_text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
_text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||||
|
|
||||||
|
Font? font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
if (font == null)
|
||||||
|
{
|
||||||
|
font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||||
|
}
|
||||||
|
if (font == null)
|
||||||
|
{
|
||||||
|
var fonts = Resources.FindObjectsOfTypeAll<Font>();
|
||||||
|
if (fonts.Length > 0) font = fonts[0];
|
||||||
|
}
|
||||||
|
_text.font = font;
|
||||||
|
|
||||||
|
RectTransform textRect = textObj.GetComponent<RectTransform>();
|
||||||
|
textRect.anchorMin = Vector2.zero;
|
||||||
|
textRect.anchorMax = Vector2.one;
|
||||||
|
textRect.offsetMin = new Vector2(20, 20); // 좀 더 넉넉하고 아름다운 패딩 여백
|
||||||
|
textRect.offsetMax = new Vector2(-20, -20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!_text) return;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"• Rendered Progress: <b>{RenderManager.FrameCount}</b> frames");
|
||||||
|
|
||||||
|
int queueCount;
|
||||||
|
lock (RenderManager.ImageQueue)
|
||||||
|
{
|
||||||
|
queueCount = RenderManager.ImageQueue.Count;
|
||||||
|
}
|
||||||
|
sb.AppendLine($"• Queued Buffers: <b>{queueCount}</b> / 60 frames");
|
||||||
|
|
||||||
|
if (RenderMod.IsThrottling)
|
||||||
|
{
|
||||||
|
sb.AppendLine("• Status: <b><color=yellow>THROTTLING (Safe Mode)</color></b>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine("• Status: <b><color=green>NORMAL (Encoding)</color></b>");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"• Throttling Controls: {RenderMod.ThrottleCount} times");
|
||||||
|
sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{RenderMod.ReadbackFailureCount}</color> times");
|
||||||
|
sb.AppendLine($"• Render Skip-Retry: {RenderMod.SkipCount} / 60");
|
||||||
|
|
||||||
|
double memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
|
||||||
|
sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB");
|
||||||
|
|
||||||
|
_text.text = sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Component
|
namespace ADOFAIRenderer.Component
|
||||||
@@ -5,9 +6,10 @@ namespace ADOFAIRenderer.Component
|
|||||||
[DefaultExecutionOrder(int.MaxValue)]
|
[DefaultExecutionOrder(int.MaxValue)]
|
||||||
public class PostRenderHook : MonoBehaviour
|
public class PostRenderHook : MonoBehaviour
|
||||||
{
|
{
|
||||||
private void OnPostRender()
|
private void LateUpdate()
|
||||||
{
|
{
|
||||||
RenderMod.OnPostRender();
|
var val = (MeshRenderer)AccessTools.Field(typeof(scrCamera), "camQuadMesh").GetValue(scrCamera.instance);
|
||||||
|
RenderMod.OnPostRender(val.material.mainTexture as RenderTexture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+228
-8
@@ -1,5 +1,10 @@
|
|||||||
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using ADOFAIRenderer.Util;
|
||||||
|
using MelonLoader;
|
||||||
using MelonLoader.Utils;
|
using MelonLoader.Utils;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -7,12 +12,13 @@ namespace ADOFAIRenderer.Component
|
|||||||
{
|
{
|
||||||
public class RenderManager : MonoBehaviour
|
public class RenderManager : MonoBehaviour
|
||||||
{
|
{
|
||||||
public static int Countdown = 0;
|
public static int Countdown;
|
||||||
public static int FrameCount = 0;
|
public static int FrameCount;
|
||||||
public static bool SkipPressToStart = false;
|
public static bool SkipPressToStart;
|
||||||
public static string LevelPath = "";
|
public static string LevelPath = "";
|
||||||
public static RenderManager Instance = null!;
|
public static RenderManager Instance = null!;
|
||||||
private static int _originalInputOffset;
|
private static int _originalInputOffset;
|
||||||
|
private static GameObject? _debugMonitorObj;
|
||||||
|
|
||||||
public static RenderSettings Configuration;
|
public static RenderSettings Configuration;
|
||||||
|
|
||||||
@@ -26,8 +32,9 @@ namespace ADOFAIRenderer.Component
|
|||||||
|
|
||||||
public int Bitrate;
|
public int Bitrate;
|
||||||
public string Codec;
|
public string Codec;
|
||||||
|
public double EndWaitTime;
|
||||||
|
|
||||||
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec)
|
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec, double endWaitTime)
|
||||||
{
|
{
|
||||||
TargetFPS = targetFPS;
|
TargetFPS = targetFPS;
|
||||||
VideoFPS = videoFPS;
|
VideoFPS = videoFPS;
|
||||||
@@ -37,9 +44,16 @@ namespace ADOFAIRenderer.Component
|
|||||||
|
|
||||||
Bitrate = bitrate;
|
Bitrate = bitrate;
|
||||||
Codec = codec;
|
Codec = codec;
|
||||||
|
EndWaitTime = endWaitTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SortedList<int, byte[]> ImageQueue = new SortedList<int, byte[]>();
|
||||||
|
public static Thread? RenderThread;
|
||||||
|
public static int PassCount = 0;
|
||||||
|
public static bool EndRequested = false;
|
||||||
|
public static bool EndSuccessful = false;
|
||||||
|
private static int _wonFrame = -1;
|
||||||
|
|
||||||
private static bool _renderSetup;
|
private static bool _renderSetup;
|
||||||
|
|
||||||
@@ -67,17 +81,46 @@ namespace ADOFAIRenderer.Component
|
|||||||
_originalInputOffset = scrConductor.currentPreset.inputOffset;
|
_originalInputOffset = scrConductor.currentPreset.inputOffset;
|
||||||
scrConductor.currentPreset.inputOffset = 0;
|
scrConductor.currentPreset.inputOffset = 0;
|
||||||
RDC.auto = true;
|
RDC.auto = true;
|
||||||
|
if (scrController.instance)
|
||||||
|
{
|
||||||
|
scrController.instance.noFail = true;
|
||||||
|
scrController.instance.noFailInfiniteMargin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderMod.TimeBase = Time.timeAsDouble;
|
||||||
|
RenderMod.UnscaledTimeBase = Time.unscaledTimeAsDouble;
|
||||||
|
RenderMod.DspTimeBase = AudioSettings.dspTime;
|
||||||
|
RenderMod.FrameCountBase = Time.frameCount;
|
||||||
|
|
||||||
RenderMod.IsRendering = true;
|
RenderMod.IsRendering = true;
|
||||||
Countdown = 3;
|
Countdown = 3;
|
||||||
|
|
||||||
scrController.instance.StartCoroutine(SetupPlay());
|
scrController.instance!.StartCoroutine(SetupPlay());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RenderMod.IsRendering && scrController.instance.state == States.Won)
|
if (!RenderMod.IsRendering) return;
|
||||||
|
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
|
||||||
{
|
{
|
||||||
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
||||||
|
|
||||||
|
Time.timeScale = 1f;
|
||||||
scrConductor.currentPreset.inputOffset = _originalInputOffset;
|
scrConductor.currentPreset.inputOffset = _originalInputOffset;
|
||||||
|
StopFFmpeg(false);
|
||||||
|
}
|
||||||
|
else if (scrController.instance.state == States.Won)
|
||||||
|
{
|
||||||
|
if (_wonFrame == -1)
|
||||||
|
{
|
||||||
|
_wonFrame = FrameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extraFrames = (int)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS);
|
||||||
|
|
||||||
|
if (FrameCount - _wonFrame < extraFrames) return;
|
||||||
|
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
||||||
|
Time.timeScale = 1f;
|
||||||
|
scrConductor.currentPreset.inputOffset = _originalInputOffset;
|
||||||
|
StopFFmpeg(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +152,11 @@ namespace ADOFAIRenderer.Component
|
|||||||
{
|
{
|
||||||
yield return new WaitForSecondsRealtime(1);
|
yield return new WaitForSecondsRealtime(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameCount = 0;
|
FrameCount = 0;
|
||||||
|
|
||||||
|
StartFFmpegThread();
|
||||||
|
|
||||||
RenderMod.IsCapturing = true;
|
RenderMod.IsCapturing = true;
|
||||||
scrController.instance.paused = scrController.instance.audioPaused = false;
|
scrController.instance.paused = scrController.instance.audioPaused = false;
|
||||||
scrController.instance.levelWasSkipped = true;
|
scrController.instance.levelWasSkipped = true;
|
||||||
@@ -118,5 +164,179 @@ namespace ADOFAIRenderer.Component
|
|||||||
scrConductor.instance.song.clip = null;
|
scrConductor.instance.song.clip = null;
|
||||||
scrConductor.instance.song.time = -1000;
|
scrConductor.instance.song.time = -1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string FindFFmpegPath()
|
||||||
|
{
|
||||||
|
bool isWindows = Application.platform == RuntimePlatform.WindowsPlayer;
|
||||||
|
string ffmpegName = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
||||||
|
|
||||||
|
// 1. Mods/ADOFAIRenderer/ffmpeg
|
||||||
|
var path1 = Path.Combine(MelonEnvironment.ModsDirectory, "ADOFAIRenderer", ffmpegName);
|
||||||
|
if (File.Exists(path1)) return path1;
|
||||||
|
|
||||||
|
// 2. Mods/ffmpeg
|
||||||
|
var path2 = Path.Combine(MelonEnvironment.ModsDirectory, ffmpegName);
|
||||||
|
if (File.Exists(path2)) return path2;
|
||||||
|
|
||||||
|
// 3. GameRootDirectory/ffmpeg
|
||||||
|
var path3 = Path.Combine(MelonEnvironment.GameRootDirectory, ffmpegName);
|
||||||
|
if (File.Exists(path3)) return path3;
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartFFmpegThread()
|
||||||
|
{
|
||||||
|
var ffmpegPath = FindFFmpegPath();
|
||||||
|
if (string.IsNullOrEmpty(ffmpegPath))
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Error("ffmpeg 바이너리를 찾을 수 없습니다. Mods/ADOFAIRenderer/ 에 ffmpeg를 넣어주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_debugMonitorObj == null)
|
||||||
|
{
|
||||||
|
_debugMonitorObj = new GameObject("DebugMonitorCanvas");
|
||||||
|
_debugMonitorObj.AddComponent<DebugMonitorUI>();
|
||||||
|
DontDestroyOnLoad(_debugMonitorObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
FFmpegProcess.FFmpegPath = ffmpegPath;
|
||||||
|
lock (ImageQueue)
|
||||||
|
{
|
||||||
|
ImageQueue.Clear();
|
||||||
|
}
|
||||||
|
PassCount = 0;
|
||||||
|
RenderMod.FrameCountForVideo = 0;
|
||||||
|
RenderMod.RenderCheck = true;
|
||||||
|
RenderMod.SkipCount = 0;
|
||||||
|
EndRequested = false;
|
||||||
|
EndSuccessful = false;
|
||||||
|
_wonFrame = -1;
|
||||||
|
|
||||||
|
double targetFps = Configuration.TargetFPS;
|
||||||
|
double videoFps = Configuration.VideoFPS;
|
||||||
|
float bitrate = Configuration.Bitrate;
|
||||||
|
string codecStr = Configuration.Codec;
|
||||||
|
|
||||||
|
VideoFFmpeg.Codec selectedCodec = VideoFFmpeg.Codec.H264_Default;
|
||||||
|
if (Enum.TryParse<VideoFFmpeg.Codec>(codecStr, true, out var parsedCodec))
|
||||||
|
{
|
||||||
|
selectedCodec = parsedCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
string ext = ".mp4";
|
||||||
|
if (selectedCodec == VideoFFmpeg.Codec.AV1_Default ||
|
||||||
|
selectedCodec == VideoFFmpeg.Codec.AV1_NVIDIA ||
|
||||||
|
selectedCodec == VideoFFmpeg.Codec.AV1_QSV ||
|
||||||
|
selectedCodec == VideoFFmpeg.Codec.VP9_Default ||
|
||||||
|
selectedCodec == VideoFFmpeg.Codec.VP9_NVIDIA)
|
||||||
|
{
|
||||||
|
ext = ".webm";
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
float workingFps = (float)(targetFps % videoFps == 0.0 ? videoFps : targetFps);
|
||||||
|
VideoFFmpeg videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, (int)workingFps, (int)videoFps, selectedCodec);
|
||||||
|
|
||||||
|
var outPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", "out" + ext);
|
||||||
|
SetupCaptureDirectory();
|
||||||
|
|
||||||
|
videoFFmpeg.Start(outPath, bitrate);
|
||||||
|
|
||||||
|
int num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
byte[]? buffer = null;
|
||||||
|
lock (ImageQueue)
|
||||||
|
{
|
||||||
|
if (ImageQueue.Count > 0)
|
||||||
|
{
|
||||||
|
int firstKey = ImageQueue.Keys[0];
|
||||||
|
if (firstKey == PassCount)
|
||||||
|
{
|
||||||
|
buffer = ImageQueue[firstKey];
|
||||||
|
ImageQueue.RemoveAt(0);
|
||||||
|
PassCount += num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer != null)
|
||||||
|
{
|
||||||
|
if (buffer.Length > 0)
|
||||||
|
{
|
||||||
|
int bufferSize = Configuration.RenderWidth * Configuration.RenderHeight * 4;
|
||||||
|
videoFFmpeg.SendDataFromBuffer(buffer, bufferSize);
|
||||||
|
System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EndRequested)
|
||||||
|
{
|
||||||
|
bool hasMore;
|
||||||
|
lock (ImageQueue)
|
||||||
|
{
|
||||||
|
hasMore = ImageQueue.Count > 0;
|
||||||
|
}
|
||||||
|
if (!hasMore) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
videoFFmpeg.Close();
|
||||||
|
EndSuccessful = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
RenderThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopFFmpeg(bool success)
|
||||||
|
{
|
||||||
|
EndRequested = true;
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
StartCoroutine(StopRenderCoroutine());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다.");
|
||||||
|
if (_debugMonitorObj)
|
||||||
|
{
|
||||||
|
Destroy(_debugMonitorObj);
|
||||||
|
_debugMonitorObj = null;
|
||||||
|
}
|
||||||
|
if (RenderThread != null && RenderThread.IsAlive)
|
||||||
|
{
|
||||||
|
RenderThread.Join(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator StopRenderCoroutine()
|
||||||
|
{
|
||||||
|
yield return new WaitUntil(() => EndSuccessful);
|
||||||
|
|
||||||
|
if (_debugMonitorObj)
|
||||||
|
{
|
||||||
|
Destroy(_debugMonitorObj);
|
||||||
|
_debugMonitorObj = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RDC.auto = false;
|
||||||
|
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Patch.ClassPatch
|
namespace ADOFAIRenderer.Patch.ClassPatch
|
||||||
{
|
{
|
||||||
@@ -12,5 +13,33 @@ namespace ADOFAIRenderer.Patch.ClassPatch
|
|||||||
{
|
{
|
||||||
return !RenderMod.IsRendering;
|
return !RenderMod.IsRendering;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("PortalTravelAction")]
|
||||||
|
public static bool PreventPortalTravel()
|
||||||
|
{
|
||||||
|
return !RenderMod.IsRendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("FailAction")]
|
||||||
|
public static bool PreventFailAction()
|
||||||
|
{
|
||||||
|
return !RenderMod.IsRendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("Fail2Action")]
|
||||||
|
public static bool PreventFail2Action()
|
||||||
|
{
|
||||||
|
return !RenderMod.IsRendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("OnPlayerDied")]
|
||||||
|
public static bool PreventPlayerDied()
|
||||||
|
{
|
||||||
|
return !RenderMod.IsRendering;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using HarmonyLib;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.Patch.ClassPatch
|
||||||
|
{
|
||||||
|
[HarmonyPatch(typeof(scrDecoration))]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static class scrDecorationPatch
|
||||||
|
{
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("SetRotation")]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public static bool SetRotationPrefix(scrDecoration __instance)
|
||||||
|
{
|
||||||
|
if (__instance == null || !__instance || __instance.pivotTrans == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("SetScale")]
|
||||||
|
public static bool SetScalePrefix(scrDecoration __instance)
|
||||||
|
{
|
||||||
|
if (__instance == null || !__instance || __instance.pivotTrans == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("SetOpacity")]
|
||||||
|
public static bool SetOpacityPrefix(scrDecoration __instance)
|
||||||
|
{
|
||||||
|
if (__instance == null || !__instance)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPrefix]
|
||||||
|
[HarmonyPatch("UpdatePosition")]
|
||||||
|
public static bool UpdatePositionPrefix(scrDecoration __instance)
|
||||||
|
{
|
||||||
|
if (__instance == null || !__instance || __instance.pivotTrans == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
|
|||||||
{
|
{
|
||||||
if (!RenderMod.IsRendering) return true;
|
if (!RenderMod.IsRendering) return true;
|
||||||
|
|
||||||
__result = RenderManager.Countdown == 0;
|
__result = RenderManager.FrameCount == 0 && RenderManager.Countdown == 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref float __result)
|
public static bool Prefix(ref float __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = (float)(RenderManager.FrameCount / RenderManager.Configuration.TargetFPS);
|
__result = (float)(RenderManager.FrameCount / RenderManager.Configuration.TargetFPS + RenderMod.TimeBase);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref double __result)
|
public static bool Prefix(ref double __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = RenderManager.FrameCount / RenderManager.Configuration.TargetFPS;
|
__result = RenderManager.FrameCount / RenderManager.Configuration.TargetFPS + RenderMod.TimeBase;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref float __result)
|
public static bool Prefix(ref float __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = (float)(RenderManager.FrameCount / RenderManager.Configuration.TargetFPS);
|
__result = (float)(RenderManager.FrameCount / RenderManager.Configuration.TargetFPS + RenderMod.UnscaledTimeBase);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref double __result)
|
public static bool Prefix(ref double __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = RenderManager.FrameCount / RenderManager.Configuration.TargetFPS;
|
__result = RenderManager.FrameCount / RenderManager.Configuration.TargetFPS + RenderMod.UnscaledTimeBase;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref double __result)
|
public static bool Prefix(ref double __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = RenderManager.FrameCount / RenderManager.Configuration.TargetFPS;
|
__result = RenderManager.FrameCount / RenderManager.Configuration.TargetFPS + RenderMod.DspTimeBase;
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref int __result)
|
public static bool Prefix(ref int __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = RenderManager.FrameCount;
|
__result = RenderManager.FrameCount + RenderMod.FrameCountBase;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-3
@@ -6,6 +6,7 @@ using ADOFAIRenderer.Util;
|
|||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.3-INDEV", "ADOFAI Editor Forum")]
|
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.3-INDEV", "ADOFAI Editor Forum")]
|
||||||
@@ -17,15 +18,24 @@ namespace ADOFAIRenderer
|
|||||||
public class RenderMod : MelonMod
|
public class RenderMod : MelonMod
|
||||||
{
|
{
|
||||||
public static AssetBundle FontBundle = null!;
|
public static AssetBundle FontBundle = null!;
|
||||||
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
public static AssetBundle SceneBundle = null!;
|
public static AssetBundle SceneBundle = null!;
|
||||||
public static bool WatchGameLoaded;
|
public static bool WatchGameLoaded;
|
||||||
public static string[] ModScenes = {};
|
public static string[] ModScenes = {};
|
||||||
|
|
||||||
private static double _beforeTime = 0;
|
private static double _beforeTime;
|
||||||
|
|
||||||
public static bool IsRendering = false;
|
public static bool IsRendering = false;
|
||||||
public static bool IsCapturing = false;
|
public static bool IsCapturing = false;
|
||||||
|
|
||||||
|
public static double TimeBase;
|
||||||
|
public static double UnscaledTimeBase;
|
||||||
|
public static double DspTimeBase;
|
||||||
|
public static int FrameCountBase;
|
||||||
|
|
||||||
|
public static bool RenderCheck = true;
|
||||||
|
public static int SkipCount = 0;
|
||||||
|
|
||||||
public override void OnInitializeMelon()
|
public override void OnInitializeMelon()
|
||||||
{
|
{
|
||||||
var renderManagerObj = new GameObject("Render Manager");
|
var renderManagerObj = new GameObject("Render Manager");
|
||||||
@@ -65,14 +75,124 @@ namespace ADOFAIRenderer
|
|||||||
_beforeTime = Time.realtimeSinceStartupAsDouble;
|
_beforeTime = Time.realtimeSinceStartupAsDouble;
|
||||||
|
|
||||||
if (!IsRendering) return;
|
if (!IsRendering) return;
|
||||||
|
|
||||||
|
// [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)]
|
||||||
|
if (RenderCheck)
|
||||||
|
{
|
||||||
|
if (SkipCount < 60)
|
||||||
|
{
|
||||||
|
Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {RenderManager.FrameCount} rendering missed. Retrying... (Attempt {SkipCount + 1}/60)");
|
||||||
|
SkipCount++;
|
||||||
|
Time.timeScale = 0f; // 진행 시간 멈추기 (재시도)
|
||||||
|
return; // 업데이트 진행하지 않고 대기
|
||||||
|
}
|
||||||
|
|
||||||
|
// 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지
|
||||||
|
Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {RenderManager.FrameCount} rendering failed after 60 retries. Supplying black frame.");
|
||||||
|
|
||||||
|
int targetFps = (int)RenderManager.Configuration.TargetFPS;
|
||||||
|
int videoFps = (int)RenderManager.Configuration.VideoFPS;
|
||||||
|
int num = targetFps % videoFps != 0 ? 1 : targetFps / videoFps;
|
||||||
|
|
||||||
|
if (FrameCountForVideo % num == 0)
|
||||||
|
{
|
||||||
|
int byteSize = RenderManager.Configuration.RenderWidth * RenderManager.Configuration.RenderHeight * 4;
|
||||||
|
byte[] blackFrame = System.Buffers.ArrayPool<byte>.Shared.Rent(byteSize);
|
||||||
|
Array.Clear(blackFrame, 0, byteSize);
|
||||||
|
|
||||||
|
lock (RenderManager.ImageQueue)
|
||||||
|
{
|
||||||
|
RenderManager.ImageQueue[FrameCountForVideo] = blackFrame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameCountForVideo++;
|
||||||
|
RenderCheck = false; // 진행 허용
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipCount = 0;
|
||||||
|
RenderCheck = true; // 다음 프레임 검사용으로 초기화
|
||||||
RenderManager.FrameCount++;
|
RenderManager.FrameCount++;
|
||||||
|
|
||||||
Time.timeScale = (float)(Time.deltaTime / realDeltaTime);
|
Time.timeScale = (float)(Time.deltaTime / realDeltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void OnPostRender()
|
public static int FrameCountForVideo = 0;
|
||||||
|
public static int ReadbackFailureCount = 0;
|
||||||
|
public static int ThrottleCount = 0;
|
||||||
|
public static bool IsThrottling = false;
|
||||||
|
|
||||||
|
public static void OnPostRender(RenderTexture? texture)
|
||||||
{
|
{
|
||||||
|
if (!IsCapturing || !texture) return;
|
||||||
|
|
||||||
|
RenderCheck = false; // 이번 프레임 정상 렌더링 완료
|
||||||
|
|
||||||
|
// [Backpressure Throttling (2번 작업)]
|
||||||
|
const int maxQueuedFrames = 60;
|
||||||
|
IsThrottling = false;
|
||||||
|
|
||||||
|
while (IsCapturing && IsRendering)
|
||||||
|
{
|
||||||
|
int count;
|
||||||
|
lock (RenderManager.ImageQueue)
|
||||||
|
{
|
||||||
|
count = RenderManager.ImageQueue.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count <= maxQueuedFrames) break;
|
||||||
|
|
||||||
|
if (!IsThrottling)
|
||||||
|
{
|
||||||
|
IsThrottling = true;
|
||||||
|
ThrottleCount++;
|
||||||
|
}
|
||||||
|
System.Threading.Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
IsThrottling = false;
|
||||||
|
|
||||||
|
var currentFrame = FrameCountForVideo++;
|
||||||
|
|
||||||
|
var targetFps = RenderManager.Configuration.TargetFPS;
|
||||||
|
var videoFps = RenderManager.Configuration.VideoFPS;
|
||||||
|
var num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps);
|
||||||
|
|
||||||
|
if (currentFrame % num != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncGPUReadback.Request(texture, 0, delegate(AsyncGPUReadbackRequest req)
|
||||||
|
{
|
||||||
|
if (!IsCapturing) return;
|
||||||
|
|
||||||
|
byte[] rawBytes;
|
||||||
|
if (req.hasError)
|
||||||
|
{
|
||||||
|
ReadbackFailureCount++;
|
||||||
|
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {currentFrame}. Queue deadlock prevented by sending an empty frame.");
|
||||||
|
rawBytes = Array.Empty<byte>(); // 1번 작업: 빈 배열로 대체하여 정체 방지
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = req.GetData<byte>();
|
||||||
|
rawBytes = FFmpegProcess.RentRawBytes(data); // 3번 작업: ArrayPool에서 대여
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ReadbackFailureCount++;
|
||||||
|
Melon<RenderMod>.Logger.Error($"[OOM Prevent] Native conversion failed for frame {currentFrame}: {ex}");
|
||||||
|
rawBytes = Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (RenderManager.ImageQueue)
|
||||||
|
{
|
||||||
|
RenderManager.ImageQueue[currentFrame] = rawBytes;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void StartRender()
|
public static void StartRender()
|
||||||
|
|||||||
+10
-1
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using ADOFAIRenderer.Component;
|
using ADOFAIRenderer.Component;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
@@ -26,11 +27,18 @@ namespace ADOFAIRenderer.UI
|
|||||||
_buttonComponent.onClick.AddListener(OnClick);
|
_buttonComponent.onClick.AddListener(OnClick);
|
||||||
|
|
||||||
RenderMod.FixEventSystem();
|
RenderMod.FixEventSystem();
|
||||||
|
|
||||||
|
if (codecDropdown == null) return;
|
||||||
|
codecDropdown.ClearOptions();
|
||||||
|
|
||||||
|
var options = System.Enum.GetNames(typeof(Util.VideoFFmpeg.Codec)).ToList();
|
||||||
|
codecDropdown.AddOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClick()
|
public void OnClick()
|
||||||
{
|
{
|
||||||
if (IsAnyFieldEmpty()) return;
|
if (IsAnyFieldEmpty()) return;
|
||||||
|
if (RenderMod.IsRendering) return;
|
||||||
Melon<RenderMod>.Logger.Msg($"레벨 로드: {RenderManager.LevelPath}");
|
Melon<RenderMod>.Logger.Msg($"레벨 로드: {RenderManager.LevelPath}");
|
||||||
|
|
||||||
double.TryParse(targetFPSInput.text, out var targetFPS);
|
double.TryParse(targetFPSInput.text, out var targetFPS);
|
||||||
@@ -49,7 +57,8 @@ namespace ADOFAIRenderer.UI
|
|||||||
RenderWidth = renderWidth,
|
RenderWidth = renderWidth,
|
||||||
RenderHeight = renderHeight,
|
RenderHeight = renderHeight,
|
||||||
Bitrate = bitrate,
|
Bitrate = bitrate,
|
||||||
Codec = selectedCodecName.ToLower(),
|
Codec = selectedCodecName,
|
||||||
|
EndWaitTime = 15.0
|
||||||
};
|
};
|
||||||
|
|
||||||
RenderManager.Instance.SetupCaptureDirectory();
|
RenderManager.Instance.SetupCaptureDirectory();
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Unity.Collections;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.Util
|
||||||
|
{
|
||||||
|
public class FFmpegProcess
|
||||||
|
{
|
||||||
|
public static string FFmpegPath = "";
|
||||||
|
|
||||||
|
private Process? _ffmpegProcess;
|
||||||
|
private Stream? _inputStream;
|
||||||
|
|
||||||
|
public bool IsEnd => _ffmpegProcess is { HasExited: true };
|
||||||
|
|
||||||
|
public void Execute(string command)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(command))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Command is empty");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(FFmpegPath) || !File.Exists(FFmpegPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"ffmpeg binary not found at '{FFmpegPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ffmpegProcess = new Process();
|
||||||
|
_ffmpegProcess.StartInfo.FileName = FFmpegPath;
|
||||||
|
_ffmpegProcess.StartInfo.Arguments = command;
|
||||||
|
_ffmpegProcess.StartInfo.UseShellExecute = false;
|
||||||
|
_ffmpegProcess.StartInfo.RedirectStandardInput = true;
|
||||||
|
_ffmpegProcess.StartInfo.CreateNoWindow = true;
|
||||||
|
_ffmpegProcess.Start();
|
||||||
|
_inputStream = _ffmpegProcess.StandardInput.BaseStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendDataFromBuffer(byte[] buffer, int length = -1)
|
||||||
|
{
|
||||||
|
if (_inputStream == null) return;
|
||||||
|
int writeLength = length < 0 ? buffer.Length : length;
|
||||||
|
_inputStream.Write(buffer, 0, writeLength);
|
||||||
|
_inputStream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return;
|
||||||
|
_inputStream.Flush();
|
||||||
|
_inputStream.Close();
|
||||||
|
_ffmpegProcess.WaitForExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] RentRawBytes<T>(NativeArray<T> arr) where T : struct
|
||||||
|
{
|
||||||
|
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
||||||
|
var array = ArrayPool<byte>.Shared.Rent(val.Length);
|
||||||
|
val.CopyTo(array);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] ToRawBytes<T>(NativeArray<T> arr) where T : struct
|
||||||
|
{
|
||||||
|
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
||||||
|
var array = new byte[val.Length];
|
||||||
|
val.CopyTo(array);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using Unity.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.Util
|
||||||
|
{
|
||||||
|
public class VideoFFmpeg : FFmpegProcess
|
||||||
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public enum Codec
|
||||||
|
{
|
||||||
|
H264_Default,
|
||||||
|
H264_NVIDIA,
|
||||||
|
H264_QSV,
|
||||||
|
HEVC_Default,
|
||||||
|
HEVC_NVIDIA,
|
||||||
|
HEVC_QSV,
|
||||||
|
AV1_Default,
|
||||||
|
AV1_NVIDIA,
|
||||||
|
AV1_QSV,
|
||||||
|
VP9_Default,
|
||||||
|
VP9_NVIDIA
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Width;
|
||||||
|
public int Height;
|
||||||
|
public int FrameRate;
|
||||||
|
public int VideoFPS;
|
||||||
|
public Codec VideoCodec;
|
||||||
|
|
||||||
|
private static string Codec2String(Codec c)
|
||||||
|
{
|
||||||
|
return c switch
|
||||||
|
{
|
||||||
|
Codec.H264_Default => "-vf vflip",
|
||||||
|
Codec.H264_NVIDIA => "-c:v h264_nvenc -vf vflip",
|
||||||
|
Codec.H264_QSV => "-c:v h264_qsv -vf vflip",
|
||||||
|
Codec.HEVC_Default => "-c:v libx265 -vf vflip",
|
||||||
|
Codec.HEVC_NVIDIA => "-c:v hevc_nvenc -vf vflip",
|
||||||
|
Codec.HEVC_QSV => "-c:v hevc_qsv -vf vflip",
|
||||||
|
Codec.AV1_Default => "-c:v libaom-av1 -vf vflip",
|
||||||
|
Codec.AV1_NVIDIA => "-c:v av1_nvenc -vf vflip",
|
||||||
|
Codec.AV1_QSV => "-c:v av1_qsv -vf vflip",
|
||||||
|
Codec.VP9_Default => "-c:v libvpx-vp9 -vf vflip",
|
||||||
|
Codec.VP9_NVIDIA => "-c:v vp9_nvenc -vf vflip",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public VideoFFmpeg(int w, int h, int frame, int videoFrame, Codec codec)
|
||||||
|
{
|
||||||
|
Width = w;
|
||||||
|
Height = h;
|
||||||
|
FrameRate = frame;
|
||||||
|
VideoFPS = videoFrame;
|
||||||
|
VideoCodec = codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendData(NativeArray<byte> source)
|
||||||
|
{
|
||||||
|
var buffer = ToRawBytes(source);
|
||||||
|
SendDataFromBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(string path, float bitrate)
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
var command = "-y -f rawvideo -vcodec rawvideo -pixel_format rgba -colorspace rgb -color_range 2 -video_size " + Width + "x" + Height + " -framerate " + FrameRate + " -threads 4 -loglevel quiet -i - " + Codec2String(VideoCodec) + " -pix_fmt yuv420p -fps_mode cfr " + " -r " + VideoFPS + $" -b:v {(int)bitrate}k" + " \"" + path + "\"";
|
||||||
|
Execute(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user