#nullable enable using System; using System.Collections.Generic; using SharpDX.Direct3D11; using SharpDX.DXGI; using T3.Core.DataTypes; using T3.Core.Logging; using T3.Core.Resource.Assets; using T3.Core.Settings; using ComputeShader = T3.Core.DataTypes.ComputeShader; using Texture2D = T3.Core.DataTypes.Texture2D; namespace T3.Core.Resource; /// /// Uses a compute shader to converts an image texture into a BGRA8 /// and provide CPU read access for writing to images and video files. /// public sealed class TextureBgraReadAccess : IDisposable { private readonly bool _useImmediateReadback; private readonly Format _targetFormat; public TextureBgraReadAccess(bool useImmediateReadback = false, Format targetFormat = Format.B8G8R8A8_UNorm) { _useImmediateReadback = useImmediateReadback; _targetFormat = targetFormat; } /// /// A helper method that uses a compute shader to convert the source texture first to 8bit. /// public Texture2D ConvertToCpuReadableBgra(Texture2D sourceTexture) { if (sourceTexture == null! || sourceTexture.IsDisposed) throw new ArgumentException("Source texture is null or disposed"); PrepareCpuAccessTexturesAndConversionTextures(sourceTexture.Description); PrepareResolveShaderResources(); ConvertTextureToRgba(sourceTexture); var context = ResourceManager.Device.ImmediateContext; var cpuAccessTexture = _imagesWithCpuAccess[0]; context.CopyResource(_conversionTexture, cpuAccessTexture); context.Flush(); // Ensure the copy is complete return cpuAccessTexture; } public sealed class ReadRequestItem { internal int RequestIndex; public required TextureBgraReadAccess TextureBgraReadAccess; /// /// An optional filepath that can be passed along /// public required string? Filepath; public required Texture2D CpuAccessTexture; public required OnReadComplete OnSuccess; public required int CpuAccessTextureCount; internal bool IsReady => RequestIndex == TextureBgraReadAccess._swapCounter - (CpuAccessTextureCount - 2); internal bool IsObsolete => RequestIndex < TextureBgraReadAccess._swapCounter - (CpuAccessTextureCount - 2); } /// /// Saving a screenshot will take several frames because it takes a while until the frames are /// downloaded from the gpu. The method need to be called once a frame. /// public void Update() { _swapCounter++; // Clear obsolete items while (_readRequests.Count > 0 && _readRequests[0].IsObsolete) { _readRequests.RemoveAt(0); } if (_readRequests.Count == 0 || !_readRequests[0].IsReady) return; var completedRequest = _readRequests[0]; //Log.Debug($"Completed frame i{completedRequest.RequestIndex} Run:{completedRequest.RequestRunTime:0.000}s Play:{completedRequest.RequestPlayback:0.000}s completed {Playback.RunTimeInSecs:0.000}"); completedRequest.OnSuccess(completedRequest); _readRequests.RemoveAt(0); } public delegate void OnReadComplete(ReadRequestItem requestItem); /// /// Convert into BRGA and initiate the readback process. /// It will take several frames, until the texture is accessible and the callback is called. /// public bool InitiateConvertAndReadBack(Texture2D originalTexture, OnReadComplete onSuccess, string? filepath = null) { if (originalTexture == null! || originalTexture.IsDisposed) return false; PrepareCpuAccessTexturesAndConversionTextures(originalTexture.Description); PrepareResolveShaderResources(); ConvertTextureToRgba(originalTexture); var context = ResourceManager.Device.ImmediateContext; var cpuAccessTexture = _imagesWithCpuAccess[_swapCounter % CpuAccessTextureCount]; context.CopyResource(_conversionTexture, cpuAccessTexture); if (_useImmediateReadback) { context.Flush(); //var dataBox = context.MapSubresource(cpuAccessTexture, 0, MapMode.Read, MapFlags.None); try { var request = new ReadRequestItem { RequestIndex = _swapCounter, TextureBgraReadAccess = this, Filepath = filepath, CpuAccessTexture = cpuAccessTexture, OnSuccess = onSuccess, CpuAccessTextureCount = CpuAccessTextureCount, }; onSuccess(request); } finally { context.UnmapSubresource(cpuAccessTexture, 0); } } else { context.UnmapSubresource(cpuAccessTexture, 0); _readRequests.Add(new ReadRequestItem { RequestIndex = _swapCounter, TextureBgraReadAccess = this, Filepath = filepath, CpuAccessTexture = cpuAccessTexture, OnSuccess = onSuccess, CpuAccessTextureCount = CpuAccessTextureCount, }); } return true; } /// /// Create several textures with a given format with CPU access to be able to read out the initial texture values /// private void PrepareCpuAccessTexturesAndConversionTextures(Texture2DDescription currentDesc) { if (_imagesWithCpuAccess.Count != 0 && _imagesWithCpuAccess[0].Description.Width == currentDesc.Width && _imagesWithCpuAccess[0].Description.Height == currentDesc.Height ) return; DisposeTextures(); if (_readRequests.Count > 0) { Log.Debug($"Discarding {_readRequests.Count} texture frames with outdated format"); _readRequests.Clear(); } // Create read back textures var cpuAccessDescription = new Texture2DDescription { BindFlags = BindFlags.None, Format = _targetFormat, Width = currentDesc.Width, Height = currentDesc.Height, MipLevels = 1, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Staging, OptionFlags = ResourceOptionFlags.None, CpuAccessFlags = CpuAccessFlags.Read, ArraySize = 1 }; for (var i = 0; i < CpuAccessTextureCount; ++i) { _imagesWithCpuAccess.Add(Texture2D.CreateTexture2D(cpuAccessDescription)); } // Create format conversion texture var convertTextureDescription = new Texture2DDescription { BindFlags = BindFlags.UnorderedAccess | BindFlags.RenderTarget | BindFlags.ShaderResource, Format = _targetFormat, Width = currentDesc.Width, Height = currentDesc.Height, MipLevels = 1, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Default, OptionFlags = ResourceOptionFlags.None, CpuAccessFlags = CpuAccessFlags.None, ArraySize = 1 }; _conversionTexture = Texture2D.CreateTexture2D(convertTextureDescription); _conversionTexture.CreateUnorderedAccessView(ref _conversionUav, "textureReadAccessUav"); } #region conversion shader private void PrepareResolveShaderResources() { if (_convertComputeShaderResource != null) return; const string sourceAddress = FileLocations.LibPackageName + ":shaders/img/ConvertFormat-cs.hlsl"; AssetRegistry.TryResolveAddress(sourceAddress, null,out var sourcePath, out _); const string entryPoint = "main"; //const string debugName = "resolve-convert-texture-format"; _convertComputeShaderResource = ResourceManager.CreateShaderResource(sourcePath, null, () => entryPoint); if (_convertComputeShaderResource.Value == null) Log.Error($"Failed to initialize video conversion shader"); } private void ConvertTextureToRgba(Texture2D inputTexture) { if (_convertComputeShaderResource?.Value == null) return; var device = ResourceManager.Device; var deviceContext = device.ImmediateContext; var csStage = deviceContext.ComputeShader; // Keep previous setup var prevShader = csStage.Get(); var prevUavs = csStage.GetUnorderedAccessViews(0, 1); var prevSrvs = csStage.GetShaderResources(0, 1); var convertShader = _convertComputeShaderResource.Value; csStage.Set(convertShader); const int threadNumX = 16, threadNumY = 16; var srv = SrvManager.GetSrvForTexture(inputTexture); csStage.SetShaderResource(0, srv); csStage.SetUnorderedAccessView(0, _conversionUav, 0); var dispatchCountX = (inputTexture.Description.Width / threadNumX) + 1; var dispatchCountY = (inputTexture.Description.Height / threadNumY) + 1; deviceContext.Dispatch(dispatchCountX, dispatchCountY, 1); // Restore prev setup csStage.SetUnorderedAccessView(0, prevUavs[0]); csStage.SetShaderResource(0, prevSrvs[0]); csStage.Set(prevShader); } private Resource? _convertComputeShaderResource; #endregion private int CpuAccessTextureCount => _useImmediateReadback ? 1 : DefaultCpuAccessTextureCount; private const int DefaultCpuAccessTextureCount = 3; private readonly List _imagesWithCpuAccess = []; /** Skip a certain number of images at the beginning since the * final content will only appear after several buffer flips*/ public const int SkipImages = 0; public void ClearQueue() { _readRequests.Clear(); } private void DisposeTextures() { _conversionTexture?.Dispose(); foreach (var image in _imagesWithCpuAccess) { image.Dispose(); } _imagesWithCpuAccess.Clear(); } public void Dispose() { DisposeTextures(); _convertComputeShaderResource?.Dispose(); _convertComputeShaderResource = null; _conversionUav?.Dispose(); _conversionUav = null; } private readonly List _readRequests = new(3); private int _swapCounter; private Texture2D? _conversionTexture; private UnorderedAccessView? _conversionUav; }