4 Commits
Author SHA256 Message Date
copecone fd54371de2 Refactor 2026-10-07 01:11:32 +09:00
copecone ea9db52c86 Remove Legacy Resource Loading 2026-10-07 00:06:53 +09:00
copecone 0e691550e7 Finalize O5Kit Theme & Window Generation 2026-10-07 00:00:40 +09:00
copecone 01f1d4be9d Add License Exception 2026-10-06 17:49:05 +09:00
12 changed files with 667 additions and 602 deletions

No files matched your search

+2 -3
View File
@@ -117,7 +117,6 @@ internal class RenderManager : MonoBehaviour
public int Length; public int Length;
public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame) public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame)
public ReadbackRequestState? ReadbackState; // Associated state to recycle public ReadbackRequestState? ReadbackState; // Associated state to recycle
public bool IsColorConverted;
} }
// Thread and Queue State // Thread and Queue State
@@ -547,7 +546,7 @@ internal class RenderManager : MonoBehaviour
{ {
if (frame.ManagedBuffer.Length > 0) if (frame.ManagedBuffer.Length > 0)
{ {
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted); videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length);
Interlocked.Increment(ref framesSent); Interlocked.Increment(ref framesSent);
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer); ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
@@ -555,7 +554,7 @@ internal class RenderManager : MonoBehaviour
} }
else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0) else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0)
{ {
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted); videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length);
Interlocked.Increment(ref framesSent); Interlocked.Increment(ref framesSent);
if (frame.ReadbackState != null) if (frame.ReadbackState != null)
+3 -9
View File
@@ -1,4 +1,4 @@
# ADOFAIRenderer API & Data Linking Exception (Dynamic Linking Only) # ADOFAIRenderer API & Data Linking Exception
> **Additional Permission under GNU General Public License version 3 section 7** > **Additional Permission under GNU General Public License version 3 section 7**
@@ -8,15 +8,13 @@
Linking this Program statically or dynamically with other modules is making a combined work based on this Program. Thus, the terms and conditions of the GNU General Public License cover the whole combination. Linking this Program statically or dynamically with other modules is making a combined work based on this Program. Thus, the terms and conditions of the GNU General Public License cover the whole combination.
As a special exception, the copyright holders of this Program give you permission to dynamically link this Program with independent modules that interact with or reference this Program solely through the public application programming interfaces (APIs), types, and data structures declared within the following namespaces: As a special exception, the copyright holders of this Program give you permission to combine this Program with independent modules that interact with or reference this Program solely through the public application programming interfaces (APIs), types, and data structures declared within the following namespaces:
- `ADOFAIRenderer.API` - `ADOFAIRenderer.API`
- `ADOFAIRenderer.Data` - `ADOFAIRenderer.Data`
and to copy, convey, and distribute the resulting combined work under terms of your choice, without requiring such combined work or independent modules to be covered under the GNU General Public License version 3, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. and to copy, convey, and distribute the resulting combined work under terms of your choice, without requiring such combined work or independent modules to be covered under the GNU General Public License version 3, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.
This exception applies exclusively to dynamic linking. Static linking, including but not limited to combining, embedding, compiling this Program into another binary, or merging assemblies (such as via ILMerge or Costura), is explicitly excluded from this exception and remains fully subject to the terms and copyleft requirements of the GNU General Public License version 3.
An **"independent module"** is a separate and distinct software module that is not derived from or based on this Program, other than referencing or communicating with the interfaces and types declared within the designated namespaces above. An **"independent module"** is a separate and distinct software module that is not derived from or based on this Program, other than referencing or communicating with the interfaces and types declared within the designated namespaces above.
This exception does not grant any permission to modify the source code of this Program itself outside the terms of the GNU General Public License version 3. This exception does not grant any permission to modify the source code of this Program itself outside the terms of the GNU General Public License version 3.
@@ -31,12 +29,8 @@ In accordance with section 7 of the GNU GPL version 3, if you modify this Progra
### 핵심 요약 ### 핵심 요약
- **동적 링킹(Dynamic Linking)만 허용**:
- 본 예외 조항은 **동적 링킹(런타임 참조 및 어셈블리 동적 로드)**에만 배타적으로 적용됩니다.
- ADOFAIRenderer 또는 그 일부 코드를 대상 모드/프로그램에 직접 병합하거나 포함(ILMerge, Costura, 바이너리 직접 내장/정적 컴파일 등)하는 **정적 링킹(Static Linking)은 본 예외 대상에서 제외**되며, 순수 GPLv3의 카피레프트(전염성) 의무가 그대로 적용됩니다.
- **외부 모드의 라이선스 자유**: - **외부 모드의 라이선스 자유**:
- 제3자 모드 또는 외부 프로그램이 동적 링킹을 통해 `ADOFAIRenderer.API` 및 `ADOFAIRenderer.Data` 네임스페이스의 공개 API, 클래스, 데이터 구조만을 참조·호출하여 연동하는 경우, 해당 모드는 **GPLv3의 카피레프트 의무를 적용받지 않습니다.** - 제3자 모드 또는 외부 프로그램이 `ADOFAIRenderer.API` 및 `ADOFAIRenderer.Data` 네임스페이스의 공개 API, 클래스, 데이터 구조를 참조·호출하여 연동하는 경우, 해당 모드는 **GPLv3의 카피레프트(전염성) 의무를 적용받지 않습니다.**
- 외부 모드 개발자는 자신의 모드를 MIT, Apache-2.0, 독점/상용 라이선스 등 원하는 라이선스로 자유롭게 배포할 수 있습니다. - 외부 모드 개발자는 자신의 모드를 MIT, Apache-2.0, 독점/상용 라이선스 등 원하는 라이선스로 자유롭게 배포할 수 있습니다.
- **ADOFAIRenderer 코어 보호**: - **ADOFAIRenderer 코어 보호**:
- ADOFAIRenderer 자체의 소스 코드(렌더링 파이프라인, 코어 패치, 내부 유틸리티 등)를 수정하거나 재배포하는 경우에는 본 예외 조항이 적용되지 않으며, 순수 GPLv3 라이선스 조건이 그대로 적용됩니다. - ADOFAIRenderer 자체의 소스 코드(렌더링 파이프라인, 코어 패치, 내부 유틸리티 등)를 수정하거나 재배포하는 경우에는 본 예외 조항이 적용되지 않으며, 순수 GPLv3 라이선스 조건이 그대로 적용됩니다.
+4 -4
View File
@@ -77,7 +77,7 @@ public static class scnGamePatch
if (!RenderManager.IsActive) if (!RenderManager.IsActive)
{ {
// Remove FFmpeg player if it was attached previously // Remove FFmpeg player if it was attached previously
var oldPlayer = __instance.videoBG.gameObject.GetComponent<VideoBGPlayer>(); var oldPlayer = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
if (oldPlayer) Object.Destroy(oldPlayer); if (oldPlayer) Object.Destroy(oldPlayer);
// Re-enable native player component // Re-enable native player component
@@ -123,15 +123,15 @@ public static class scnGamePatch
__instance.videoBG.enabled = false; __instance.videoBG.enabled = false;
// Add or retrieve our custom FFmpeg video background player // Add or retrieve our custom FFmpeg video background player
if (!__instance.videoBG.gameObject.TryGetComponent<VideoBGPlayer>(out var player)) if (!__instance.videoBG.gameObject.TryGetComponent<FFmpegVideoBGPlayer>(out var player))
{ {
player = __instance.videoBG.gameObject.AddComponent<VideoBGPlayer>(); player = __instance.videoBG.gameObject.AddComponent<FFmpegVideoBGPlayer>();
} }
var isLooping = __instance.levelData.miscSettings.GetBool("loopVideo"); var isLooping = __instance.levelData.miscSettings.GetBool("loopVideo");
// Initialize player. If targetTexture is null (CameraFarPlane mode), it will build a quad in front of targetCamera. // Initialize player. If targetTexture is null (CameraFarPlane mode), it will build a quad in front of targetCamera.
player.Init(videoPath, __instance.videoBG.targetTexture, isLooping, targetCamera, __instance.videoBG.gameObject); player.Init(videoPath, __instance.videoBG?.targetTexture, isLooping, targetCamera, __instance.videoBG.gameObject);
return false; // Skip original UpdateVideo return false; // Skip original UpdateVideo
} }
+2 -2
View File
@@ -90,6 +90,6 @@ The compiled mod will be output to `bin/Release/netstandard2.1/ADOFAIRenderer.dl
## 📄 License ## 📄 License
This project is licensed under the **GNU General Public License v3.0 with API & Data Linking Exception (GPLv3 with Exception, Dynamic Linking Only)**. This project is licensed under the **GNU General Public License v3.0 with API & Data Linking Exception (GPLv3 with Exception)**.
- Third-party mods and external tools that reference or dynamically link to ADOFAIRenderer solely via the `ADOFAIRenderer.API` and `ADOFAIRenderer.Data` namespaces are exempt from GPLv3 copyleft requirements and may be distributed under terms of their author's choice. (Static linking is excluded from this exception.) - Third-party mods and external tools that reference or link to ADOFAIRenderer solely via the `ADOFAIRenderer.API` and `ADOFAIRenderer.Data` namespaces are exempt from GPLv3 copyleft requirements and may be distributed under terms of their author's choice.
- For full license terms and exception details, see [LICENSE](LICENSE) and [LICENSE-EXCEPTION.md](LICENSE-EXCEPTION.md). - For full license terms and exception details, see [LICENSE](LICENSE) and [LICENSE-EXCEPTION.md](LICENSE-EXCEPTION.md).
+2 -1
View File
@@ -2,6 +2,7 @@ using System;
using ADOFAIRenderer; using ADOFAIRenderer;
using ADOFAIRenderer.Core; using ADOFAIRenderer.Core;
using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Core.Data;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.UI; using ADOFAIRenderer.UI;
using FFmpeg.AutoGen; using FFmpeg.AutoGen;
using MelonLoader; using MelonLoader;
@@ -9,7 +10,7 @@ using MelonLoader.Utils;
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.2-INDEV", "ADOFAI Editor Forum")] [assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.1-INDEV", "ADOFAI Editor Forum")]
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
namespace ADOFAIRenderer; namespace ADOFAIRenderer;
+17 -2
View File
@@ -50,7 +50,20 @@ internal sealed class RendererFontProvider : IFontProvider
internal class RendererUIManager : MonoBehaviour internal class RendererUIManager : MonoBehaviour
{ {
public static RendererUIManager Instance = null!; private static RendererUIManager? _instance;
public static RendererUIManager Instance
{
get
{
if (_instance != null) return _instance;
var go = new GameObject("RendererUIManager");
DontDestroyOnLoad(go);
_instance = go.AddComponent<RendererUIManager>();
return _instance;
}
}
public static O5Context Context => Instance._context; public static O5Context Context => Instance._context;
public O5WindowManager WindowManager => _windowManager; public O5WindowManager WindowManager => _windowManager;
@@ -59,7 +72,7 @@ internal class RendererUIManager : MonoBehaviour
private O5WindowManager _windowManager = null!; private O5WindowManager _windowManager = null!;
private void Awake() { private void Awake() {
Instance = this; _instance = this;
_context = new O5Context( _context = new O5Context(
config: new O5Config { config: new O5Config {
@@ -105,5 +118,7 @@ internal class RendererUIManager : MonoBehaviour
private void OnDestroy() { private void OnDestroy() {
_windowManager.Dispose(); _windowManager.Dispose();
_context.Dispose(); _context.Dispose();
if (_instance == this) _instance = null;
} }
} }
+3 -2
View File
@@ -59,7 +59,8 @@ internal static class RenderConfigWindow
{ {
Id = "RendererMod_RenderConfig", Id = "RendererMod_RenderConfig",
Title = LocalizationManager.Get("ui.render_config.window_title"), Title = LocalizationManager.Get("ui.render_config.window_title"),
Size = new Vector2(900, 600), Draggable = true, Resizable = true, Size = new Vector2(900, 600), TopBarHeight = 40,
Draggable = true, Resizable = true,
ShowCloseButton = true ShowCloseButton = true
}); });
@@ -158,7 +159,7 @@ internal static class RenderConfigWindow
format: "F0", clampMode: ClampMode.All, format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2), sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2),
onComplete: null, onChanged: val => { config.RenderHeight = (int)Math.Round(val); }, onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); },
text: LocalizationManager.Get("ui.render_config.render_height"), text: LocalizationManager.Get("ui.render_config.render_height"),
id: "slider_render_height", showFill: true id: "slider_render_height", showFill: true
); );
+51 -15
View File
@@ -974,22 +974,29 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// Encodes a frame from IntPtr (native pointer). /// Encodes a frame from IntPtr (native pointer).
/// Can be used from external native plugins or custom capture sources. /// Can be used from external native plugins or custom capture sources.
/// </summary> /// </summary>
public void SendDataFromNativeBuffer(IntPtr pointer, int length, bool isColorConverted) public void SendDataFromNativeBuffer(IntPtr pointer, int length)
{ {
lock (_lock) lock (_lock)
{ {
if (_disposed || !_isInitialized) return; if (_disposed || !_isInitialized) return;
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
if (!_loggedConversionMode) if (!_loggedConversionMode)
{ {
_loggedConversionMode = true; _loggedConversionMode = true;
Melon<RenderMod>.Logger.Msg(isColorConverted Melon<RenderMod>.Logger.Msg(length == preConvertedSize
? "[Encoder] GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)" ? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)"
: "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)"); : "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)");
} }
if (isColorConverted) WritePreConvertedFrame((byte*)pointer.ToPointer()); if (length == preConvertedSize)
else WriteFrameFromBuffer((byte*)pointer.ToPointer()); {
WritePreConvertedFrame((byte*)pointer.ToPointer());
}
else
{
WriteFrameFromBuffer((byte*)pointer.ToPointer());
}
} }
} }
@@ -997,23 +1004,31 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// Encodes a frame from a managed byte[]. /// Encodes a frame from a managed byte[].
/// Pins the array using fixed keyword to prevent GC from moving it before passing the pointer. /// Pins the array using fixed keyword to prevent GC from moving it before passing the pointer.
/// </summary> /// </summary>
public void SendDataFromBuffer(byte[] buffer, int length, bool isColorConverted) public void SendDataFromBuffer(byte[] buffer, int length)
{ {
lock (_lock) lock (_lock)
{ {
if (_disposed || !_isInitialized) return; if (_disposed || !_isInitialized) return;
fixed (byte* pBuffer = buffer) fixed (byte* pBuffer = buffer)
{ {
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
if (!_loggedConversionMode) if (!_loggedConversionMode)
{ {
_loggedConversionMode = true; _loggedConversionMode = true;
Melon<RenderMod>.Logger.Msg(isColorConverted Melon<RenderMod>.Logger.Msg(length == preConvertedSize
? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)" ? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)"
: "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)"); : "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)");
} }
if (isColorConverted) WritePreConvertedFrame(pBuffer); if (length == preConvertedSize)
else WriteFrameFromBuffer(pBuffer); {
WritePreConvertedFrame(pBuffer);
}
else
{
WriteFrameFromBuffer(pBuffer);
}
} }
} }
} }
@@ -1130,7 +1145,33 @@ namespace ADOFAIRenderer.Util.FFmpeg
throw new NotSupportedException($"This pixel format ({_dstPixFmt}) does not support direct copying of pre-converted buffers."); throw new NotSupportedException($"This pixel format ({_dstPixFmt}) does not support direct copying of pre-converted buffers.");
} }
SubmitDstFrame(); // QSV acceleration handling or encoder send
if (_isQsvEncoder)
{
ffmpeg.av_frame_unref(_pHwFrame);
try
{
var ret = ffmpeg.av_hwframe_get_buffer(_pCodecContext->hw_frames_ctx, _pHwFrame, 0);
if (ret < 0)
throw new Exception($"Failed to allocate QSV hardware frame buffer — {GetFFmpegErrorString(ret)} (Code: {ret})");
ret = ffmpeg.av_hwframe_transfer_data(_pHwFrame, _pDstFrame, 0);
if (ret < 0)
throw new Exception($"Failed to upload QSV frame — {GetFFmpegErrorString(ret)} (Code: {ret})");
_pHwFrame->pts = _frameIndex++;
SendFrameAndWritePackets(_pHwFrame);
}
finally
{
ffmpeg.av_frame_unref(_pHwFrame);
}
}
else
{
_pDstFrame->pts = _frameIndex++;
SendFrameAndWritePackets(_pDstFrame);
}
} }
/// <summary> /// <summary>
@@ -1168,11 +1209,6 @@ namespace ADOFAIRenderer.Util.FFmpeg
_pDstFrame->data, _pDstFrame->linesize _pDstFrame->data, _pDstFrame->linesize
); );
SubmitDstFrame();
}
private void SubmitDstFrame()
{
if (_isQsvEncoder) if (_isQsvEncoder)
{ {
// ── QSV hw_frames_ctx mode: System memory → GPU surface upload ── // ── QSV hw_frames_ctx mode: System memory → GPU surface upload ──
+518
View File
@@ -0,0 +1,518 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using ADOFAIRenderer.UI;
using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using UnityEngine;
using FFmpeg.AutoGen;
namespace ADOFAIRenderer.Util
{
// ReSharper disable once InconsistentNaming
internal class FFmpegVideoBGPlayer : MonoBehaviour
{
private static readonly int YTex = Shader.PropertyToID("_YTex");
private static readonly int UVTex = Shader.PropertyToID("_UVTex");
private static readonly int Result = Shader.PropertyToID("_Result");
private static readonly int Width = Shader.PropertyToID("Width");
private static readonly int Height = Shader.PropertyToID("Height");
private static readonly int UTex = Shader.PropertyToID("_UTex");
private static readonly int VTex = Shader.PropertyToID("_VTex");
private static readonly int ColorRange = Shader.PropertyToID("ColorRange");
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum GPUMode
{
None,
YUV420P,
NV12
}
private FFmpegNativeDecoder? _decoder;
// CPU fallback rendering
// ReSharper disable once InconsistentNaming
private Texture2D? _textureRGBA;
// GPU conversion textures
private Texture2D? _textureY;
private Texture2D? _textureU;
private Texture2D? _textureV;
private Texture2D? _textureUV;
// Compute Shader variables
private ComputeShader? _computeShader;
private RenderTexture? _renderTexture;
private int _kernelIndex = -1;
private double _currentFrameTimestamp;
private double _nextFrameTimestamp;
private GPUMode _gpuMode = GPUMode.None;
private RenderTexture? _targetTexture;
private bool _isLooping;
private bool _isInitialized;
private GameObject? _quad;
private Material? _material;
// ReSharper disable once InconsistentNaming
public void Init(string videoPath, RenderTexture? targetTexture, bool loop, Camera? targetCamera, GameObject videoBGGo)
{
CleanUp();
_targetTexture = targetTexture;
_isLooping = loop;
if (_targetTexture == null && targetCamera != null)
{
SetupBackgroundQuad(targetCamera, videoBGGo);
}
if (!File.Exists(videoPath))
{
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] Video file does not exist: {videoPath}");
return;
}
try
{
_decoder = new FFmpegNativeDecoder(videoPath);
_computeShader = ModAssets.ColorConversionShader;
var w = _decoder.Width;
var h = _decoder.Height;
var pixFmt = _decoder.PixelFormat;
var gpuInitSuccess = false;
if (_computeShader != null)
{
switch (pixFmt)
{
case AVPixelFormat.AV_PIX_FMT_NV12:
try
{
_kernelIndex = _computeShader.FindKernel("CSMain_NV12ToRGBA");
_textureY = new Texture2D(w, h, TextureFormat.R8, false);
_textureUV = new Texture2D(w / 2, h / 2, TextureFormat.RG16, false);
ConfigureTexture(_textureY);
ConfigureTexture(_textureUV);
_gpuMode = GPUMode.NV12;
gpuInitSuccess = true;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader NV12 mode successfully");
}
catch
{
// ignored
}
break;
case AVPixelFormat.AV_PIX_FMT_YUV420P:
case AVPixelFormat.AV_PIX_FMT_YUVJ420P:
try
{
_kernelIndex = _computeShader.FindKernel("CSMain_YUV420PToRGBA");
_textureY = new Texture2D(w, h, TextureFormat.R8, false);
_textureU = new Texture2D(w / 2, h / 2, TextureFormat.R8, false);
_textureV = new Texture2D(w / 2, h / 2, TextureFormat.R8, false);
ConfigureTexture(_textureY);
ConfigureTexture(_textureU);
ConfigureTexture(_textureV);
_gpuMode = GPUMode.YUV420P;
gpuInitSuccess = true;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader YUV420P mode successfully");
}
catch
{
// ignored
}
break;
default:
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Cannot use Compute Shader for colorspace conversion.");
break;
}
}
if (gpuInitSuccess && _computeShader != null)
{
_renderTexture = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32)
{
enableRandomWrite = true
};
_renderTexture.Create();
}
else
{
_textureRGBA = new Texture2D(w, h, TextureFormat.RGBA32, false);
ConfigureTexture(_textureRGBA);
_gpuMode = GPUMode.None;
Melon<RenderMod>.Logger.Warning("[FFmpegVideoBGPlayer] GPU acceleration unsupported or failed to load, falling back to CPU (RGBA).");
}
_currentFrameTimestamp = 0.0;
_nextFrameTimestamp = 0.0;
_isInitialized = true;
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Successfully initialized - Resolution: {w}x{h}, FPS: {_decoder.FPS:F2}");
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during initialization: {ex}");
CleanUp();
}
}
private static void ConfigureTexture(Texture2D tex)
{
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
}
private void LateUpdate()
{
if (!_isInitialized || _decoder == null) return;
var cond = scrConductor.instance;
if (!cond || !cond.hasSongStarted) return;
var pitch = cond.song.pitch;
var separateCountdownOffset = cond.separateCountdownTime ?
cond.crotchetAtStart * cond.adjustedCountdownTicks : 0.0;
var songTime = (cond.dspTime - cond.dspTimeSong) * pitch - cond.addoffset - separateCountdownOffset;
var vidOffset = scrVfxPlus.instance ? scrVfxPlus.instance.vidOffset : 0.0;
var targetVideoTime = songTime + vidOffset;
if (targetVideoTime < 0)
{
if (_targetTexture)
{
var active = RenderTexture.active;
RenderTexture.active = _targetTexture;
GL.Clear(true, true, Color.black);
RenderTexture.active = active;
}
if (_quad)
{
_quad.SetActive(false);
}
return;
}
if (ADOBase.customLevel)
{
ADOBase.customLevel.ShowTutorialBackground(false);
}
try
{
if (targetVideoTime < _currentFrameTimestamp || targetVideoTime - _currentFrameTimestamp > 1.0)
{
_decoder.SeekToTime(targetVideoTime);
_currentFrameTimestamp = targetVideoTime;
_nextFrameTimestamp = targetVideoTime;
}
bool frameUpdated = false;
while (targetVideoTime >= _nextFrameTimestamp)
{
double timestamp = 0.0;
bool success = false;
if (_gpuMode == GPUMode.NV12 && _textureY && _textureUV)
{
success = _decoder.TryReadNV12Frame(
_textureY.GetRawTextureData<byte>(),
_textureUV.GetRawTextureData<byte>(),
out timestamp
);
}
else if (_gpuMode == GPUMode.YUV420P && _textureY && _textureU && _textureV)
{
success = _decoder.TryReadYUV420PFrame(
_textureY.GetRawTextureData<byte>(),
_textureU.GetRawTextureData<byte>(),
_textureV.GetRawTextureData<byte>(),
out timestamp
);
}
else
{
if (_textureRGBA != null)
{
success = _decoder.TryReadFrame(
_textureRGBA.GetRawTextureData<byte>(),
vflip: true,
out timestamp
);
}
}
if (success)
{
_currentFrameTimestamp = timestamp;
_nextFrameTimestamp = timestamp + (1.0 / _decoder.FPS);
frameUpdated = true;
}
else
{
if (_isLooping)
{
_decoder.SeekToTime(0);
_currentFrameTimestamp = 0;
_nextFrameTimestamp = 1.0 / _decoder.FPS;
break;
}
else
{
break;
}
}
}
if (frameUpdated)
{
RenderToTarget();
}
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during playback update: {ex}");
_isInitialized = false;
}
}
private void RenderToTarget()
{
if (_decoder == null) return;
int w = _decoder.Width;
int h = _decoder.Height;
if (_gpuMode == GPUMode.NV12 && _textureY && _textureUV && _computeShader && _renderTexture)
{
_textureY.Apply();
_textureUV.Apply();
_computeShader.SetTexture(_kernelIndex, YTex, _textureY);
_computeShader.SetTexture(_kernelIndex, UVTex, _textureUV);
_computeShader.SetTexture(_kernelIndex, Result, _renderTexture);
_computeShader.SetInt(Width, w);
_computeShader.SetInt(Height, h);
_computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0);
int threadGroupsX = (w + 7) / 8;
int threadGroupsY = (h + 7) / 8;
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
if (_targetTexture)
{
Graphics.Blit(_renderTexture, _targetTexture);
}
else if (_material)
{
if (_material.mainTexture != _renderTexture)
{
_material.mainTexture = _renderTexture;
}
if (_quad != null && !_quad.activeSelf)
{
_quad.SetActive(true);
}
}
}
else if (_gpuMode == GPUMode.YUV420P && _textureY && _textureU && _textureV && _computeShader && _renderTexture)
{
_textureY.Apply();
_textureU.Apply();
_textureV.Apply();
_computeShader.SetTexture(_kernelIndex, YTex, _textureY);
_computeShader.SetTexture(_kernelIndex, UTex, _textureU);
_computeShader.SetTexture(_kernelIndex, VTex, _textureV);
_computeShader.SetTexture(_kernelIndex, Result, _renderTexture);
_computeShader.SetInt(Width, w);
_computeShader.SetInt(Height, h);
_computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0);
int threadGroupsX = (w + 7) / 8;
int threadGroupsY = (h + 7) / 8;
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
if (_targetTexture)
{
Graphics.Blit(_renderTexture, _targetTexture);
}
else if (_material)
{
if (_material.mainTexture != _renderTexture)
{
_material.mainTexture = _renderTexture;
}
if (_quad != null && !_quad.activeSelf)
{
_quad.SetActive(true);
}
}
}
else if (_textureRGBA)
{
_textureRGBA.Apply();
if (_targetTexture)
{
Graphics.Blit(_textureRGBA, _targetTexture);
}
else if (_material)
{
if (_material.mainTexture != _textureRGBA)
{
_material.mainTexture = _textureRGBA;
}
if (_quad != null && !_quad.activeSelf)
{
_quad.SetActive(true);
}
}
}
}
private void OnDestroy()
{
CleanUp();
}
public void CleanUp()
{
_isInitialized = false;
if (_decoder != null)
{
_decoder.Dispose();
_decoder = null;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Decoder successfully disposed.");
}
if (_textureRGBA != null)
{
Destroy(_textureRGBA);
_textureRGBA = null;
}
if (_textureY != null) Destroy(_textureY);
if (_textureU != null) Destroy(_textureU);
if (_textureV != null) Destroy(_textureV);
if (_textureUV != null) Destroy(_textureUV);
_textureY = null;
_textureU = null;
_textureV = null;
_textureUV = null;
if (_renderTexture != null)
{
_renderTexture.Release();
Destroy(_renderTexture);
_renderTexture = null;
}
if (_quad != null)
{
Destroy(_quad);
_quad = null;
}
if (_material != null)
{
Destroy(_material);
_material = null;
}
}
// ReSharper disable once InconsistentNaming
private void SetupBackgroundQuad(Camera targetCamera, GameObject videoBGGo)
{
_quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
_quad.name = "FFmpegVideoBGQuad";
// ReSharper disable once Unity.UnresolvedComponentOrScriptableObject
var col = _quad.GetComponent("MeshCollider");
if (col) Destroy(col);
_quad.transform.parent = targetCamera.transform;
var zPos = targetCamera.farClipPlane - 0.5f;
_quad.transform.localPosition = new Vector3(0f, 0f, zPos);
_quad.transform.localRotation = Quaternion.identity;
float height;
float width;
if (targetCamera.orthographic)
{
height = 2.0f * targetCamera.orthographicSize;
width = height * targetCamera.aspect;
}
else
{
height = 2.0f * zPos * Mathf.Tan(targetCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
width = height * targetCamera.aspect;
}
_quad.transform.localScale = new Vector3(width, height, 1.0f);
_quad.layer = videoBGGo.layer;
// Set vertex colors to white so Sprites/Default works properly (otherwise it may draw transparently)
var meshFilter = _quad.GetComponent<MeshFilter>();
if (meshFilter != null && meshFilter.mesh != null)
{
var mesh = meshFilter.mesh;
var colors = new Color[mesh.vertexCount];
for (var i = 0; i < colors.Length; i++)
{
colors[i] = Color.white;
}
mesh.colors = colors;
}
Shader? shader = null;
if (scrCamera.instance != null)
{
try
{
var camQuadMeshField = typeof(scrCamera).GetField("camQuadMesh", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (camQuadMeshField != null)
{
var renderer = camQuadMeshField.GetValue(scrCamera.instance) as MeshRenderer;
if (renderer != null && renderer.sharedMaterial != null)
{
shader = renderer.sharedMaterial.shader;
}
}
}
catch (Exception)
{
// ignored
}
}
if (shader == null) shader = Shader.Find("Sprites/Default");
if (shader == null) shader = Shader.Find("Unlit/Texture");
if (shader == null) shader = Shader.Find("Standard");
_material = new Material(shader)
{
color = Color.white
};
_quad.GetComponent<MeshRenderer>().material = _material;
// Set inactive initially to prevent white flash
_quad.SetActive(false);
}
}
}
+7 -7
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -10,8 +9,6 @@ using MelonLoader;
namespace ADOFAIRenderer.Util; namespace ADOFAIRenderer.Util;
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Local")]
internal static class FolderDialogUtil internal static class FolderDialogUtil
{ {
private static int _isDialogOpen; private static int _isDialogOpen;
@@ -190,12 +187,14 @@ internal static class FolderDialogUtil
private static string? OpenWindowsFolderBrowserModern(string initialDirectory, IntPtr ownerHandle) private static string? OpenWindowsFolderBrowserModern(string initialDirectory, IntPtr ownerHandle)
{ {
IFileOpenDialog? dialog = null;
// 1. Try native CoCreateInstance first (works on Mono and .NET without class registration) // 1. Try native CoCreateInstance first (works on Mono and .NET without class registration)
var clsid = CLSID_FileOpenDialog; var clsid = CLSID_FileOpenDialog;
var iid = IID_IFileOpenDialog; var iid = IID_IFileOpenDialog;
int createHr = CoCreateInstance(ref clsid, IntPtr.Zero, 1 /* CLSCTX_INPROC_SERVER */, ref iid, out var dialog); int createHr = CoCreateInstance(ref clsid, IntPtr.Zero, 1 /* CLSCTX_INPROC_SERVER */, ref iid, out dialog);
if (createHr != 0) if (createHr != 0 || dialog == null)
{ {
// 2. Fallback to Activator via Type.GetTypeFromCLSID // 2. Fallback to Activator via Type.GetTypeFromCLSID
var comType = Type.GetTypeFromCLSID(CLSID_FileOpenDialog); var comType = Type.GetTypeFromCLSID(CLSID_FileOpenDialog);
@@ -225,7 +224,7 @@ internal static class FolderDialogUtil
{ {
var shellItemGuid = IID_IShellItem; var shellItemGuid = IID_IShellItem;
int itemHr = SHCreateItemFromParsingName(normalized, IntPtr.Zero, ref shellItemGuid, out var initialItem); int itemHr = SHCreateItemFromParsingName(normalized, IntPtr.Zero, ref shellItemGuid, out var initialItem);
if (itemHr == 0) if (itemHr == 0 && initialItem != null)
{ {
try try
{ {
@@ -264,6 +263,7 @@ internal static class FolderDialogUtil
} }
dialog.GetResult(out var resultItem); dialog.GetResult(out var resultItem);
if (resultItem == null) return null;
try try
{ {
@@ -319,7 +319,7 @@ internal static class FolderDialogUtil
if (Directory.Exists(normalized)) if (Directory.Exists(normalized))
{ {
initialDirPtr = Marshal.StringToHGlobalUni(normalized); initialDirPtr = Marshal.StringToHGlobalUni(normalized);
callback = (hwnd, uMsg, _, lpData) => callback = (hwnd, uMsg, lParam, lpData) =>
{ {
if (uMsg == BFFM_INITIALIZED && lpData != IntPtr.Zero) if (uMsg == BFFM_INITIALIZED && lpData != IntPtr.Zero)
{ {
+58 -40
View File
@@ -71,55 +71,72 @@ namespace ADOFAIRenderer.Util
state.IsInFlight = true; state.IsInFlight = true;
} }
// Check if GPU color conversion is available // Check if GPU color conversion is enabled
var expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; bool useGPUConversion = ModAssets.ColorConversionShader;
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
var kernelIndex = -1;
if (ModAssets.ColorConversionShader) if (useGPUConversion)
{ {
var kernalName = _targetFormat switch expectedFmt = _targetFormat;
// Compute Shader only supports NV12 and YUV420P; fallback to CPU for other formats
if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P)
{ {
AVPixelFormat.AV_PIX_FMT_NV12 => "CSMain_NV12", useGPUConversion = false;
AVPixelFormat.AV_PIX_FMT_YUV420P => "CSMain_YUV420P", }
_ => null
};
if (kernalName != null)
kernelIndex = ModAssets.ColorConversionShader.FindKernel(kernalName);
} }
var useGPUConversion = kernelIndex >= 0;
if (useGPUConversion) expectedFmt = _targetFormat;
var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel
state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height); state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height);
if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader) if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader)
{ {
ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture); int kernelIndex = -1;
ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer); if (expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12)
ModAssets.ColorConversionShader.SetInt(Width, texture.width); {
ModAssets.ColorConversionShader.SetInt(Height, texture.height); kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_NV12");
}
else if (expectedFmt == AVPixelFormat.AV_PIX_FMT_YUV420P)
{
kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_YUV420P");
}
var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12 if (kernelIndex >= 0)
? (texture.width + 31) / 32 {
: (texture.width + 63) / 64; ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture);
var threadGroupsY = (texture.height + 15) / 16; ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer);
ModAssets.ColorConversionShader.SetInt(Width, texture.width);
ModAssets.ColorConversionShader.SetInt(Height, texture.height);
ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1); var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12
? (texture.width + 31) / 32
: (texture.width + 63) / 64;
var threadGroupsY = (texture.height + 15) / 16;
AsyncGPUReadback.RequestIntoNativeArray( ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1);
ref state.NativeBuffer, state.GPUBuffer, state.CachedCallback
); AsyncGPUReadback.Request(
/*AsyncGPUReadback.Request( state.GPUBuffer,
state.GPUBuffer, state.CachedCallback
state.CachedCallback );
);*/ }
else
{
// If kernel is not found, fallback to standard RGBA texture readback
AsyncGPUReadback.RequestIntoNativeArray(
ref state.NativeBuffer,
texture,
0,
state.CachedCallback
);
}
} }
else else
{ {
AsyncGPUReadback.RequestIntoNativeArray( AsyncGPUReadback.RequestIntoNativeArray(
ref state.NativeBuffer, texture, 0, state.CachedCallback ref state.NativeBuffer,
texture,
0,
state.CachedCallback
); );
} }
} }
@@ -152,14 +169,16 @@ 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.Width * state.Height * 3 / 2; 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);
var capFrame = new RenderManager.CapturedFrame var capFrame = new RenderManager.CapturedFrame
{ {
ManagedBuffer = blackFrame, Length = byteSize, ManagedBuffer = blackFrame,
IsColorConverted = true Length = byteSize
}; };
PushToQueue(frame, capFrame); PushToQueue(frame, capFrame);
RecycleState(state); RecycleState(state);
@@ -168,20 +187,19 @@ namespace ADOFAIRenderer.Util
{ {
unsafe unsafe
{ {
/*
if (state.IsGPUConversion) if (state.IsGPUConversion)
{ {
var data = request.GetData<byte>(); var data = request.GetData<byte>();
// Copy async request buffer data to persistent NativeBuffer memory // Copy async request buffer data to persistent NativeBuffer memory
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length); NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
} }
*/
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr(); var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
var capFrame = new RenderManager.CapturedFrame var capFrame = new RenderManager.CapturedFrame
{ {
NativePointer = ptr, Length = state.NativeBuffer.Length, NativePointer = ptr,
ReadbackState = state, IsColorConverted = state.IsGPUConversion Length = state.NativeBuffer.Length,
ReadbackState = state
}; };
PushToQueue(frame, capFrame); PushToQueue(frame, capFrame);
} }
-517
View File
@@ -1,517 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using ADOFAIRenderer.UI;
using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using UnityEngine;
using FFmpeg.AutoGen;
namespace ADOFAIRenderer.Util;
// ReSharper disable once InconsistentNaming
internal class VideoBGPlayer : MonoBehaviour
{
private static readonly int YTex = Shader.PropertyToID("_YTex");
private static readonly int UVTex = Shader.PropertyToID("_UVTex");
private static readonly int Result = Shader.PropertyToID("_Result");
private static readonly int Width = Shader.PropertyToID("Width");
private static readonly int Height = Shader.PropertyToID("Height");
private static readonly int UTex = Shader.PropertyToID("_UTex");
private static readonly int VTex = Shader.PropertyToID("_VTex");
private static readonly int ColorRange = Shader.PropertyToID("ColorRange");
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum GPUMode
{
None,
YUV420P,
NV12
}
private FFmpegNativeDecoder? _decoder;
// CPU fallback rendering
// ReSharper disable once InconsistentNaming
private Texture2D? _textureRGBA;
// GPU conversion textures
private Texture2D? _textureY;
private Texture2D? _textureU;
private Texture2D? _textureV;
private Texture2D? _textureUV;
// Compute Shader variables
private ComputeShader? _computeShader;
private RenderTexture? _renderTexture;
private int _kernelIndex = -1;
private double _currentFrameTimestamp;
private double _nextFrameTimestamp;
private GPUMode _gpuMode = GPUMode.None;
private RenderTexture? _targetTexture;
private bool _isLooping;
private bool _isInitialized;
private GameObject? _quad;
private Material? _material;
// ReSharper disable once InconsistentNaming
public void Init(string videoPath, RenderTexture? targetTexture, bool loop, Camera? targetCamera, GameObject videoBGGo)
{
CleanUp();
_targetTexture = targetTexture;
_isLooping = loop;
if (_targetTexture == null && targetCamera != null)
{
SetupBackgroundQuad(targetCamera, videoBGGo);
}
if (!File.Exists(videoPath))
{
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] Video file does not exist: {videoPath}");
return;
}
try
{
_decoder = new FFmpegNativeDecoder(videoPath);
_computeShader = ModAssets.ColorConversionShader;
var w = _decoder.Width;
var h = _decoder.Height;
var pixFmt = _decoder.PixelFormat;
var gpuInitSuccess = false;
if (_computeShader != null)
{
switch (pixFmt)
{
case AVPixelFormat.AV_PIX_FMT_NV12:
try
{
_kernelIndex = _computeShader.FindKernel("CSMain_NV12ToRGBA");
_textureY = new Texture2D(w, h, TextureFormat.R8, false);
_textureUV = new Texture2D(w / 2, h / 2, TextureFormat.RG16, false);
ConfigureTexture(_textureY);
ConfigureTexture(_textureUV);
_gpuMode = GPUMode.NV12;
gpuInitSuccess = true;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader NV12 mode successfully");
}
catch
{
// ignored
}
break;
case AVPixelFormat.AV_PIX_FMT_YUV420P:
case AVPixelFormat.AV_PIX_FMT_YUVJ420P:
try
{
_kernelIndex = _computeShader.FindKernel("CSMain_YUV420PToRGBA");
_textureY = new Texture2D(w, h, TextureFormat.R8, false);
_textureU = new Texture2D(w / 2, h / 2, TextureFormat.R8, false);
_textureV = new Texture2D(w / 2, h / 2, TextureFormat.R8, false);
ConfigureTexture(_textureY);
ConfigureTexture(_textureU);
ConfigureTexture(_textureV);
_gpuMode = GPUMode.YUV420P;
gpuInitSuccess = true;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader YUV420P mode successfully");
}
catch
{
// ignored
}
break;
default:
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Cannot use Compute Shader for colorspace conversion.");
break;
}
}
if (gpuInitSuccess && _computeShader != null)
{
_renderTexture = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32)
{
enableRandomWrite = true
};
_renderTexture.Create();
}
else
{
_textureRGBA = new Texture2D(w, h, TextureFormat.RGBA32, false);
ConfigureTexture(_textureRGBA);
_gpuMode = GPUMode.None;
Melon<RenderMod>.Logger.Warning("[FFmpegVideoBGPlayer] GPU acceleration unsupported or failed to load, falling back to CPU (RGBA).");
}
_currentFrameTimestamp = 0.0;
_nextFrameTimestamp = 0.0;
_isInitialized = true;
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Successfully initialized - Resolution: {w}x{h}, FPS: {_decoder.FPS:F2}");
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during initialization: {ex}");
CleanUp();
}
}
private static void ConfigureTexture(Texture2D tex)
{
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
}
private void LateUpdate()
{
if (!_isInitialized || _decoder == null) return;
var cond = scrConductor.instance;
if (!cond || !cond.hasSongStarted) return;
var pitch = cond.song.pitch;
var separateCountdownOffset = cond.separateCountdownTime ?
cond.crotchetAtStart * cond.adjustedCountdownTicks : 0.0;
var songTime = (cond.dspTime - cond.dspTimeSong) * pitch - cond.addoffset - separateCountdownOffset;
var vidOffset = scrVfxPlus.instance ? scrVfxPlus.instance.vidOffset : 0.0;
var targetVideoTime = songTime + vidOffset;
if (targetVideoTime < 0)
{
if (_targetTexture)
{
var active = RenderTexture.active;
RenderTexture.active = _targetTexture;
GL.Clear(true, true, Color.black);
RenderTexture.active = active;
}
if (_quad)
{
_quad.SetActive(false);
}
return;
}
if (ADOBase.customLevel)
{
ADOBase.customLevel.ShowTutorialBackground(false);
}
try
{
if (targetVideoTime < _currentFrameTimestamp || targetVideoTime - _currentFrameTimestamp > 1.0)
{
_decoder.SeekToTime(targetVideoTime);
_currentFrameTimestamp = targetVideoTime;
_nextFrameTimestamp = targetVideoTime;
}
bool frameUpdated = false;
while (targetVideoTime >= _nextFrameTimestamp)
{
double timestamp = 0.0;
bool success = false;
if (_gpuMode == GPUMode.NV12 && _textureY && _textureUV)
{
success = _decoder.TryReadNV12Frame(
_textureY.GetRawTextureData<byte>(),
_textureUV.GetRawTextureData<byte>(),
out timestamp
);
}
else if (_gpuMode == GPUMode.YUV420P && _textureY && _textureU && _textureV)
{
success = _decoder.TryReadYUV420PFrame(
_textureY.GetRawTextureData<byte>(),
_textureU.GetRawTextureData<byte>(),
_textureV.GetRawTextureData<byte>(),
out timestamp
);
}
else
{
if (_textureRGBA != null)
{
success = _decoder.TryReadFrame(
_textureRGBA.GetRawTextureData<byte>(),
vflip: true,
out timestamp
);
}
}
if (success)
{
_currentFrameTimestamp = timestamp;
_nextFrameTimestamp = timestamp + (1.0 / _decoder.FPS);
frameUpdated = true;
}
else
{
if (_isLooping)
{
_decoder.SeekToTime(0);
_currentFrameTimestamp = 0;
_nextFrameTimestamp = 1.0 / _decoder.FPS;
break;
}
else
{
break;
}
}
}
if (frameUpdated)
{
RenderToTarget();
}
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during playback update: {ex}");
_isInitialized = false;
}
}
private void RenderToTarget()
{
if (_decoder == null) return;
int w = _decoder.Width;
int h = _decoder.Height;
if (_gpuMode == GPUMode.NV12 && _textureY && _textureUV && _computeShader && _renderTexture)
{
_textureY.Apply();
_textureUV.Apply();
_computeShader.SetTexture(_kernelIndex, YTex, _textureY);
_computeShader.SetTexture(_kernelIndex, UVTex, _textureUV);
_computeShader.SetTexture(_kernelIndex, Result, _renderTexture);
_computeShader.SetInt(Width, w);
_computeShader.SetInt(Height, h);
_computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0);
int threadGroupsX = (w + 7) / 8;
int threadGroupsY = (h + 7) / 8;
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
if (_targetTexture)
{
Graphics.Blit(_renderTexture, _targetTexture);
}
else if (_material)
{
if (_material.mainTexture != _renderTexture)
{
_material.mainTexture = _renderTexture;
}
if (_quad != null && !_quad.activeSelf)
{
_quad.SetActive(true);
}
}
}
else if (_gpuMode == GPUMode.YUV420P && _textureY && _textureU && _textureV && _computeShader && _renderTexture)
{
_textureY.Apply();
_textureU.Apply();
_textureV.Apply();
_computeShader.SetTexture(_kernelIndex, YTex, _textureY);
_computeShader.SetTexture(_kernelIndex, UTex, _textureU);
_computeShader.SetTexture(_kernelIndex, VTex, _textureV);
_computeShader.SetTexture(_kernelIndex, Result, _renderTexture);
_computeShader.SetInt(Width, w);
_computeShader.SetInt(Height, h);
_computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0);
int threadGroupsX = (w + 7) / 8;
int threadGroupsY = (h + 7) / 8;
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
if (_targetTexture)
{
Graphics.Blit(_renderTexture, _targetTexture);
}
else if (_material)
{
if (_material.mainTexture != _renderTexture)
{
_material.mainTexture = _renderTexture;
}
if (_quad != null && !_quad.activeSelf)
{
_quad.SetActive(true);
}
}
}
else if (_textureRGBA)
{
_textureRGBA.Apply();
if (_targetTexture)
{
Graphics.Blit(_textureRGBA, _targetTexture);
}
else if (_material)
{
if (_material.mainTexture != _textureRGBA)
{
_material.mainTexture = _textureRGBA;
}
if (_quad != null && !_quad.activeSelf)
{
_quad.SetActive(true);
}
}
}
}
private void OnDestroy()
{
CleanUp();
}
public void CleanUp()
{
_isInitialized = false;
if (_decoder != null)
{
_decoder.Dispose();
_decoder = null;
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Decoder successfully disposed.");
}
if (_textureRGBA != null)
{
Destroy(_textureRGBA);
_textureRGBA = null;
}
if (_textureY != null) Destroy(_textureY);
if (_textureU != null) Destroy(_textureU);
if (_textureV != null) Destroy(_textureV);
if (_textureUV != null) Destroy(_textureUV);
_textureY = null;
_textureU = null;
_textureV = null;
_textureUV = null;
if (_renderTexture != null)
{
_renderTexture.Release();
Destroy(_renderTexture);
_renderTexture = null;
}
if (_quad != null)
{
Destroy(_quad);
_quad = null;
}
if (_material != null)
{
Destroy(_material);
_material = null;
}
}
// ReSharper disable once InconsistentNaming
private void SetupBackgroundQuad(Camera targetCamera, GameObject videoBGGo)
{
_quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
_quad.name = "FFmpegVideoBGQuad";
// ReSharper disable once Unity.UnresolvedComponentOrScriptableObject
var col = _quad.GetComponent("MeshCollider");
if (col) Destroy(col);
_quad.transform.parent = targetCamera.transform;
var zPos = targetCamera.farClipPlane - 0.5f;
_quad.transform.localPosition = new Vector3(0f, 0f, zPos);
_quad.transform.localRotation = Quaternion.identity;
float height;
float width;
if (targetCamera.orthographic)
{
height = 2.0f * targetCamera.orthographicSize;
width = height * targetCamera.aspect;
}
else
{
height = 2.0f * zPos * Mathf.Tan(targetCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
width = height * targetCamera.aspect;
}
_quad.transform.localScale = new Vector3(width, height, 1.0f);
_quad.layer = videoBGGo.layer;
// Set vertex colors to white so Sprites/Default works properly (otherwise it may draw transparently)
var meshFilter = _quad.GetComponent<MeshFilter>();
if (meshFilter != null && meshFilter.mesh != null)
{
var mesh = meshFilter.mesh;
var colors = new Color[mesh.vertexCount];
for (var i = 0; i < colors.Length; i++)
{
colors[i] = Color.white;
}
mesh.colors = colors;
}
Shader? shader = null;
if (scrCamera.instance != null)
{
try
{
var camQuadMeshField = typeof(scrCamera).GetField("camQuadMesh", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (camQuadMeshField != null)
{
var renderer = camQuadMeshField.GetValue(scrCamera.instance) as MeshRenderer;
if (renderer != null && renderer.sharedMaterial != null)
{
shader = renderer.sharedMaterial.shader;
}
}
}
catch (Exception)
{
// ignored
}
}
if (shader == null) shader = Shader.Find("Sprites/Default");
if (shader == null) shader = Shader.Find("Unlit/Texture");
if (shader == null) shader = Shader.Find("Standard");
_material = new Material(shader)
{
color = Color.white
};
_quad.GetComponent<MeshRenderer>().material = _material;
// Set inactive initially to prevent white flash
_quad.SetActive(false);
}
}