#nullable enable using System; using SharpDX.Direct3D11; using T3.Core.Model; using T3.Core.Resource; using T3.Core.Video; using Texture2D = T3.Core.DataTypes.Texture2D; namespace T3.Editor.Gui.Windows.RenderExport; /// The render-export video writer contract, shared by the Media Foundation and FFmpeg backends. internal interface IRenderVideoWriter : IDisposable { bool ProcessFrames(Texture2D gpuTexture, ref byte[] audioFrame, int channels, int sampleRate); } /// /// Render-export writer backed by the FFmpeg encoder. The encoder lives in the operator-loaded video assembly /// (the editor cannot depend on it directly) and is reached through the Core facade. /// Each output frame is converted and read back to RGBA8 on the immediate context (synchronously — render /// export is offline, so a per-frame GPU stall is acceptable and avoids the async-readback bookkeeping), then /// handed to the encoder together with the audio mixdown. /// internal sealed class FfmpegVideoExportWriter : IRenderVideoWriter { /// Returns null (with a reason) when the FFmpeg encoder isn't available — the caller falls back. public static FfmpegVideoExportWriter? TryCreate(RenderProcess.ExportSession session, out string? error) { error = null; var factory = VideoExport.Factory; if (factory == null) { // The factory registers via the Video package's [ModuleInitializer], which only fires once that // package's code has run (e.g. a video op was used). For an export from a project with no video // operator, force each loaded package's module initializer so the encoder registers regardless. foreach (var package in SymbolPackage.AllPackages) package.AssemblyInformation.RunModuleInitializers(); factory = VideoExport.Factory; } if (factory == null) { error = "FFmpeg video encoder is unavailable (video package not loaded)."; return null; } var settings = new VideoExportSettings { FilePath = session.TargetFilePath, Width = session.RenderToFileResolution.Width, Height = session.RenderToFileResolution.Height, FrameRate = session.Settings.FrameRate, BitRate = session.Settings.Bitrate, Codec = session.Settings.VideoCodec, ExportAudio = session.Settings.ExportAudio, AudioChannels = RenderAudioInfo.SoundtrackChannels(), AudioSampleRate = RenderAudioInfo.SoundtrackSampleRate(), }; var writer = factory.TryCreateWriter(settings, out error); return writer == null ? null : new FfmpegVideoExportWriter(writer); } private FfmpegVideoExportWriter(IVideoFileWriter writer) => _writer = writer; public unsafe bool ProcessFrames(Texture2D gpuTexture, ref byte[] audioFrame, int channels, int sampleRate) { // Synchronous convert (any source format) → RGBA8 staging texture, then map and feed the bytes. var cpuTexture = _readAccess.ConvertToCpuReadableBgra(gpuTexture); var context = ResourceManager.Device.ImmediateContext; var dataBox = context.MapSubresource(cpuTexture, 0, 0, MapMode.Read, MapFlags.None, out _); try { var pixels = new ReadOnlySpan((void*)dataBox.DataPointer, cpuTexture.Description.Height * dataBox.RowPitch); _writer.AddVideoFrame(pixels, dataBox.RowPitch); if (audioFrame is { Length: > 0 }) _writer.AddAudioSamples(audioFrame); } finally { context.UnmapSubresource(cpuTexture, 0); } return true; } public void Dispose() { try { _writer.Finish(); } catch (Exception e) { Log.Warning("FFmpeg export: finishing the output failed - " + e.Message); } _writer.Dispose(); _readAccess.Dispose(); } private readonly IVideoFileWriter _writer; // Immediate (synchronous) readback, converting straight to RGBA8 so the bytes match the encoder's input. private readonly TextureBgraReadAccess _readAccess = new(useImmediateReadback: true, targetFormat: SharpDX.DXGI.Format.R8G8B8A8_UNorm); }