From c90d43e9173eb0c48fdca5f431d0877e1eaf113c Mon Sep 17 00:00:00 2001 From: ww-rm Date: Wed, 14 Jan 2026 00:42:51 +0800 Subject: [PATCH 1/5] update docs --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f382ae3..b7cc7f3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,8 @@ 仓库目前包含 4 个分支: -- `main`: 默认分支, 也是项目最新版的发布用分支 +- `main`: 默认分支, 也是项目最新版的代码分支 +- `release/wpf`: WPF 版本发布分支 - `dev/wpf`: WPF 版本开发分支 - `release/wf`: Winforms 旧版本发布分支 (已弃用, 仅进行 bug 修复) - `dev/wf`: Winforms 旧版本开发分支 (已弃用, 仅进行 bug 修复) From 5f5d0e69808cae53656c94e3968bce9bf767516a Mon Sep 17 00:00:00 2001 From: ww-rm Date: Wed, 14 Jan 2026 00:46:23 +0800 Subject: [PATCH 2/5] update docs --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7cc7f3e..2865d835 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## 仓库分支 -仓库目前包含 4 个分支: +仓库目前包含以下分支: - `main`: 默认分支, 也是项目最新版的代码分支 - `release/wpf`: WPF 版本发布分支 From 71b3ec49678375267b3db6ab34ee2700ec79ddec Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 18 Jan 2026 20:20:25 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Spine/Exporters/BaseExporter.cs | 5 +- Spine/Exporters/CustomFFmpegExporter.cs | 16 ------ Spine/Exporters/FFmpegVideoExporter.cs | 16 ------ Spine/Exporters/VideoExporter.cs | 68 ++++++++++++++++++++++--- 4 files changed, 63 insertions(+), 42 deletions(-) diff --git a/Spine/Exporters/BaseExporter.cs b/Spine/Exporters/BaseExporter.cs index 0679eb1d..f8b537a9 100644 --- a/Spine/Exporters/BaseExporter.cs +++ b/Spine/Exporters/BaseExporter.cs @@ -160,14 +160,15 @@ public float Rotation /// /// 获取的一帧, 结果是预乘的 /// - protected virtual SFMLImageVideoFrame GetFrame(SpineObject[] spines) + protected SFMLImageVideoFrame GetFrame(SpineObject[] spines) { _renderTexture.SetActive(true); _renderTexture.Clear(_backgroundColorPma); foreach (var sp in spines.Reverse()) _renderTexture.Draw(sp); _renderTexture.Display(); + var frame = new SFMLImageVideoFrame(_renderTexture.Texture.CopyToImage()); _renderTexture.SetActive(false); - return new(_renderTexture.Texture.CopyToImage()); + return frame; } /// diff --git a/Spine/Exporters/CustomFFmpegExporter.cs b/Spine/Exporters/CustomFFmpegExporter.cs index 016ae493..bc0af230 100644 --- a/Spine/Exporters/CustomFFmpegExporter.cs +++ b/Spine/Exporters/CustomFFmpegExporter.cs @@ -65,22 +65,6 @@ private void SetOutputOptions(FFMpegArgumentOptions options) if (!string.IsNullOrEmpty(_customArgs)) options.WithCustomArgument($"{_customArgs}"); } - /// - /// 获取的一帧, 结果是预乘的 - /// - protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines) - { - // BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死 - // 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失 - using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y); - using var view = _renderTexture.GetView(); - tex.SetView(view); - tex.Clear(_backgroundColorPma); - foreach (var sp in spines.Reverse()) tex.Draw(sp); - tex.Display(); - return new(tex.Texture.CopyToImage()); - } - public override void Export(string output, CancellationToken ct, params SpineObject[] spines) { var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps }; diff --git a/Spine/Exporters/FFmpegVideoExporter.cs b/Spine/Exporters/FFmpegVideoExporter.cs index 860e397a..10dbfd2a 100644 --- a/Spine/Exporters/FFmpegVideoExporter.cs +++ b/Spine/Exporters/FFmpegVideoExporter.cs @@ -104,22 +104,6 @@ public enum MovProfile public MovProfile Profile { get => _profile; set => _profile = value; } private MovProfile _profile = MovProfile.Yuv4444Extreme; - /// - /// 获取的一帧, 结果是预乘的 - /// - protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines) - { - // BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死 - // 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失 - using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y); - using var view = _renderTexture.GetView(); - tex.SetView(view); - tex.Clear(_backgroundColorPma); - foreach (var sp in spines.Reverse()) tex.Draw(sp); - tex.Display(); - return new(tex.Texture.CopyToImage()); - } - public override void Export(string output, CancellationToken ct, params SpineObject[] spines) { var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps }; diff --git a/Spine/Exporters/VideoExporter.cs b/Spine/Exporters/VideoExporter.cs index 9cf2a97d..a3211a69 100644 --- a/Spine/Exporters/VideoExporter.cs +++ b/Spine/Exporters/VideoExporter.cs @@ -16,6 +16,9 @@ namespace Spine.Exporters /// public abstract class VideoExporter : BaseExporter { + private readonly object _frameOutputLock = new(); + private SFMLImageVideoFrame? _frameOutput; + public VideoExporter(uint width, uint height) : base(width, height) { } public VideoExporter(Vector2u resolution) : base(resolution) { } @@ -83,11 +86,10 @@ public int GetFrameCount() { var delta = 1f / _fps; var total = (int)(_duration * _fps); // 完整帧的数量 - var deltaFinal = _duration - delta * total; // 最后一帧时长 - var final = _keepLast && deltaFinal > 1e-3 ? 1 : 0; + bool hasFinal = _keepLast && deltaFinal > 1e-3; - var frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧 + var frameCount = 1 + total + (hasFinal ? 1 : 0); // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧 return frameCount; } @@ -98,7 +100,8 @@ protected IEnumerable GetFrames(SpineObject[] spines) { float delta = 1f / _fps; int total = (int)(_duration * _fps); // 完整帧的数量 - bool hasFinal = _keepLast && (_duration - delta * total) > 1e-3; + var deltaFinal = _duration - delta * total; // 最后一帧时长 + bool hasFinal = _keepLast && deltaFinal > 1e-3; // 导出首帧 var firstFrame = GetFrame(spines); @@ -114,12 +117,39 @@ protected IEnumerable GetFrames(SpineObject[] spines) // 导出最后一帧 if (hasFinal) { - // XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长 - foreach (var spine in spines) spine.Update(delta * _speed); + foreach (var spine in spines) spine.Update(deltaFinal * _speed); yield return GetFrame(spines); } } + /// + /// 帧渲染任务, 用于保证每一帧的渲染都在同一个线程里完成 + /// + private void GetFramesTask(SpineObject[] spines, CancellationToken ct) + { + // XXX: 也许和 SFML 多线程或者 FFmpeg 调用有关, GetFrame 无法在异步调用中连续使用, 会导致画面帧丢失或者卡死等异常现象 + // 因此把帧生成包在一个子线程中连续调用, 通过成员变量和锁来输出帧数据 + foreach (var frame in GetFrames(spines)) + { + while (!ct.IsCancellationRequested) + { + // 等待之前的数据被取走 + lock (_frameOutputLock) + { + if (_frameOutput is null) + break; + } + Thread.Sleep(10); + } + if (ct.IsCancellationRequested) + { + frame.Dispose(); + break; + } + _frameOutput = frame; + } + } + /// /// 生成帧序列, 支持中途取消和进度输出, 用于动图视频等单个文件输出 /// @@ -127,21 +157,43 @@ protected IEnumerable GetFrames(SpineObject[] spines, strin { int frameCount = GetFrameCount(); int frameIdx = 0; + SFMLImageVideoFrame frame = null; + + using var getFramesTask = Task.Run(() => GetFramesTask(spines, ct), ct); _progressReporter?.Invoke(frameCount, 0, $"[0/{frameCount}] {output}"); - foreach (var frame in GetFrames(spines)) + while (frameIdx < frameCount) { + while (!ct.IsCancellationRequested) + { + // 等待新帧的生成 + lock (_frameOutputLock) + { + if (_frameOutput is not null) + { + frame = _frameOutput; + _frameOutput = null; + break; + } + } + + Thread.Sleep(10); + } + if (ct.IsCancellationRequested) { _logger.Info("Export cancelled"); - frame.Dispose(); + frame?.Dispose(); break; } _progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {output}"); yield return frame; + frame = null; frameIdx++; } + + getFramesTask.Wait(CancellationToken.None); // 等待结束 (正常结束或者被取消) } public sealed override void Export(string output, params SpineObject[] spines) => Export(output, default, spines); From f7bd44aabfdba1ad112b2596d59a2263a3faf054 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 18 Jan 2026 20:21:11 +0800 Subject: [PATCH 4/5] update to v0.16.14 --- Spine/Spine.csproj | 2 +- SpineViewer/SpineViewer.csproj | 2 +- SpineViewerCLI/SpineViewerCLI.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj index 054c2638..34cf0d5e 100644 --- a/Spine/Spine.csproj +++ b/Spine/Spine.csproj @@ -11,7 +11,7 @@ $(SolutionDir)out $(BaseOutputPath)\$(Configuration)\$(PlatformTarget) false - 0.16.13 + 0.16.14 diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj index 113468b2..114cfdcb 100644 --- a/SpineViewer/SpineViewer.csproj +++ b/SpineViewer/SpineViewer.csproj @@ -11,7 +11,7 @@ $(SolutionDir)out $(BaseOutputPath)\$(Configuration)\$(PlatformTarget) false - 0.16.14-beta + 0.16.14 WinExe true diff --git a/SpineViewerCLI/SpineViewerCLI.csproj b/SpineViewerCLI/SpineViewerCLI.csproj index 6b6b54c5..e787d461 100644 --- a/SpineViewerCLI/SpineViewerCLI.csproj +++ b/SpineViewerCLI/SpineViewerCLI.csproj @@ -11,7 +11,7 @@ $(SolutionDir)out $(BaseOutputPath)\$(Configuration)\$(PlatformTarget) false - 0.16.13 + 0.16.14 Exe From 5dd40fccf1be822a13f107d9a645365afdf00d99 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 18 Jan 2026 20:22:36 +0800 Subject: [PATCH 5/5] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba0178a..5f4b0269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## v0.16.14 + +- 优化 FFmpeg 导出速度 +- 修复可能的内存泄漏 + ## v0.16.13 - 增加 FFmpeg 下载页面跳转菜单项