SHA256
Compare commits
11
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
daed6adcd7
|
||
|
|
86c8a87a0f
|
||
|
|
892f1ca7b9
|
||
|
|
e7ef081e55
|
||
|
|
a523efd4fc
|
||
|
|
b43c8be5dc
|
||
|
|
671bdda9a1
|
||
|
|
8567878f07
|
||
|
|
b900a5444e
|
||
|
|
2d70fb5ae8
|
||
|
|
0d9cf44c77
|
No files matched your search
@@ -82,6 +82,9 @@
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UniTask">
|
||||
<HintPath>$(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UniTask.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@ using MelonLoader;
|
||||
using MelonLoader.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ADOFAIRenderer.Data;
|
||||
namespace ADOFAIRenderer.Core.Data;
|
||||
|
||||
public class GlobalConfig
|
||||
{
|
||||
+10
-7
@@ -1,5 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ADOFAIRenderer.Core;
|
||||
internal class PostRenderHook : MonoBehaviour
|
||||
{
|
||||
private FieldInfo? _camQuadMeshField;
|
||||
private MeshRenderer? _camQuadMeshRenderer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -20,15 +21,17 @@ internal class PostRenderHook : MonoBehaviour
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.OnLastUpdate();
|
||||
|
||||
StartCoroutine(PostFrameCoroutine());
|
||||
PostFrameAsync().Forget();
|
||||
}
|
||||
|
||||
private IEnumerator PostFrameCoroutine()
|
||||
private async UniTaskVoid PostFrameAsync()
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
await UniTask.WaitForEndOfFrame(this, destroyCancellationToken);
|
||||
|
||||
var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance);
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.OnPostFrame(val.sharedMaterial.mainTexture as RenderTexture);
|
||||
if (!_camQuadMeshRenderer && scrCamera.instance)
|
||||
_camQuadMeshRenderer = _camQuadMeshField?.GetValue(scrCamera.instance) as MeshRenderer;
|
||||
|
||||
if (RenderManager.Instance && _camQuadMeshRenderer)
|
||||
RenderManager.Instance.OnPostFrame(_camQuadMeshRenderer.sharedMaterial.mainTexture as RenderTexture);
|
||||
}
|
||||
}
|
||||
@@ -177,9 +177,9 @@ internal static class PresetManager
|
||||
|
||||
public static void DeletePreset(string name)
|
||||
{
|
||||
string dir = GetPresetsDirectory();
|
||||
string safeName = GetSafeFileName(name);
|
||||
string filePath = Path.Combine(dir, safeName + ".json");
|
||||
var dir = GetPresetsDirectory();
|
||||
var safeName = GetSafeFileName(name);
|
||||
var filePath = Path.Combine(dir, safeName + ".json");
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
@@ -7,6 +7,7 @@ using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using ADOFAIRenderer.API;
|
||||
using ADOFAIRenderer.Core.Data;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.UI;
|
||||
using ADOFAIRenderer.UI.Window;
|
||||
@@ -116,6 +117,7 @@ internal class RenderManager : MonoBehaviour
|
||||
public int Length;
|
||||
public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame)
|
||||
public ReadbackRequestState? ReadbackState; // Associated state to recycle
|
||||
public bool IsColorConverted;
|
||||
}
|
||||
|
||||
// Thread and Queue State
|
||||
@@ -545,7 +547,7 @@ internal class RenderManager : MonoBehaviour
|
||||
{
|
||||
if (frame.ManagedBuffer.Length > 0)
|
||||
{
|
||||
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length);
|
||||
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted);
|
||||
Interlocked.Increment(ref framesSent);
|
||||
|
||||
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
|
||||
@@ -553,7 +555,7 @@ internal class RenderManager : MonoBehaviour
|
||||
}
|
||||
else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0)
|
||||
{
|
||||
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length);
|
||||
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted);
|
||||
Interlocked.Increment(ref framesSent);
|
||||
|
||||
if (frame.ReadbackState != null)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FFmpeg.AutoGen;
|
||||
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum Codec
|
||||
{
|
||||
H264, H265, VP9, AV1, VVC
|
||||
}
|
||||
|
||||
internal static class CodecUtil
|
||||
{
|
||||
public static readonly Dictionary<Codec, AVCodecID> AudioCodecMatch = new()
|
||||
{
|
||||
[Codec.H264] = AVCodecID.AV_CODEC_ID_AAC,
|
||||
[Codec.H265] = AVCodecID.AV_CODEC_ID_AAC,
|
||||
[Codec.VP9] = AVCodecID.AV_CODEC_ID_OPUS,
|
||||
[Codec.AV1] = AVCodecID.AV_CODEC_ID_OPUS,
|
||||
[Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS
|
||||
};
|
||||
|
||||
public static readonly Dictionary<AVCodecID, List<int>> AvailableSampleRate = new()
|
||||
{
|
||||
[AVCodecID.AV_CODEC_ID_AAC] = new List<int> { 44100, 48000, 96000 },
|
||||
[AVCodecID.AV_CODEC_ID_OPUS] = new List<int> { 48000 }
|
||||
};
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for double input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// </summary>
|
||||
internal abstract class DoubleLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the given double value against this limit.
|
||||
/// </summary>
|
||||
/// <param name="value">The parsed double value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(double value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyDoubleLimit : DoubleLimit
|
||||
{
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
{
|
||||
if (value > 0)
|
||||
{
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "Value must be a positive number greater than 0.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeDoubleLimit : DoubleLimit
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public double? Min { get; }
|
||||
public double? Max { get; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
public RangeDoubleLimit(double? min, double? max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
{
|
||||
if (value < Min)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"Value must be a number between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be a number greater than or equal to {Min.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value > Max)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"Value must be a number between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be a number less than or equal to {Max.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MelonLoader;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace ADOFAIRenderer.Data.Input;
|
||||
|
||||
internal class DoubleInputValidator
|
||||
{
|
||||
private struct FieldEntry
|
||||
{
|
||||
public List<DoubleLimit> Limits;
|
||||
}
|
||||
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
|
||||
public DoubleInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="DoubleLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits)
|
||||
{
|
||||
if (!_fields.TryGetValue(fieldName, out var entry))
|
||||
{
|
||||
entry = new FieldEntry
|
||||
{
|
||||
Limits = new List<DoubleLimit>()
|
||||
};
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
{
|
||||
var field = _rootVisualElement.Q<DoubleField>(fieldName);
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"DoubleField '{fieldName}' not found. Skipping validation.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var limits = entry.Limits;
|
||||
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||
{
|
||||
field.SetValueWithoutNotify(evt.previousValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MelonLoader;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace ADOFAIRenderer.Data.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Binds a UI Toolkit <see cref="DropdownField"/> to an enum type.
|
||||
/// Populates choices from enum names and provides typed access to the selected value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type to bind.</typeparam>
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
internal class EnumDropdownBinder<T> where T : struct, Enum
|
||||
{
|
||||
private readonly DropdownField? _dropdown;
|
||||
private readonly List<T> _values;
|
||||
private Action<T>? _onValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected enum value.
|
||||
/// </summary>
|
||||
public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count
|
||||
? _values[_dropdown.index]
|
||||
: default;
|
||||
|
||||
/// <summary>
|
||||
/// Queries a <see cref="DropdownField"/> by name from the root VisualElement,
|
||||
/// populates it with enum names, and selects the default value.
|
||||
/// </summary>
|
||||
/// <param name="rootVisualElement">The root VisualElement loaded from the UXML bundle.</param>
|
||||
/// <param name="fieldName">The name of the DropdownField in the UXML.</param>
|
||||
/// <param name="defaultValue">The initial selected value. Defaults to the first enum member.</param>
|
||||
public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null)
|
||||
{
|
||||
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
||||
|
||||
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
||||
if (_dropdown == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error(
|
||||
$"DropdownField '{fieldName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
_dropdown.choices = _values.Select(v => v.ToString()).ToList();
|
||||
|
||||
// Register the callback BEFORE setting index so the initial selection fires it too
|
||||
_dropdown.RegisterValueChangedCallback(_ =>
|
||||
{
|
||||
_onValueChanged?.Invoke(Value);
|
||||
});
|
||||
|
||||
var initial = defaultValue ?? _values[0];
|
||||
_dropdown.index = _values.IndexOf(initial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback that fires when the selection changes, providing the new enum value.
|
||||
/// </summary>
|
||||
public void OnValueChanged(Action<T> callback)
|
||||
{
|
||||
_onValueChanged = callback;
|
||||
|
||||
// Fire immediately with current value so the caller gets the initial state
|
||||
if (_dropdown != null)
|
||||
{
|
||||
callback(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected enum value and updates the dropdown index.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value to set.</param>
|
||||
public void SetValue(T value)
|
||||
{
|
||||
if (_dropdown == null) return;
|
||||
int idx = _values.IndexOf(value);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_dropdown.index = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MelonLoader;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace ADOFAIRenderer.Data.Input;
|
||||
|
||||
internal class IntegerInputValidator
|
||||
{
|
||||
private struct FieldEntry
|
||||
{
|
||||
public List<IntegerLimit> Limits;
|
||||
}
|
||||
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
|
||||
public IntegerInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="IntegerLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits)
|
||||
{
|
||||
if (!_fields.TryGetValue(fieldName, out var entry))
|
||||
{
|
||||
entry = new FieldEntry
|
||||
{
|
||||
Limits = new List<IntegerLimit>()
|
||||
};
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
{
|
||||
var field = _rootVisualElement.Q<IntegerField>(fieldName);
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"IntegerField '{fieldName}' not found. Skipping validation.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var limits = entry.Limits;
|
||||
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||
{
|
||||
field.SetValueWithoutNotify(evt.previousValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for integer input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// </summary>
|
||||
internal abstract class IntegerLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the given integer value against this limit.
|
||||
/// </summary>
|
||||
/// <param name="value">The parsed integer value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(int value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyIntegerLimit : IntegerLimit
|
||||
{
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
{
|
||||
if (value > 0)
|
||||
{
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "Value must be a positive integer greater than 0.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeIntegerLimit : IntegerLimit
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public int? Min { get; }
|
||||
public int? Max { get; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
public RangeIntegerLimit(int? min, int? max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
{
|
||||
if (value < Min)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"Value must be an integer between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be an integer greater than or equal to {Min.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value > Max)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"Value must be an integer between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be an integer less than or equal to {Max.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum RateControlMode
|
||||
{
|
||||
CBR,
|
||||
VBR,
|
||||
CQP
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ADOFAIRenderer.Util.FFmpeg;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# ADOFAIRenderer API & Data Linking Exception (Dynamic Linking Only)
|
||||
|
||||
> **Additional Permission under GNU General Public License version 3 section 7**
|
||||
|
||||
---
|
||||
|
||||
## 1. Legal Terms (Official)
|
||||
|
||||
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:
|
||||
|
||||
- `ADOFAIRenderer.API`
|
||||
- `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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
In accordance with section 7 of the GNU GPL version 3, if you modify this Program, you may extend this exception to your version of the Program, but you are not obligated to do so. If you do not wish to provide this exception in your derivative work, you may remove this exception statement from your version.
|
||||
|
||||
---
|
||||
|
||||
## 2. 조항 해설 (Korean Guide)
|
||||
|
||||
본 문서는 GNU General Public License 버전 3 (GPLv3) 제7조(추가적인 허가, Additional Permissions)에 따라 제공되는 공식 예외 조항입니다.
|
||||
|
||||
### 핵심 요약
|
||||
|
||||
- **동적 링킹(Dynamic Linking)만 허용**:
|
||||
- 본 예외 조항은 **동적 링킹(런타임 참조 및 어셈블리 동적 로드)**에만 배타적으로 적용됩니다.
|
||||
- ADOFAIRenderer 또는 그 일부 코드를 대상 모드/프로그램에 직접 병합하거나 포함(ILMerge, Costura, 바이너리 직접 내장/정적 컴파일 등)하는 **정적 링킹(Static Linking)은 본 예외 대상에서 제외**되며, 순수 GPLv3의 카피레프트(전염성) 의무가 그대로 적용됩니다.
|
||||
- **외부 모드의 라이선스 자유**:
|
||||
- 제3자 모드 또는 외부 프로그램이 동적 링킹을 통해 `ADOFAIRenderer.API` 및 `ADOFAIRenderer.Data` 네임스페이스의 공개 API, 클래스, 데이터 구조만을 참조·호출하여 연동하는 경우, 해당 모드는 **GPLv3의 카피레프트 의무를 적용받지 않습니다.**
|
||||
- 외부 모드 개발자는 자신의 모드를 MIT, Apache-2.0, 독점/상용 라이선스 등 원하는 라이선스로 자유롭게 배포할 수 있습니다.
|
||||
- **ADOFAIRenderer 코어 보호**:
|
||||
- ADOFAIRenderer 자체의 소스 코드(렌더링 파이프라인, 코어 패치, 내부 유틸리티 등)를 수정하거나 재배포하는 경우에는 본 예외 조항이 적용되지 않으며, 순수 GPLv3 라이선스 조건이 그대로 적용됩니다.
|
||||
|
||||
@@ -40,7 +40,6 @@ public static class EndscreenLanternsPatch
|
||||
for (var i = 0; i < lampSequencesArray.Length; i++)
|
||||
{
|
||||
var item = lampSequencesArray.GetValue(i);
|
||||
if (item == null) continue;
|
||||
try
|
||||
{
|
||||
var killMethod = item.GetType().GetMethod("Kill", new[] { typeof(bool) });
|
||||
|
||||
@@ -77,11 +77,9 @@ public static class scnGamePatch
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
// Remove FFmpeg player if it was attached previously
|
||||
var oldPlayer = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
|
||||
if (oldPlayer != null)
|
||||
{
|
||||
Object.Destroy(oldPlayer);
|
||||
}
|
||||
var oldPlayer = __instance.videoBG.gameObject.GetComponent<VideoBGPlayer>();
|
||||
if (oldPlayer) Object.Destroy(oldPlayer);
|
||||
|
||||
// Re-enable native player component
|
||||
__instance.videoBG.enabled = true;
|
||||
return true; // Execute original
|
||||
@@ -125,10 +123,9 @@ public static class scnGamePatch
|
||||
__instance.videoBG.enabled = false;
|
||||
|
||||
// Add or retrieve our custom FFmpeg video background player
|
||||
var player = __instance.videoBG.gameObject.GetComponent<FFmpegVideoBGPlayer>();
|
||||
if (player == null)
|
||||
if (!__instance.videoBG.gameObject.TryGetComponent<VideoBGPlayer>(out var player))
|
||||
{
|
||||
player = __instance.videoBG.gameObject.AddComponent<FFmpegVideoBGPlayer>();
|
||||
player = __instance.videoBG.gameObject.AddComponent<VideoBGPlayer>();
|
||||
}
|
||||
|
||||
var isLooping = __instance.levelData.miscSettings.GetBool("loopVideo");
|
||||
|
||||
@@ -90,4 +90,6 @@ The compiled mod will be output to `bin/Release/netstandard2.1/ADOFAIRenderer.dl
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 (GPLv3). See `LICENSE` for details.
|
||||
This project is licensed under the **GNU General Public License v3.0 with API & Data Linking Exception (GPLv3 with Exception, Dynamic Linking Only)**.
|
||||
- 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.)
|
||||
- For full license terms and exception details, see [LICENSE](LICENSE) and [LICENSE-EXCEPTION.md](LICENSE-EXCEPTION.md).
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using ADOFAIRenderer;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Core.Data;
|
||||
using ADOFAIRenderer.UI;
|
||||
using FFmpeg.AutoGen;
|
||||
using MelonLoader;
|
||||
@@ -9,7 +9,7 @@ using MelonLoader.Utils;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.1-INDEV", "ADOFAI Editor Forum")]
|
||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.2-INDEV", "ADOFAI Editor Forum")]
|
||||
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
||||
|
||||
namespace ADOFAIRenderer;
|
||||
|
||||
+1
-41
@@ -85,46 +85,6 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private static TMP_FontAsset GetNanumSquareNeoBdFontAsset()
|
||||
{
|
||||
TMP_FontAsset? font = null;
|
||||
if (ModAssets.FontBundle)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = ModAssets.FontBundle.LoadAsset<TMP_FontAsset>("NanumSquareNeoOTF-Bd SDF");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Loading failed
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback processing
|
||||
if (font == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = RDString.fontData.fontTMP;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
if (font == null)
|
||||
{
|
||||
// ReSharper disable once Unity.UnknownResource
|
||||
font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||
}
|
||||
|
||||
if (font) return font;
|
||||
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
||||
if (fonts.Length > 0) font = fonts[0];
|
||||
|
||||
return font!;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Set UI layer (5)
|
||||
@@ -159,7 +119,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
bgRect.offsetMin = Vector2.zero;
|
||||
bgRect.offsetMax = Vector2.zero;
|
||||
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
var font = ModAssets.NanumSquareNeoNormal;
|
||||
|
||||
// 2. Create modal container
|
||||
var modalContainerObj = new GameObject("ModalContainer");
|
||||
|
||||
+3
-18
@@ -50,20 +50,7 @@ internal sealed class RendererFontProvider : IFontProvider
|
||||
|
||||
internal class RendererUIManager : MonoBehaviour
|
||||
{
|
||||
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 RendererUIManager Instance = null!;
|
||||
|
||||
public static O5Context Context => Instance._context;
|
||||
public O5WindowManager WindowManager => _windowManager;
|
||||
@@ -72,7 +59,7 @@ internal class RendererUIManager : MonoBehaviour
|
||||
private O5WindowManager _windowManager = null!;
|
||||
|
||||
private void Awake() {
|
||||
_instance = this;
|
||||
Instance = this;
|
||||
|
||||
_context = new O5Context(
|
||||
config: new O5Config {
|
||||
@@ -89,7 +76,7 @@ internal class RendererUIManager : MonoBehaviour
|
||||
ObjectBG = new Color(0f, 0f, 0f, 1f),
|
||||
ObjectButton = new Color(0f, 0f, 0f, 1f),
|
||||
ObjectActive = new Color(1.000f, 0.294f, 0.079f, 1f),
|
||||
ObjectActiveBright = new Color(0.812f, 0.827f, 1.000f, 1f),
|
||||
ObjectActiveBright = new Color(1.000f, 0.294f, 0.079f, 1f),
|
||||
ControlOutlineIdle = new Color(1f, 1f, 1f, 0.4f),
|
||||
ButtonHover = new Color(0.133f, 0.133f, 0.133f, 1.0f),
|
||||
ControlOutlineHover = new Color(1f, 1f, 1f, 0.4f),
|
||||
@@ -118,7 +105,5 @@ internal class RendererUIManager : MonoBehaviour
|
||||
private void OnDestroy() {
|
||||
_windowManager.Dispose();
|
||||
_context.Dispose();
|
||||
|
||||
if (_instance == this) _instance = null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using ADOFAIRenderer.API;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Core.Data;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Localization;
|
||||
using ADOFAIRenderer.Util;
|
||||
@@ -58,9 +59,7 @@ internal static class RenderConfigWindow
|
||||
{
|
||||
Id = "RendererMod_RenderConfig",
|
||||
Title = LocalizationManager.Get("ui.render_config.window_title"),
|
||||
Size = new Vector2(580f, 480f),
|
||||
Draggable = true,
|
||||
Resizable = true,
|
||||
Size = new Vector2(900, 600), Draggable = true, Resizable = true,
|
||||
ShowCloseButton = true
|
||||
});
|
||||
|
||||
@@ -83,7 +82,7 @@ internal static class RenderConfigWindow
|
||||
private static void BuildUI(RectTransform content)
|
||||
{
|
||||
var ctx = RendererUIManager.Context;
|
||||
var (viewport, scrollContent, scroller) =
|
||||
var (_, scrollContent, _) =
|
||||
O5Factory.ScrollView(ctx, content, spacing: 10f, padding: 8f);
|
||||
|
||||
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
||||
@@ -125,7 +124,7 @@ internal static class RenderConfigWindow
|
||||
defaultValue: 60, min: 0.01, max: 3000, value: 60,
|
||||
format: "F3", clampMode: ClampMode.All,
|
||||
|
||||
sliderFilter: value => renderFpsSnap.OrderBy(n => Math.Abs(n - value)).First(),
|
||||
sliderFilter: value => Snap(renderFpsSnap, value),
|
||||
inputFilter: null, onComplete: null, onChanged: val => { config.RenderFPS = val; },
|
||||
text: LocalizationManager.Get("ui.render_config.render_fps"),
|
||||
id: "slider_render_fps", showFill: true
|
||||
@@ -136,7 +135,7 @@ internal static class RenderConfigWindow
|
||||
defaultValue: 60, min: 0.01, max: 600, value: 60,
|
||||
format: "F3", clampMode: ClampMode.All,
|
||||
|
||||
sliderFilter: value => videoFpsSnap.OrderBy(n => Math.Abs(n - value)).First(),
|
||||
sliderFilter: value => Snap(videoFpsSnap, value),
|
||||
inputFilter: null, onComplete: null, onChanged: val => { config.VideoFPS = val; },
|
||||
text: LocalizationManager.Get("ui.render_config.video_fps"),
|
||||
id: "slider_video_fps", showFill: true
|
||||
@@ -147,7 +146,7 @@ internal static class RenderConfigWindow
|
||||
defaultValue: 2560, min: 0, max: 15360, value: 2560,
|
||||
format: "F0", clampMode: ClampMode.All,
|
||||
|
||||
sliderFilter: value => renderWidthSnap.OrderBy(n => Math.Abs(n - value)).First(), inputFilter: value => Math.Max(value, 2),
|
||||
sliderFilter: value => Snap(renderWidthSnap, value), inputFilter: value => Math.Max(value, 2),
|
||||
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); },
|
||||
text: LocalizationManager.Get("ui.render_config.render_width"),
|
||||
id: "slider_render_width", showFill: true
|
||||
@@ -158,8 +157,8 @@ internal static class RenderConfigWindow
|
||||
defaultValue: 1440, min: 0, max: 8640, value: 1440,
|
||||
format: "F0", clampMode: ClampMode.All,
|
||||
|
||||
sliderFilter: value => renderHeightSnap.OrderBy(n => Math.Abs(n - value)).First(), inputFilter: value => Math.Max(value, 2),
|
||||
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); },
|
||||
sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2),
|
||||
onComplete: null, onChanged: val => { config.RenderHeight = (int)Math.Round(val); },
|
||||
text: LocalizationManager.Get("ui.render_config.render_height"),
|
||||
id: "slider_render_height", showFill: true
|
||||
);
|
||||
@@ -292,7 +291,7 @@ internal static class RenderConfigWindow
|
||||
defaultValue: 256, min: 64, max: 384, value: 256,
|
||||
format: "F0", clampMode: ClampMode.All,
|
||||
|
||||
sliderFilter: value => audioBitrateSnap.OrderBy(n => Math.Abs(n - value)).First(),
|
||||
sliderFilter: value => Snap(audioBitrateSnap, value),
|
||||
inputFilter: null, onComplete: null, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; },
|
||||
text: LocalizationManager.Get("ui.render_config.audio_bitrate"),
|
||||
id: "slider_audio_bitrate", showFill: true
|
||||
@@ -464,6 +463,9 @@ internal static class RenderConfigWindow
|
||||
|
||||
return;
|
||||
|
||||
double Snap(double[] array, double value) =>
|
||||
array.OrderBy(n => Math.Abs(n - value)).First();
|
||||
|
||||
void UpdateVideoCodec(Codec codec)
|
||||
{
|
||||
_sampleRateDropdown?.SetValues(CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[codec]]);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FFmpeg.AutoGen;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum Codec
|
||||
{
|
||||
H264, H265, VP9, AV1, VVC
|
||||
}
|
||||
|
||||
internal static class CodecUtil
|
||||
{
|
||||
public static readonly Dictionary<Codec, AVCodecID> AudioCodecMatch = new()
|
||||
{
|
||||
[Codec.H264] = AVCodecID.AV_CODEC_ID_AAC,
|
||||
[Codec.H265] = AVCodecID.AV_CODEC_ID_AAC,
|
||||
[Codec.VP9] = AVCodecID.AV_CODEC_ID_OPUS,
|
||||
[Codec.AV1] = AVCodecID.AV_CODEC_ID_OPUS,
|
||||
[Codec.VVC] = AVCodecID.AV_CODEC_ID_OPUS
|
||||
};
|
||||
|
||||
public static readonly Dictionary<AVCodecID, List<int>> AvailableSampleRate = new()
|
||||
{
|
||||
[AVCodecID.AV_CODEC_ID_AAC] = new List<int> { 44100, 48000, 96000 },
|
||||
[AVCodecID.AV_CODEC_ID_OPUS] = new List<int> { 48000 }
|
||||
};
|
||||
}
|
||||
}
|
||||
+739
-740
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using ADOFAIRenderer.Data;
|
||||
using FFmpeg.AutoGen;
|
||||
using MelonLoader;
|
||||
using Unity.Collections;
|
||||
@@ -973,29 +974,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
/// Encodes a frame from IntPtr (native pointer).
|
||||
/// Can be used from external native plugins or custom capture sources.
|
||||
/// </summary>
|
||||
public void SendDataFromNativeBuffer(IntPtr pointer, int length)
|
||||
public void SendDataFromNativeBuffer(IntPtr pointer, int length, bool isColorConverted)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed || !_isInitialized) return;
|
||||
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
|
||||
|
||||
if (!_loggedConversionMode)
|
||||
{
|
||||
_loggedConversionMode = true;
|
||||
Melon<RenderMod>.Logger.Msg(length == preConvertedSize
|
||||
? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)"
|
||||
Melon<RenderMod>.Logger.Msg(isColorConverted
|
||||
? "[Encoder] GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)"
|
||||
: "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)");
|
||||
}
|
||||
|
||||
if (length == preConvertedSize)
|
||||
{
|
||||
WritePreConvertedFrame((byte*)pointer.ToPointer());
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteFrameFromBuffer((byte*)pointer.ToPointer());
|
||||
}
|
||||
if (isColorConverted) WritePreConvertedFrame((byte*)pointer.ToPointer());
|
||||
else WriteFrameFromBuffer((byte*)pointer.ToPointer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1003,31 +997,23 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
/// Encodes a frame from a managed byte[].
|
||||
/// Pins the array using fixed keyword to prevent GC from moving it before passing the pointer.
|
||||
/// </summary>
|
||||
public void SendDataFromBuffer(byte[] buffer, int length)
|
||||
public void SendDataFromBuffer(byte[] buffer, int length, bool isColorConverted)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed || !_isInitialized) return;
|
||||
fixed (byte* pBuffer = buffer)
|
||||
{
|
||||
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
|
||||
|
||||
if (!_loggedConversionMode)
|
||||
{
|
||||
_loggedConversionMode = true;
|
||||
Melon<RenderMod>.Logger.Msg(length == preConvertedSize
|
||||
Melon<RenderMod>.Logger.Msg(isColorConverted
|
||||
? "[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)");
|
||||
}
|
||||
|
||||
if (length == preConvertedSize)
|
||||
{
|
||||
WritePreConvertedFrame(pBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteFrameFromBuffer(pBuffer);
|
||||
}
|
||||
if (isColorConverted) WritePreConvertedFrame(pBuffer);
|
||||
else WriteFrameFromBuffer(pBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1144,33 +1130,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
throw new NotSupportedException($"This pixel format ({_dstPixFmt}) does not support direct copying of pre-converted buffers.");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
SubmitDstFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1208,6 +1168,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
_pDstFrame->data, _pDstFrame->linesize
|
||||
);
|
||||
|
||||
SubmitDstFrame();
|
||||
}
|
||||
|
||||
private void SubmitDstFrame()
|
||||
{
|
||||
if (_isQsvEncoder)
|
||||
{
|
||||
// ── QSV hw_frames_ctx mode: System memory → GPU surface upload ──
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum RateControlMode
|
||||
{
|
||||
CBR,
|
||||
VBR,
|
||||
CQP
|
||||
}
|
||||
}
|
||||
+106
-106
@@ -2,112 +2,112 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using ADOFAIRenderer.Data;
|
||||
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
namespace ADOFAIRenderer.Util.FFmpeg;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum EncoderKind
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum EncoderKind
|
||||
{
|
||||
Software,
|
||||
NVENC,
|
||||
AMF,
|
||||
QSV,
|
||||
VideoToolbox
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal sealed class VideoEncoder : IEquatable<VideoEncoder>
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
// ── H.264 ──
|
||||
public static readonly VideoEncoder H264_NVENC = new("h264_nvenc", Codec.H264, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder H264_QSV = new("h264_qsv", Codec.H264, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder H264_AMF = new("h264_amf", Codec.H264, GPUVendor.AMD, EncoderKind.AMF);
|
||||
public static readonly VideoEncoder H264_VideoToolbox = new("h264_videotoolbox", Codec.H264, GPUVendor.Apple, EncoderKind.VideoToolbox);
|
||||
public static readonly VideoEncoder Libx264 = new("libx264", Codec.H264, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── H.265 (HEVC) ──
|
||||
public static readonly VideoEncoder HEVC_NVENC = new("hevc_nvenc", Codec.H265, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder HEVC_QSV = new("hevc_qsv", Codec.H265, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder HEVC_AMF = new("hevc_amf", Codec.H265, GPUVendor.AMD, EncoderKind.AMF);
|
||||
public static readonly VideoEncoder HEVC_VideoToolbox = new("hevc_videotoolbox", Codec.H265, GPUVendor.Apple, EncoderKind.VideoToolbox);
|
||||
public static readonly VideoEncoder Libx265 = new("libx265", Codec.H265, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── VP9 ──
|
||||
public static readonly VideoEncoder VP9_NVENC = new("vp9_nvenc", Codec.VP9, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder VP9_QSV = new("vp9_qsv", Codec.VP9, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder LibvpxVP9 = new("libvpx-vp9", Codec.VP9, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── AV1 ──
|
||||
public static readonly VideoEncoder AV1_NVENC = new("av1_nvenc", Codec.AV1, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder AV1_QSV = new("av1_qsv", Codec.AV1, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder AV1_AMF = new("av1_amf", Codec.AV1, GPUVendor.AMD, EncoderKind.AMF);
|
||||
public static readonly VideoEncoder LibSvtAv1 = new("libsvtav1", Codec.AV1, GPUVendor.Unknown, EncoderKind.Software);
|
||||
public static readonly VideoEncoder LibAomAv1 = new("libaom-av1", Codec.AV1, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── VVC ──
|
||||
public static readonly VideoEncoder LibVvenc = new("libvvenc", Codec.VVC, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
public static readonly IReadOnlyList<VideoEncoder> All = new[]
|
||||
{
|
||||
H264_NVENC, H264_QSV, H264_AMF, H264_VideoToolbox, Libx264,
|
||||
HEVC_NVENC, HEVC_QSV, HEVC_AMF, HEVC_VideoToolbox, Libx265,
|
||||
VP9_NVENC, VP9_QSV, LibvpxVP9,
|
||||
AV1_NVENC, AV1_QSV, AV1_AMF, LibSvtAv1, LibAomAv1,
|
||||
LibVvenc
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, VideoEncoder> ByName =
|
||||
All.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public string Name { get; }
|
||||
public Codec Codec { get; }
|
||||
public GPUVendor Vendor { get; }
|
||||
public EncoderKind Kind { get; }
|
||||
public bool IsHardware => Kind != EncoderKind.Software;
|
||||
|
||||
private VideoEncoder(string name, Codec codec, GPUVendor vendor, EncoderKind kind)
|
||||
{
|
||||
Name = name;
|
||||
Codec = codec;
|
||||
Vendor = vendor;
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public static VideoEncoder? FromName(string name)
|
||||
{
|
||||
return string.IsNullOrEmpty(name) ? null : ByName.GetValueOrDefault(name);
|
||||
}
|
||||
|
||||
public bool Equals(VideoEncoder other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
return ReferenceEquals(this, other) || string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is VideoEncoder other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
|
||||
}
|
||||
|
||||
public static bool operator ==(VideoEncoder left, VideoEncoder right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(VideoEncoder left, VideoEncoder right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
Software,
|
||||
NVENC,
|
||||
AMF,
|
||||
QSV,
|
||||
VideoToolbox
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal sealed class VideoEncoder : IEquatable<VideoEncoder>
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
// ── H.264 ──
|
||||
public static readonly VideoEncoder H264_NVENC = new("h264_nvenc", Codec.H264, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder H264_QSV = new("h264_qsv", Codec.H264, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder H264_AMF = new("h264_amf", Codec.H264, GPUVendor.AMD, EncoderKind.AMF);
|
||||
public static readonly VideoEncoder H264_VideoToolbox = new("h264_videotoolbox", Codec.H264, GPUVendor.Apple, EncoderKind.VideoToolbox);
|
||||
public static readonly VideoEncoder Libx264 = new("libx264", Codec.H264, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── H.265 (HEVC) ──
|
||||
public static readonly VideoEncoder HEVC_NVENC = new("hevc_nvenc", Codec.H265, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder HEVC_QSV = new("hevc_qsv", Codec.H265, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder HEVC_AMF = new("hevc_amf", Codec.H265, GPUVendor.AMD, EncoderKind.AMF);
|
||||
public static readonly VideoEncoder HEVC_VideoToolbox = new("hevc_videotoolbox", Codec.H265, GPUVendor.Apple, EncoderKind.VideoToolbox);
|
||||
public static readonly VideoEncoder Libx265 = new("libx265", Codec.H265, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── VP9 ──
|
||||
public static readonly VideoEncoder VP9_NVENC = new("vp9_nvenc", Codec.VP9, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder VP9_QSV = new("vp9_qsv", Codec.VP9, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder LibvpxVP9 = new("libvpx-vp9", Codec.VP9, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── AV1 ──
|
||||
public static readonly VideoEncoder AV1_NVENC = new("av1_nvenc", Codec.AV1, GPUVendor.NVIDIA, EncoderKind.NVENC);
|
||||
public static readonly VideoEncoder AV1_QSV = new("av1_qsv", Codec.AV1, GPUVendor.Intel, EncoderKind.QSV);
|
||||
public static readonly VideoEncoder AV1_AMF = new("av1_amf", Codec.AV1, GPUVendor.AMD, EncoderKind.AMF);
|
||||
public static readonly VideoEncoder LibSvtAv1 = new("libsvtav1", Codec.AV1, GPUVendor.Unknown, EncoderKind.Software);
|
||||
public static readonly VideoEncoder LibAomAv1 = new("libaom-av1", Codec.AV1, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ── VVC ──
|
||||
public static readonly VideoEncoder LibVvenc = new("libvvenc", Codec.VVC, GPUVendor.Unknown, EncoderKind.Software);
|
||||
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
public static readonly IReadOnlyList<VideoEncoder> All = new[]
|
||||
{
|
||||
H264_NVENC, H264_QSV, H264_AMF, H264_VideoToolbox, Libx264,
|
||||
HEVC_NVENC, HEVC_QSV, HEVC_AMF, HEVC_VideoToolbox, Libx265,
|
||||
VP9_NVENC, VP9_QSV, LibvpxVP9,
|
||||
AV1_NVENC, AV1_QSV, AV1_AMF, LibSvtAv1, LibAomAv1,
|
||||
LibVvenc
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, VideoEncoder> ByName =
|
||||
All.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public string Name { get; }
|
||||
public Codec Codec { get; }
|
||||
public GPUVendor Vendor { get; }
|
||||
public EncoderKind Kind { get; }
|
||||
public bool IsHardware => Kind != EncoderKind.Software;
|
||||
|
||||
private VideoEncoder(string name, Codec codec, GPUVendor vendor, EncoderKind kind)
|
||||
{
|
||||
Name = name;
|
||||
Codec = codec;
|
||||
Vendor = vendor;
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public static VideoEncoder? FromName(string name)
|
||||
{
|
||||
return string.IsNullOrEmpty(name) ? null : ByName.GetValueOrDefault(name);
|
||||
}
|
||||
|
||||
public bool Equals(VideoEncoder other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
return ReferenceEquals(this, other) || string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is VideoEncoder other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
|
||||
}
|
||||
|
||||
public static bool operator ==(VideoEncoder left, VideoEncoder right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(VideoEncoder left, VideoEncoder right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
@@ -1,518 +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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -9,6 +10,8 @@ using MelonLoader;
|
||||
|
||||
namespace ADOFAIRenderer.Util;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Local")]
|
||||
internal static class FolderDialogUtil
|
||||
{
|
||||
private static int _isDialogOpen;
|
||||
@@ -187,14 +190,12 @@ internal static class FolderDialogUtil
|
||||
|
||||
private static string? OpenWindowsFolderBrowserModern(string initialDirectory, IntPtr ownerHandle)
|
||||
{
|
||||
IFileOpenDialog? dialog = null;
|
||||
|
||||
// 1. Try native CoCreateInstance first (works on Mono and .NET without class registration)
|
||||
var clsid = CLSID_FileOpenDialog;
|
||||
var iid = IID_IFileOpenDialog;
|
||||
int createHr = CoCreateInstance(ref clsid, IntPtr.Zero, 1 /* CLSCTX_INPROC_SERVER */, ref iid, out dialog);
|
||||
int createHr = CoCreateInstance(ref clsid, IntPtr.Zero, 1 /* CLSCTX_INPROC_SERVER */, ref iid, out var dialog);
|
||||
|
||||
if (createHr != 0 || dialog == null)
|
||||
if (createHr != 0)
|
||||
{
|
||||
// 2. Fallback to Activator via Type.GetTypeFromCLSID
|
||||
var comType = Type.GetTypeFromCLSID(CLSID_FileOpenDialog);
|
||||
@@ -224,7 +225,7 @@ internal static class FolderDialogUtil
|
||||
{
|
||||
var shellItemGuid = IID_IShellItem;
|
||||
int itemHr = SHCreateItemFromParsingName(normalized, IntPtr.Zero, ref shellItemGuid, out var initialItem);
|
||||
if (itemHr == 0 && initialItem != null)
|
||||
if (itemHr == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -263,7 +264,6 @@ internal static class FolderDialogUtil
|
||||
}
|
||||
|
||||
dialog.GetResult(out var resultItem);
|
||||
if (resultItem == null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -319,7 +319,7 @@ internal static class FolderDialogUtil
|
||||
if (Directory.Exists(normalized))
|
||||
{
|
||||
initialDirPtr = Marshal.StringToHGlobalUni(normalized);
|
||||
callback = (hwnd, uMsg, lParam, lpData) =>
|
||||
callback = (hwnd, uMsg, _, lpData) =>
|
||||
{
|
||||
if (uMsg == BFFM_INITIALIZED && lpData != IntPtr.Zero)
|
||||
{
|
||||
|
||||
+39
-57
@@ -71,72 +71,55 @@ namespace ADOFAIRenderer.Util
|
||||
state.IsInFlight = true;
|
||||
}
|
||||
|
||||
// Check if GPU color conversion is enabled
|
||||
bool useGPUConversion = ModAssets.ColorConversionShader;
|
||||
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
|
||||
// Check if GPU color conversion is available
|
||||
var expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
|
||||
|
||||
if (useGPUConversion)
|
||||
var kernelIndex = -1;
|
||||
if (ModAssets.ColorConversionShader)
|
||||
{
|
||||
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)
|
||||
var kernalName = _targetFormat switch
|
||||
{
|
||||
useGPUConversion = false;
|
||||
}
|
||||
AVPixelFormat.AV_PIX_FMT_NV12 => "CSMain_NV12",
|
||||
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
|
||||
state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height);
|
||||
|
||||
if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader)
|
||||
{
|
||||
int kernelIndex = -1;
|
||||
if (expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12)
|
||||
{
|
||||
kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_NV12");
|
||||
}
|
||||
else if (expectedFmt == AVPixelFormat.AV_PIX_FMT_YUV420P)
|
||||
{
|
||||
kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_YUV420P");
|
||||
}
|
||||
ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture);
|
||||
ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer);
|
||||
ModAssets.ColorConversionShader.SetInt(Width, texture.width);
|
||||
ModAssets.ColorConversionShader.SetInt(Height, texture.height);
|
||||
|
||||
if (kernelIndex >= 0)
|
||||
{
|
||||
ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture);
|
||||
ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer);
|
||||
ModAssets.ColorConversionShader.SetInt(Width, texture.width);
|
||||
ModAssets.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;
|
||||
|
||||
var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12
|
||||
? (texture.width + 31) / 32
|
||||
: (texture.width + 63) / 64;
|
||||
var threadGroupsY = (texture.height + 15) / 16;
|
||||
ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||||
|
||||
ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||||
|
||||
AsyncGPUReadback.Request(
|
||||
state.GPUBuffer,
|
||||
state.CachedCallback
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If kernel is not found, fallback to standard RGBA texture readback
|
||||
AsyncGPUReadback.RequestIntoNativeArray(
|
||||
ref state.NativeBuffer,
|
||||
texture,
|
||||
0,
|
||||
state.CachedCallback
|
||||
);
|
||||
}
|
||||
AsyncGPUReadback.RequestIntoNativeArray(
|
||||
ref state.NativeBuffer, state.GPUBuffer, state.CachedCallback
|
||||
);
|
||||
/*AsyncGPUReadback.Request(
|
||||
state.GPUBuffer,
|
||||
state.CachedCallback
|
||||
);*/
|
||||
}
|
||||
else
|
||||
{
|
||||
AsyncGPUReadback.RequestIntoNativeArray(
|
||||
ref state.NativeBuffer,
|
||||
texture,
|
||||
0,
|
||||
state.CachedCallback
|
||||
ref state.NativeBuffer, texture, 0, state.CachedCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -169,16 +152,14 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame.");
|
||||
|
||||
var byteSize = state.IsGPUConversion
|
||||
? state.Width * state.Height * 3 / 2
|
||||
: state.Width * state.Height * 4;
|
||||
var byteSize = state.Width * state.Height * 3 / 2;
|
||||
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
|
||||
Array.Clear(blackFrame, 0, byteSize);
|
||||
|
||||
var capFrame = new RenderManager.CapturedFrame
|
||||
{
|
||||
ManagedBuffer = blackFrame,
|
||||
Length = byteSize
|
||||
ManagedBuffer = blackFrame, Length = byteSize,
|
||||
IsColorConverted = true
|
||||
};
|
||||
PushToQueue(frame, capFrame);
|
||||
RecycleState(state);
|
||||
@@ -187,19 +168,20 @@ namespace ADOFAIRenderer.Util
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
/*
|
||||
if (state.IsGPUConversion)
|
||||
{
|
||||
var data = request.GetData<byte>();
|
||||
// Copy async request buffer data to persistent NativeBuffer memory
|
||||
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
|
||||
}
|
||||
*/
|
||||
|
||||
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
|
||||
var capFrame = new RenderManager.CapturedFrame
|
||||
{
|
||||
NativePointer = ptr,
|
||||
Length = state.NativeBuffer.Length,
|
||||
ReadbackState = state
|
||||
NativePointer = ptr, Length = state.NativeBuffer.Length,
|
||||
ReadbackState = state, IsColorConverted = state.IsGPUConversion
|
||||
};
|
||||
PushToQueue(frame, capFrame);
|
||||
}
|
||||
|
||||
+120
-120
@@ -2,152 +2,152 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Util.FFmpeg;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util
|
||||
namespace ADOFAIRenderer.Util;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum GPUVendor
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum GPUVendor
|
||||
Unknown, NVIDIA, AMD, Intel, Apple
|
||||
}
|
||||
|
||||
internal static class GPUUtil
|
||||
{
|
||||
private const string NvidiaPattern =
|
||||
@"^NVIDIA GeForce [RG]TX [0-9]+(?> SUPER| Ti(?> SUPER)?)?(?> [0-9]+GB)?(?> Laptop GPU)?(?>/PCIe/SSE2)?$";
|
||||
|
||||
private const string IntelPattern =
|
||||
@"^Intel(?>\(R\))? (?>U?HD Graphics [0-9]+|Arc\(TM\) [A-C][0-9]+ Graphics)(?> \([^)]+\))?$";
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private const string AMDPattern =
|
||||
@"^AMD Radeon RX [0-9]+(?> XT)?$";
|
||||
|
||||
private static GPUVendor _cachedVendor = GPUVendor.Unknown;
|
||||
private static bool _hasCachedVendor;
|
||||
|
||||
private static GPUVendor GetActiveGPUVendor()
|
||||
{
|
||||
Unknown, NVIDIA, AMD, Intel, Apple
|
||||
}
|
||||
if (_hasCachedVendor) return _cachedVendor;
|
||||
|
||||
internal static class GPUUtil
|
||||
{
|
||||
private const string NvidiaPattern =
|
||||
@"^NVIDIA GeForce [RG]TX [0-9]+(?> SUPER| Ti(?> SUPER)?)?(?> [0-9]+GB)?(?> Laptop GPU)?(?>/PCIe/SSE2)?$";
|
||||
var gpuName = SystemInfo.graphicsDeviceName;
|
||||
Melon<RenderMod>.Logger.Msg($"Active GPU: {gpuName}");
|
||||
|
||||
private const string IntelPattern =
|
||||
@"^Intel(?>\(R\))? (?>U?HD Graphics [0-9]+|Arc\(TM\) [A-C][0-9]+ Graphics)(?> \([^)]+\))?$";
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private const string AMDPattern =
|
||||
@"^AMD Radeon RX [0-9]+(?> XT)?$";
|
||||
|
||||
private static GPUVendor _cachedVendor = GPUVendor.Unknown;
|
||||
private static bool _hasCachedVendor;
|
||||
|
||||
private static GPUVendor GetActiveGPUVendor()
|
||||
_cachedVendor = GPUVendor.Unknown;
|
||||
foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern))
|
||||
{
|
||||
if (_hasCachedVendor) return _cachedVendor;
|
||||
_cachedVendor = GPUVendor.NVIDIA;
|
||||
break;
|
||||
}
|
||||
|
||||
var gpuName = SystemInfo.graphicsDeviceName;
|
||||
Melon<RenderMod>.Logger.Msg($"Active GPU: {gpuName}");
|
||||
|
||||
_cachedVendor = GPUVendor.Unknown;
|
||||
foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern))
|
||||
if (_cachedVendor == GPUVendor.Unknown)
|
||||
{
|
||||
foreach (Match unused in Regex.Matches(gpuName, IntelPattern))
|
||||
{
|
||||
_cachedVendor = GPUVendor.NVIDIA;
|
||||
_cachedVendor = GPUVendor.Intel;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_cachedVendor == GPUVendor.Unknown)
|
||||
{
|
||||
foreach (Match unused in Regex.Matches(gpuName, IntelPattern))
|
||||
{
|
||||
_cachedVendor = GPUVendor.Intel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cachedVendor == GPUVendor.Unknown)
|
||||
{
|
||||
foreach (Match unused in Regex.Matches(gpuName, AMDPattern))
|
||||
{
|
||||
_cachedVendor = GPUVendor.AMD;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cachedVendor == GPUVendor.Unknown && Application.platform == RuntimePlatform.OSXPlayer)
|
||||
{
|
||||
_cachedVendor = GPUVendor.Apple;
|
||||
}
|
||||
|
||||
_hasCachedVendor = true;
|
||||
return _cachedVendor;
|
||||
}
|
||||
|
||||
private static double? _cachedNvidiaVersion;
|
||||
private static bool _hasCachedNvidiaVersion;
|
||||
|
||||
public static double? GetNvidiaDriverVersion()
|
||||
if (_cachedVendor == GPUVendor.Unknown)
|
||||
{
|
||||
if (_hasCachedNvidiaVersion) return _cachedNvidiaVersion;
|
||||
|
||||
_hasCachedNvidiaVersion = true;
|
||||
_cachedNvidiaVersion = null;
|
||||
|
||||
if (GetActiveGPUVendor() != GPUVendor.NVIDIA) return null;
|
||||
|
||||
// 1. Try parsing from UnityEngine.SystemInfo.graphicsDeviceVersion
|
||||
var deviceVersion = SystemInfo.graphicsDeviceVersion;
|
||||
if (!string.IsNullOrEmpty(deviceVersion))
|
||||
foreach (Match unused in Regex.Matches(gpuName, AMDPattern))
|
||||
{
|
||||
var match = Regex.Match(deviceVersion, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b");
|
||||
if (match.Success)
|
||||
_cachedVendor = GPUVendor.AMD;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cachedVendor == GPUVendor.Unknown && Application.platform == RuntimePlatform.OSXPlayer)
|
||||
{
|
||||
_cachedVendor = GPUVendor.Apple;
|
||||
}
|
||||
|
||||
_hasCachedVendor = true;
|
||||
return _cachedVendor;
|
||||
}
|
||||
|
||||
private static double? _cachedNvidiaVersion;
|
||||
private static bool _hasCachedNvidiaVersion;
|
||||
|
||||
public static double? GetNvidiaDriverVersion()
|
||||
{
|
||||
if (_hasCachedNvidiaVersion) return _cachedNvidiaVersion;
|
||||
|
||||
_hasCachedNvidiaVersion = true;
|
||||
_cachedNvidiaVersion = null;
|
||||
|
||||
if (GetActiveGPUVendor() != GPUVendor.NVIDIA) return null;
|
||||
|
||||
// 1. Try parsing from UnityEngine.SystemInfo.graphicsDeviceVersion
|
||||
var deviceVersion = SystemInfo.graphicsDeviceVersion;
|
||||
if (!string.IsNullOrEmpty(deviceVersion))
|
||||
{
|
||||
var match = Regex.Match(deviceVersion, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b");
|
||||
if (match.Success)
|
||||
{
|
||||
if (int.TryParse(match.Groups[3].Value, out var sub) && int.TryParse(match.Groups[4].Value, out int build))
|
||||
{
|
||||
if (int.TryParse(match.Groups[3].Value, out var sub) && int.TryParse(match.Groups[4].Value, out int build))
|
||||
{
|
||||
var nvidiaVerInt = sub % 10 * 10000 + build;
|
||||
_cachedNvidiaVersion = nvidiaVerInt / 100.0;
|
||||
Melon<RenderMod>.Logger.Msg($"Parsed NVIDIA Driver Version from SystemInfo: {_cachedNvidiaVersion:F2}");
|
||||
return _cachedNvidiaVersion;
|
||||
}
|
||||
var nvidiaVerInt = sub % 10 * 10000 + build;
|
||||
_cachedNvidiaVersion = nvidiaVerInt / 100.0;
|
||||
Melon<RenderMod>.Logger.Msg($"Parsed NVIDIA Driver Version from SystemInfo: {_cachedNvidiaVersion:F2}");
|
||||
return _cachedNvidiaVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to Windows Registry (only on Windows NT platforms via RegistryUtil)
|
||||
if (System.Environment.OSVersion.Platform != System.PlatformID.Win32NT) return null;
|
||||
const string classKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
|
||||
var subkeys = RegistryUtil.GetHKLMSubKeyNames(classKey);
|
||||
if (subkeys == null) return null;
|
||||
|
||||
foreach (var subkey in subkeys)
|
||||
{
|
||||
var subkeyPath = classKey + "\\" + subkey;
|
||||
var provider = RegistryUtil.GetHKLMValue(subkeyPath, "ProviderName");
|
||||
if (provider == null || !provider.Contains("NVIDIA")) continue;
|
||||
|
||||
var driverVersionStr = RegistryUtil.GetHKLMValue(subkeyPath, "DriverVersion");
|
||||
if (string.IsNullOrEmpty(driverVersionStr)) continue;
|
||||
|
||||
var regMatch = Regex.Match(driverVersionStr, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b");
|
||||
if (!regMatch.Success) continue;
|
||||
if (!int.TryParse(regMatch.Groups[3].Value, out var sub) ||
|
||||
!int.TryParse(regMatch.Groups[4].Value, out var build)) continue;
|
||||
|
||||
var nvidiaVerInt = sub % 10 * 10000 + build;
|
||||
_cachedNvidiaVersion = nvidiaVerInt / 100f;
|
||||
Melon<RenderMod>.Logger.Msg($"Parsed NVIDIA Driver Version from Registry (RegistryUtil): {_cachedNvidiaVersion:F2}");
|
||||
return _cachedNvidiaVersion;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<VideoEncoder> GetPrioritizedEncoders(Codec codec, bool forceSoftware = false)
|
||||
// 2. Fallback to Windows Registry (only on Windows NT platforms via RegistryUtil)
|
||||
if (System.Environment.OSVersion.Platform != System.PlatformID.Win32NT) return null;
|
||||
const string classKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
|
||||
var subkeys = RegistryUtil.GetHKLMSubKeyNames(classKey);
|
||||
if (subkeys == null) return null;
|
||||
|
||||
foreach (var subkey in subkeys)
|
||||
{
|
||||
var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList();
|
||||
if (forceSoftware)
|
||||
{
|
||||
return encoders.Where(e => !e.IsHardware).ToList();
|
||||
}
|
||||
var subkeyPath = classKey + "\\" + subkey;
|
||||
var provider = RegistryUtil.GetHKLMValue(subkeyPath, "ProviderName");
|
||||
if (provider == null || !provider.Contains("NVIDIA")) continue;
|
||||
|
||||
var vendor = GetActiveGPUVendor();
|
||||
var prioritized = new List<VideoEncoder>();
|
||||
var others = new List<VideoEncoder>();
|
||||
var driverVersionStr = RegistryUtil.GetHKLMValue(subkeyPath, "DriverVersion");
|
||||
if (string.IsNullOrEmpty(driverVersionStr)) continue;
|
||||
|
||||
foreach (var item in encoders)
|
||||
{
|
||||
if (item.Vendor == vendor) prioritized.Add(item);
|
||||
else others.Add(item);
|
||||
}
|
||||
var regMatch = Regex.Match(driverVersionStr, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b");
|
||||
if (!regMatch.Success) continue;
|
||||
if (!int.TryParse(regMatch.Groups[3].Value, out var sub) ||
|
||||
!int.TryParse(regMatch.Groups[4].Value, out var build)) continue;
|
||||
|
||||
prioritized.AddRange(others);
|
||||
return prioritized;
|
||||
var nvidiaVerInt = sub % 10 * 10000 + build;
|
||||
_cachedNvidiaVersion = nvidiaVerInt / 100f;
|
||||
Melon<RenderMod>.Logger.Msg($"Parsed NVIDIA Driver Version from Registry (RegistryUtil): {_cachedNvidiaVersion:F2}");
|
||||
return _cachedNvidiaVersion;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<VideoEncoder> GetPrioritizedEncoders(Codec codec, bool forceSoftware = false)
|
||||
{
|
||||
var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList();
|
||||
if (forceSoftware)
|
||||
{
|
||||
return encoders.Where(e => !e.IsHardware).ToList();
|
||||
}
|
||||
|
||||
var vendor = GetActiveGPUVendor();
|
||||
var prioritized = new List<VideoEncoder>();
|
||||
var others = new List<VideoEncoder>();
|
||||
|
||||
foreach (var item in encoders)
|
||||
{
|
||||
if (item.Vendor == vendor) prioritized.Add(item);
|
||||
else others.Add(item);
|
||||
}
|
||||
|
||||
prioritized.AddRange(others);
|
||||
return prioritized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user