Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
41 changes: 27 additions & 14 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
name: .NET Build

# Trigger the workflow on push or pull request
on: [push]
on:
push:
branches:
- main
- encoder
pull_request:
branches:
- main

jobs:
build:

strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
fail-fast: false

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v1

- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '10.0.x'

- name: Build with dotnet
working-directory: ./
run: dotnet build --configuration Release
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: Restore
working-directory: ./
run: dotnet restore

- name: Build
working-directory: ./
run: dotnet build --configuration Release

- name: Test
working-directory: ./
run: dotnet test --configuration Release --no-build
6 changes: 5 additions & 1 deletion HnmPlayer/App.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Themes.Fluent;
using System.Runtime.Versioning;
Comment on lines 1 to +4

namespace logo;

Expand All @@ -11,11 +12,14 @@ public override void Initialize()
Styles.Add(new FluentTheme());
}

[SupportedOSPlatform("windows")]
public override void OnFrameworkInitializationCompleted()
{
Comment on lines +15 to 17
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
desktop.MainWindow = PlayerLaunchContext.Current.Headless
? new HeadlessCaptureWindow()
: new MainWindow();
}

base.OnFrameworkInitializationCompleted();
Expand Down
34 changes: 34 additions & 0 deletions HnmPlayer/HeadlessCaptureWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using System.Diagnostics;

namespace logo;

public sealed class HeadlessCaptureWindow : Window
{
public HeadlessCaptureWindow()
{
Opened += OnOpened;
}

private async void OnOpened(object? sender, EventArgs e)
{
Debug.Assert(PlayerLaunchContext.Current.Headless, "Headless capture window should only be used in headless mode.");
if (string.IsNullOrWhiteSpace(PlayerLaunchContext.Current.InputPath))
{
throw new InvalidOperationException("Headless mode requires an input path.");
}

if (string.IsNullOrWhiteSpace(PlayerLaunchContext.Current.ScreenshotDirectory))
{
throw new InvalidOperationException("Headless mode requires a screenshot directory.");
}

string inputPath = PlayerLaunchContext.Current.InputPath;
string screenshotDirectory = PlayerLaunchContext.Current.ScreenshotDirectory;

await HeadlessPlayerRunner.RunAsync(inputPath, screenshotDirectory);
Close();
}
}
60 changes: 60 additions & 0 deletions HnmPlayer/HeadlessPlayerRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using System.Diagnostics;

namespace logo;

internal static class HeadlessPlayerRunner
{
public static async Task RunAsync(string inputPath, string screenshotDirectory)
{
Debug.Assert(!string.IsNullOrWhiteSpace(inputPath), "Input path must not be empty.");
Debug.Assert(!string.IsNullOrWhiteSpace(screenshotDirectory), "Screenshot directory must not be empty.");

Directory.CreateDirectory(screenshotDirectory);

var engine = new HnmPlaybackEngine();
await engine.LoadAsync(inputPath);

using WriteableBitmap bitmap = new(
new PixelSize(HnmPlaybackEngine.FrameWidth, HnmPlaybackEngine.FrameHeight),
new Vector(96, 96),
Avalonia.Platform.PixelFormat.Bgra8888,
AlphaFormat.Opaque);

int screenshotIndex = 0;
SaveScreenshot(engine, bitmap, Path.Combine(screenshotDirectory, FormatScreenshotName(screenshotIndex++)));

while (true)
{
HnmStepResult stepResult = engine.Step();
if (!stepResult.Advanced)
{
break;
}

if (stepResult.VisualChanged)
{
SaveScreenshot(engine, bitmap, Path.Combine(screenshotDirectory, FormatScreenshotName(screenshotIndex++)));
}
}
}

private static void SaveScreenshot(HnmPlaybackEngine engine, WriteableBitmap bitmap, string path)
{
engine.RenderCurrentFrame(bitmap);
WriteableBitmapToPng(bitmap, path);
}

private static void WriteableBitmapToPng(WriteableBitmap bitmap, string path)
{
using FileStream stream = File.Create(path);
bitmap.Save(stream);
}

private static string FormatScreenshotName(int index)
{
return $"frame{index:0000}.png";
}
}
4 changes: 4 additions & 0 deletions HnmPlayer/HnmPlayer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
<Compile Include="MainWindow.cs" />
<Compile Include="CirclesPaletteData.cs" />
<Compile Include="HnmPlaybackEngine.cs" />
<Compile Include="PlayerLaunchContext.cs" />
<Compile Include="HeadlessCaptureWindow.cs" />
<Compile Include="HeadlessPlayerRunner.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Headless" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
</ItemGroup>
</Project>
77 changes: 77 additions & 0 deletions HnmPlayer/PlayerLaunchContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
namespace logo;

internal static class PlayerLaunchContext
{
public static PlayerLaunchOptions Current { get; set; } = PlayerLaunchOptions.UiDefault;
}

public sealed record PlayerLaunchOptions(bool Headless, string? InputPath, string? ScreenshotDirectory)
{
public static PlayerLaunchOptions UiDefault { get; } = new(false, null, null);
}

internal static class PlayerCommandLineParser
{
public static PlayerLaunchOptions Parse(string[] args)
{
bool headless = false;
string? inputPath = null;
string? screenshotDirectory = null;

for (int i = 0; i < args.Length; i++)
{
string arg = args[i];
switch (arg)
{
case "--headless":
headless = true;
break;
case "--input":
inputPath = ReadValue(args, ref i, arg);
break;
case "--screenshot-dir":
screenshotDirectory = ReadValue(args, ref i, arg);
break;
default:
throw new InvalidOperationException($"Unknown argument '{arg}'.");
}
}

if (!headless)
{
return PlayerLaunchOptions.UiDefault;
}

if (string.IsNullOrWhiteSpace(inputPath))
{
throw new InvalidOperationException("Missing required argument --input for headless mode.");
}

if (string.IsNullOrWhiteSpace(screenshotDirectory))
{
throw new InvalidOperationException("Missing required argument --screenshot-dir for headless mode.");
}

return new PlayerLaunchOptions(
true,
Path.GetFullPath(inputPath),
Path.GetFullPath(screenshotDirectory));
}

private static string ReadValue(string[] args, ref int index, string argName)
{
if (index + 1 >= args.Length)
{
throw new InvalidOperationException($"Missing value after {argName}.");
}

index++;
string value = args[index];
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"Argument {argName} requires a non-empty value.");
}

return value;
}
}
22 changes: 18 additions & 4 deletions HnmPlayer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Headless;

namespace logo;

Expand All @@ -7,10 +8,23 @@ public static class Program
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
PlayerLaunchOptions launchOptions = PlayerCommandLineParser.Parse(args);
PlayerLaunchContext.Current = launchOptions;
BuildAvaloniaApp(launchOptions).StartWithClassicDesktopLifetime(args);
}

public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect();
public static AppBuilder BuildAvaloniaApp(PlayerLaunchOptions launchOptions)
{
AppBuilder builder = AppBuilder.Configure<App>();
if (launchOptions.Headless)
{
builder = builder.UseHeadless(new Avalonia.Headless.AvaloniaHeadlessPlatformOptions());
}
else
{
builder = builder.UsePlatformDetect();
}

return builder;
}
}
31 changes: 31 additions & 0 deletions logo.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnHsq", "tools\UnHsq\UnHsq.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HnmPlayer", "HnmPlayer\HnmPlayer.csproj", "{F8DEF2B9-AD93-47AF-B31F-AE852AA7ABD9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hnm0Encoder", "tools\Hnm0Encoder\Hnm0Encoder.csproj", "{6162235E-64B9-4E11-8B27-D0BAC2C24382}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HnmEndToEndTests", "tests\HnmEndToEndTests\HnmEndToEndTests.csproj", "{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -57,11 +63,36 @@ Global
{F8DEF2B9-AD93-47AF-B31F-AE852AA7ABD9}.Release|x64.Build.0 = Release|Any CPU
{F8DEF2B9-AD93-47AF-B31F-AE852AA7ABD9}.Release|x86.ActiveCfg = Release|Any CPU
{F8DEF2B9-AD93-47AF-B31F-AE852AA7ABD9}.Release|x86.Build.0 = Release|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Debug|x64.ActiveCfg = Debug|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Debug|x64.Build.0 = Debug|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Debug|x86.ActiveCfg = Debug|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Debug|x86.Build.0 = Debug|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Release|Any CPU.Build.0 = Release|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Release|x64.ActiveCfg = Release|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Release|x64.Build.0 = Release|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Release|x86.ActiveCfg = Release|Any CPU
{6162235E-64B9-4E11-8B27-D0BAC2C24382}.Release|x86.Build.0 = Release|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Debug|x64.ActiveCfg = Debug|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Debug|x64.Build.0 = Debug|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Debug|x86.ActiveCfg = Debug|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Debug|x86.Build.0 = Debug|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Release|Any CPU.Build.0 = Release|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Release|x64.ActiveCfg = Release|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Release|x64.Build.0 = Release|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Release|x86.ActiveCfg = Release|Any CPU
{7A6B7A63-ED7A-4E1A-9FE1-7E85F8B8D1A4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0113F160-F445-4A6E-A865-13CD1AC4C956} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
{6162235E-64B9-4E11-8B27-D0BAC2C24382} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
EndGlobalSection
EndGlobal
Loading
Loading