using System; using System.Collections.Generic; using ADOFAIRenderer.API; using MelonLoader; using UnityEngine; namespace ADOFAIRenderer.Core; internal class FrameHookPipeline : IDisposable { private RenderTexture? _pingRT; private RenderTexture? _pongRT; private RenderTexture? _lastOutputRT; private FrameHookContext? _reusableContext; private int _width; private int _height; private RenderTextureFormat _format; public RenderTexture? Process( RenderTexture sourceTexture, long frameIndex, long renderFrameIndex, double videoTime, double renderFps, double videoFps, IReadOnlyList> hooks) { if (hooks.Count == 0 || !sourceTexture || !sourceTexture.IsCreated()) return sourceTexture; EnsureBuffers(sourceTexture); if (!_pingRT || !_pongRT || !_pingRT.IsCreated() || !_pongRT.IsCreated()) return sourceTexture; var prevActive = RenderTexture.active; try { // Inter-frame ping-pong safety: start with the buffer that was NOT used as the output // of the previous frame, ensuring the buffer currently being read back is not immediately overwritten. var firstDestination = _lastOutputRT == _pingRT ? _pongRT : _pingRT; if (_reusableContext == null) { _reusableContext = new FrameHookContext( sourceTexture, _pingRT, _pongRT, firstDestination, frameIndex, renderFrameIndex, videoTime, renderFps, videoFps ); } else { _reusableContext.Reset( sourceTexture, _pingRT, _pongRT, firstDestination, frameIndex, renderFrameIndex, videoTime, renderFps, videoFps ); } for (var i = 0; i < hooks.Count; i++) { try { hooks[i](_reusableContext); } catch (Exception ex) { Melon.Logger.Error($"[FrameHook] Error executing frame hook #{i} on frame {frameIndex}: {ex}"); } } var result = _reusableContext.Current; _lastOutputRT = _reusableContext.HasModified ? result : null; return result; } finally { RenderTexture.active = prevActive; } } private void EnsureBuffers(RenderTexture sourceTexture) { var width = sourceTexture.width; var height = sourceTexture.height; var format = sourceTexture.format; if (_pingRT && _pongRT && _width == width && _height == height && _format == format && _pingRT.IsCreated() && _pongRT.IsCreated()) { return; } ReleaseBuffers(); _width = width; _height = height; _format = format; try { var desc = sourceTexture.descriptor; desc.depthBufferBits = 0; desc.msaaSamples = 1; desc.useMipMap = false; desc.autoGenerateMips = false; var supportsUav = SystemInfo.supportsComputeShaders && SystemInfo.SupportsRandomWriteOnRenderTextureFormat(desc.colorFormat); desc.enableRandomWrite = supportsUav; _pingRT = new RenderTexture(desc) { name = "ADOFAIRenderer_PingBuffer", filterMode = sourceTexture.filterMode, wrapMode = sourceTexture.wrapMode }; _pingRT.Create(); _pongRT = new RenderTexture(desc) { name = "ADOFAIRenderer_PongBuffer", filterMode = sourceTexture.filterMode, wrapMode = sourceTexture.wrapMode }; _pongRT.Create(); } catch (Exception ex) { ReleaseBuffers(); Melon.Logger.Warning($"[FrameHook] Failed to create descriptor-based PingPong RTs: {ex.Message}. Falling back to standard ARGB32."); _pingRT = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32) { name = "ADOFAIRenderer_PingBuffer", filterMode = sourceTexture.filterMode, wrapMode = sourceTexture.wrapMode }; _pingRT.Create(); _pongRT = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32) { name = "ADOFAIRenderer_PongBuffer", filterMode = sourceTexture.filterMode, wrapMode = sourceTexture.wrapMode }; _pongRT.Create(); } } public void ReleaseBuffers() { if (_pingRT) { if (_pingRT.IsCreated()) _pingRT.Release(); UnityEngine.Object.Destroy(_pingRT); _pingRT = null; } if (_pongRT) { if (_pongRT.IsCreated()) _pongRT.Release(); UnityEngine.Object.Destroy(_pongRT); _pongRT = null; } _width = 0; _height = 0; _lastOutputRT = null; _reusableContext = null; } public void Dispose() { ReleaseBuffers(); } }