Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
497103b
Merge pull request #69 from ww-rm/dev/wpf
ww-rm Jul 25, 2025
eca59dc
Merge pull request #74 from ww-rm/dev/wpf
ww-rm Jul 26, 2025
7bc82ab
Merge pull request #75 from ww-rm/dev/wpf
ww-rm Jul 26, 2025
6550878
Merge pull request #77 from ww-rm/dev/wpf
ww-rm Jul 31, 2025
a697ccc
Merge pull request #80 from ww-rm/dev/wpf
ww-rm Aug 17, 2025
40bde84
Merge pull request #81 from ww-rm/dev/wpf
ww-rm Aug 18, 2025
ad190d8
Merge pull request #85 from ww-rm/dev/wpf
ww-rm Aug 20, 2025
8e1f586
Merge pull request #86 from ww-rm/dev/wpf
ww-rm Aug 20, 2025
be8193e
Merge pull request #90 from ww-rm/dev/wpf
ww-rm Sep 3, 2025
707aa7f
Merge pull request #91 from ww-rm/dev/wpf
ww-rm Sep 3, 2025
0c16b2f
Merge pull request #93 from ww-rm/dev/wpf
ww-rm Sep 4, 2025
5ef1323
Merge pull request #97 from ww-rm/dev/wpf
ww-rm Sep 7, 2025
b3cd0b9
Merge pull request #99 from ww-rm/dev/wpf
ww-rm Sep 11, 2025
2c3b076
Merge pull request #101 from ww-rm/dev/wpf
ww-rm Sep 20, 2025
a28cb3f
Merge pull request #102 from ww-rm/dev/wpf
ww-rm Sep 21, 2025
0443d5e
Merge pull request #104 from ww-rm/dev/wpf
ww-rm Sep 24, 2025
f7ace4d
Merge pull request #106 from ww-rm/dev/wpf
ww-rm Sep 27, 2025
34f9eef
Merge pull request #109 from ww-rm/dev/wpf
ww-rm Sep 30, 2025
6e46152
Merge pull request #110 from ww-rm/dev/wpf
ww-rm Sep 30, 2025
267c7b8
Merge pull request #111 from ww-rm/dev/wpf
ww-rm Sep 30, 2025
47aafc7
Merge pull request #113 from ww-rm/dev/wpf
ww-rm Sep 30, 2025
d1d32b6
Merge pull request #115 from ww-rm/dev/wpf
ww-rm Oct 1, 2025
6f9b357
Merge pull request #116 from ww-rm/dev/wpf
ww-rm Oct 2, 2025
03c4974
Merge pull request #118 from ww-rm/dev/wpf
ww-rm Oct 3, 2025
a0b7db0
Merge pull request #119 from ww-rm/dev/wpf
ww-rm Oct 3, 2025
6727fa8
Merge pull request #120 from ww-rm/dev/wpf
ww-rm Oct 4, 2025
249b930
Merge pull request #122 from ww-rm/dev/wpf
ww-rm Oct 4, 2025
4b23c77
Merge pull request #124 from ww-rm/dev/wpf
ww-rm Oct 6, 2025
0893bd4
Merge pull request #133 from ww-rm/dev/wpf
ww-rm Oct 19, 2025
64bc12d
Merge pull request #136 from ww-rm/dev/wpf
ww-rm Oct 27, 2025
8b0ea75
Merge pull request #139 from ww-rm/dev/wpf
ww-rm Oct 29, 2025
9040e02
Merge pull request #140 from ww-rm/dev/wpf
ww-rm Oct 29, 2025
1be9e9e
Merge pull request #141 from ww-rm/dev/wpf
ww-rm Oct 29, 2025
659f6fb
Merge pull request #142 from ww-rm/dev/wpf
ww-rm Oct 29, 2025
c538fd8
Merge pull request #143 from ww-rm/dev/wpf
ww-rm Nov 2, 2025
dc2cb61
Merge pull request #144 from ww-rm/dev/wpf
ww-rm Nov 2, 2025
c1677bd
Merge pull request #148 from ww-rm/dev/wpf
ww-rm Nov 9, 2025
f3011f1
Merge pull request #154 from ww-rm/dev/wpf
ww-rm Dec 4, 2025
45ae963
Merge pull request #155 from ww-rm/dev/wpf
ww-rm Dec 4, 2025
f322666
Merge pull request #157 from ww-rm/dev/wpf
ww-rm Dec 6, 2025
2c8bc28
Merge pull request #158 from ww-rm/dev/wpf
ww-rm Dec 7, 2025
8225c22
Merge pull request #159 from ww-rm/dev/wpf
ww-rm Dec 7, 2025
c90d43e
update docs
ww-rm Jan 13, 2026
a95a6f2
Merge pull request #160 from ww-rm/dev/wpf
ww-rm Jan 13, 2026
5f5d0e6
update docs
ww-rm Jan 13, 2026
6a918d8
Merge pull request #161 from ww-rm/dev/wpf
ww-rm Jan 13, 2026
71b3ec4
优化导出性能
ww-rm Jan 18, 2026
f7bd44a
update to v0.16.14
ww-rm Jan 18, 2026
5dd40fc
update changelog
ww-rm Jan 18, 2026
ff1f599
Merge pull request #162 from ww-rm/dev/wpf
ww-rm Jan 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## v0.16.14

- 优化 FFmpeg 导出速度
- 修复可能的内存泄漏

## v0.16.13

- 增加 FFmpeg 下载页面跳转菜单项
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

## 仓库分支

仓库目前包含 4 个分支:
仓库目前包含以下分支:

- `main`: 默认分支, 也是项目最新版的发布用分支
- `main`: 默认分支, 也是项目最新版的代码分支
- `release/wpf`: WPF 版本发布分支
- `dev/wpf`: WPF 版本开发分支
- `release/wf`: Winforms 旧版本发布分支 (已弃用, 仅进行 bug 修复)
- `dev/wf`: Winforms 旧版本开发分支 (已弃用, 仅进行 bug 修复)
Expand Down
5 changes: 3 additions & 2 deletions Spine/Exporters/BaseExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,15 @@ public float Rotation
/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
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;
}

/// <summary>
Expand Down
16 changes: 0 additions & 16 deletions Spine/Exporters/CustomFFmpegExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,6 @@ private void SetOutputOptions(FFMpegArgumentOptions options)
if (!string.IsNullOrEmpty(_customArgs)) options.WithCustomArgument($"{_customArgs}");
}

/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
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 };
Expand Down
16 changes: 0 additions & 16 deletions Spine/Exporters/FFmpegVideoExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,6 @@ public enum MovProfile
public MovProfile Profile { get => _profile; set => _profile = value; }
private MovProfile _profile = MovProfile.Yuv4444Extreme;

/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
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 };
Expand Down
68 changes: 60 additions & 8 deletions Spine/Exporters/VideoExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ namespace Spine.Exporters
/// </summary>
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) { }

Expand Down Expand Up @@ -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;
}

Expand All @@ -98,7 +100,8 @@ protected IEnumerable<SFMLImageVideoFrame> 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);
Expand All @@ -114,34 +117,83 @@ protected IEnumerable<SFMLImageVideoFrame> 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);
}
}

/// <summary>
/// 帧渲染任务, 用于保证每一帧的渲染都在同一个线程里完成
/// </summary>
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;
}
}

/// <summary>
/// 生成帧序列, 支持中途取消和进度输出, 用于动图视频等单个文件输出
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines, string output, CancellationToken ct)
{
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);
Expand Down
2 changes: 1 addition & 1 deletion Spine/Spine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.13</Version>
<Version>0.16.14</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion SpineViewer/SpineViewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.14-beta</Version>
<Version>0.16.14</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion SpineViewerCLI/SpineViewerCLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.13</Version>
<Version>0.16.14</Version>
<OutputType>Exe</OutputType>
</PropertyGroup>

Expand Down
Loading