engine: add exact Windows GDI glyph backend

This commit is contained in:
gamer147
2026-07-30 18:35:46 -04:00
parent 7397e4e5fb
commit 6a8c919df1
10 changed files with 679 additions and 7 deletions

View File

@@ -22,6 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\Age.Engine\Age.Engine.csproj" />
<ProjectReference Include="..\Age.Engine.Text.Windows\Age.Engine.Text.Windows.csproj" />
<Compile Include="..\..\godot\IMovieDecoder.cs" Link="IMovieDecoder.cs" />
<Compile Include="..\..\godot\MovieRuntime.cs" Link="MovieRuntime.cs" />
<Compile Include="..\..\godot\MovieAudioTimeline.cs" Link="MovieAudioTimeline.cs" />

View File

@@ -0,0 +1,272 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using Age.Engine.Text;
using Age.Engine.Text.Windows;
public class WindowsGdiGlyphMaskRasterizerTests
{
private const uint GgoGray4Bitmap = 5;
private const uint GdiError = 0xffffffff;
[Fact]
public void BackendSelectionIsExplicitAndDiagnostic()
{
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out string availability))
{
Assert.NotEmpty(availability);
return;
}
using var rasterizer = new WindowsGdiGlyphMaskRasterizer(fontCacheCapacity: 2);
Assert.Equal(
new GlyphRasterizerBackendInfo(
"windows-gdi-gray4",
"Windows GDI Gray-4 (AGE reference)",
GlyphRasterPolicy.NativeCp932Gray4,
NativePixelExact: true,
"CreateICA(\"DISPLAY\") + CreateFontIndirectA + GetGlyphOutlineA(GGO_GRAY4_BITMAP), ACP 932"),
rasterizer.BackendInfo);
var portable = new GlyphRasterRequest(
" 明朝", 24, -12, 700, 0x3042, null, GlyphRasterPolicy.PortableUnicode);
Assert.Throws<ArgumentException>(() => rasterizer.Rasterize(portable));
}
[Theory]
[InlineData(" 明朝", 24, -12, 0, 0x3042)] // あ
[InlineData(" 明朝", 24, -12, 700, 0x59eb)] // 姫
[InlineData(" ゴシック", 16, -8, 700, 0x30a2)] // ア
public void AnsiCp932BackendMatchesIndependentUnicodeGdiOracle(
string face, int height, int width, int weight, int scalar)
{
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
GlyphRasterRequest request = NativeRequest(face, height, width, weight, scalar);
using var rasterizer = new WindowsGdiGlyphMaskRasterizer();
GlyphMask actual = rasterizer.Rasterize(request);
DirectGlyph expected = RasterizeUnicodeDirect(face, height, width, weight, scalar);
Assert.Equal(
(expected.Width, expected.Height, expected.Stride,
expected.OriginX, expected.OriginY,
expected.CellAdvanceX, expected.CellAdvanceY,
expected.CellWidth, expected.CellHeight),
(actual.Width, actual.Height, actual.Stride,
actual.OriginX, actual.OriginY,
actual.CellAdvanceX, actual.CellAdvanceY,
actual.CellWidth, actual.CellHeight));
Assert.Equal(expected.Coverage, actual.Coverage.ToArray());
Assert.All(actual.Coverage.ToArray(), value => Assert.InRange(value, (byte)0, (byte)16));
}
[Fact]
public void FontHandlesUseTheSharedBoundedLruAndDisposeCleanly()
{
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
var rasterizer = new WindowsGdiGlyphMaskRasterizer(fontCacheCapacity: 1);
rasterizer.Rasterize(NativeRequest(" 明朝", 24, -12, 0, 0x3042));
rasterizer.Rasterize(NativeRequest(" 明朝", 24, -12, 700, 0x3042));
Assert.Equal(1, rasterizer.FontCacheCount);
Assert.Equal(1, rasterizer.FontCacheCapacity);
rasterizer.Dispose();
Assert.Throws<ObjectDisposedException>(
() => rasterizer.Rasterize(NativeRequest(" 明朝", 24, -12, 700, 0x3042)));
}
private static GlyphRasterRequest NativeRequest(
string face, int height, int width, int weight, int scalar)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
byte[] encoded = Encoding.GetEncoding(
932, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback)
.GetBytes(char.ConvertFromUtf32(scalar));
Assert.InRange(encoded.Length, 1, 2);
ushort cp932 = encoded.Length == 1
? encoded[0]
: (ushort)((encoded[0] << 8) | encoded[1]);
return new GlyphRasterRequest(
face, height, width, weight, scalar, cp932, GlyphRasterPolicy.NativeCp932Gray4);
}
private static DirectGlyph RasterizeUnicodeDirect(
string face, int height, int width, int weight, int scalar)
{
IntPtr dc = Native.CreateICW("DISPLAY", null, null, IntPtr.Zero);
if (dc == IntPtr.Zero) ThrowWin32("CreateICW failed");
IntPtr font = IntPtr.Zero;
IntPtr previous = IntPtr.Zero;
try
{
var logFont = new LogFontW
{
Height = -height,
Width = width,
Weight = weight,
CharSet = 1,
FaceName = face,
};
font = Native.CreateFontIndirectW(ref logFont);
if (font == IntPtr.Zero) ThrowWin32("CreateFontIndirectW failed");
previous = Native.SelectObject(dc, font);
if (previous == IntPtr.Zero || previous == new IntPtr(-1))
ThrowWin32("SelectObject failed");
Mat2 identity = Mat2.Identity;
uint size = Native.GetGlyphOutlineW(
dc, (uint)scalar, GgoGray4Bitmap,
out GlyphMetrics metrics, 0, null, ref identity);
if (size == GdiError) ThrowWin32("GetGlyphOutlineW query failed");
int glyphWidth = checked((int)metrics.BlackBoxX);
int glyphHeight = checked((int)metrics.BlackBoxY);
int stride = checked((glyphWidth + 3) & ~3);
Assert.Equal(checked(stride * glyphHeight), (int)size);
byte[] coverage = new byte[size];
if (size > 0)
{
identity = Mat2.Identity;
uint written = Native.GetGlyphOutlineW(
dc, (uint)scalar, GgoGray4Bitmap,
out GlyphMetrics second, size, coverage, ref identity);
Assert.Equal(size, written);
Assert.Equal(metrics, second);
}
string text = char.ConvertFromUtf32(scalar);
if (!Native.GetTextExtentPoint32W(dc, text, text.Length, out NativeSize cell))
ThrowWin32("GetTextExtentPoint32W failed");
return new DirectGlyph(
glyphWidth, glyphHeight, stride,
metrics.GlyphOrigin.X, metrics.GlyphOrigin.Y,
metrics.CellIncrementX, metrics.CellIncrementY,
cell.Width, cell.Height, coverage);
}
finally
{
if (previous != IntPtr.Zero && previous != new IntPtr(-1))
Native.SelectObject(dc, previous);
if (font != IntPtr.Zero) Native.DeleteObject(font);
Native.DeleteDC(dc);
}
}
private static void ThrowWin32(string message)
=> throw new Win32Exception(Marshal.GetLastPInvokeError(), message);
private sealed record DirectGlyph(
int Width,
int Height,
int Stride,
int OriginX,
int OriginY,
int CellAdvanceX,
int CellAdvanceY,
int CellWidth,
int CellHeight,
byte[] Coverage);
[StructLayout(LayoutKind.Sequential)]
private struct NativePoint
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
private struct NativeSize
{
public int Width;
public int Height;
}
[StructLayout(LayoutKind.Sequential)]
private struct GlyphMetrics
{
public uint BlackBoxX;
public uint BlackBoxY;
public NativePoint GlyphOrigin;
public short CellIncrementX;
public short CellIncrementY;
}
[StructLayout(LayoutKind.Sequential)]
private struct Fixed
{
public ushort Fraction;
public short Value;
public static Fixed One => new() { Value = 1 };
}
[StructLayout(LayoutKind.Sequential)]
private struct Mat2
{
public Fixed M11;
public Fixed M12;
public Fixed M21;
public Fixed M22;
public static Mat2 Identity => new() { M11 = Fixed.One, M22 = Fixed.One };
}
[StructLayout(LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
private struct LogFontW
{
public int Height;
public int Width;
public int Escapement;
public int Orientation;
public int Weight;
public byte Italic;
public byte Underline;
public byte StrikeOut;
public byte CharSet;
public byte OutPrecision;
public byte ClipPrecision;
public byte Quality;
public byte PitchAndFamily;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string FaceName;
}
private static class Native
{
[DllImport("gdi32.dll", EntryPoint = "CreateICW", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr CreateICW(
string driver, string? device, string? output, IntPtr initializationData);
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC(IntPtr deviceContext);
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(IntPtr gdiObject);
[DllImport("gdi32.dll", EntryPoint = "CreateFontIndirectW", SetLastError = true)]
public static extern IntPtr CreateFontIndirectW(ref LogFontW logFont);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr deviceContext, IntPtr gdiObject);
[DllImport("gdi32.dll", EntryPoint = "GetGlyphOutlineW", SetLastError = true)]
public static extern uint GetGlyphOutlineW(
IntPtr deviceContext,
uint character,
uint format,
out GlyphMetrics metrics,
uint bufferSize,
[Out] byte[]? buffer,
ref Mat2 transform);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W", CharSet = CharSet.Unicode,
SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetTextExtentPoint32W(
IntPtr deviceContext,
string text,
int length,
out NativeSize size);
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Age.Engine\Age.Engine.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,345 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
using Age.Engine.Text;
using Microsoft.Win32.SafeHandles;
namespace Age.Engine.Text.Windows;
/// <summary>
/// Exact Windows reference backend for AGE's CreateICA/CreateFontIndirectA/GetGlyphOutlineA
/// GGO_GRAY4_BITMAP path. It is opt-in and requires the Japanese ANSI system code page.
/// </summary>
public sealed class WindowsGdiGlyphMaskRasterizer
: IIdentifiedGlyphMaskRasterizer, IDisposable
{
private const uint GgoGray4Bitmap = 5;
private const uint GdiError = 0xffffffff;
private const byte DefaultCharset = 1;
private static readonly Encoding Cp932 = CreateCp932();
private readonly object _gate = new();
private readonly SafeDeviceContextHandle _displayIc;
private readonly BoundedLruCache<FontKey, SafeGdiObjectHandle> _fonts;
private bool _disposed;
private readonly record struct FontKey(
string Face, int PixelHeight, int RequestedWidth, int Weight);
public WindowsGdiGlyphMaskRasterizer(int fontCacheCapacity = 16)
{
if (fontCacheCapacity <= 0)
throw new ArgumentOutOfRangeException(nameof(fontCacheCapacity));
if (!TryGetAvailability(out string reason))
throw new PlatformNotSupportedException(reason);
IntPtr dc = NativeMethods.CreateICA("DISPLAY", null, null, IntPtr.Zero);
if (dc == IntPtr.Zero) ThrowWin32("CreateICA(\"DISPLAY\") failed");
_displayIc = new SafeDeviceContextHandle(dc);
_fonts = new BoundedLruCache<FontKey, SafeGdiObjectHandle>(
fontCacheCapacity, font => font.Dispose());
}
public static bool TryGetAvailability(out string reason)
{
if (!OperatingSystem.IsWindows())
{
reason = "Windows GDI is unavailable on this operating system.";
return false;
}
uint activeCodePage = NativeMethods.GetACP();
if (activeCodePage != 932)
{
reason =
$"Exact AGE ANSI rasterization requires Windows system code page 932; active ACP is {activeCodePage}.";
return false;
}
reason = "Windows GDI DISPLAY information context with Japanese ANSI code page 932.";
return true;
}
public GlyphRasterizerBackendInfo BackendInfo => new(
"windows-gdi-gray4",
"Windows GDI Gray-4 (AGE reference)",
GlyphRasterPolicy.NativeCp932Gray4,
NativePixelExact: true,
Detail: "CreateICA(\"DISPLAY\") + CreateFontIndirectA + GetGlyphOutlineA(GGO_GRAY4_BITMAP), ACP 932");
public int FontCacheCount => _fonts.Count;
public int FontCacheCapacity => _fonts.Capacity;
public GlyphMask Rasterize(GlyphRasterRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (request.Policy != GlyphRasterPolicy.NativeCp932Gray4 || request.Cp932Code == null)
throw new ArgumentException(
"The Windows GDI reference backend accepts only native CP932 gray-4 requests.",
nameof(request));
lock (_gate)
{
ObjectDisposedException.ThrowIf(_disposed, this);
SafeGdiObjectHandle font = GetOrCreateFont(request);
IntPtr previous = NativeMethods.SelectObject(
_displayIc.DangerousGetHandle(), font.DangerousGetHandle());
if (previous == IntPtr.Zero || previous == new IntPtr(-1))
ThrowWin32("SelectObject(font) failed");
try
{
return RasterizeSelectedFont(request);
}
finally
{
NativeMethods.SelectObject(_displayIc.DangerousGetHandle(), previous);
}
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed) return;
_disposed = true;
_fonts.Clear();
_displayIc.Dispose();
}
GC.SuppressFinalize(this);
}
private GlyphMask RasterizeSelectedFont(GlyphRasterRequest request)
{
ushort code = request.Cp932Code!.Value;
if (code == 0) throw new ArgumentException("CP932 NUL is not a drawable glyph.", nameof(request));
Mat2 identity = Mat2.Identity;
uint size = NativeMethods.GetGlyphOutlineA(
_displayIc.DangerousGetHandle(), code, GgoGray4Bitmap,
out GlyphMetrics metrics, 0, null, ref identity);
if (size == GdiError) ThrowWin32($"GetGlyphOutlineA query failed for CP932 0x{code:x4}");
int width = checked((int)metrics.BlackBoxX);
int height = checked((int)metrics.BlackBoxY);
int stride = checked((width + 3) & ~3);
int expectedBytes = checked(stride * height);
if (size != expectedBytes)
throw new InvalidOperationException(
$"GDI gray-4 buffer size {size} disagrees with {width}x{height}, stride {stride}.");
byte[] coverage = new byte[expectedBytes];
if (size > 0)
{
identity = Mat2.Identity;
uint written = NativeMethods.GetGlyphOutlineA(
_displayIc.DangerousGetHandle(), code, GgoGray4Bitmap,
out GlyphMetrics secondMetrics, size, coverage, ref identity);
if (written == GdiError) ThrowWin32($"GetGlyphOutlineA read failed for CP932 0x{code:x4}");
if (written != size || !metrics.Equals(secondMetrics))
throw new InvalidOperationException("GDI glyph metrics changed between query and read.");
}
byte[] encoded = EncodeCp932Code(code);
if (!NativeMethods.GetTextExtentPoint32A(
_displayIc.DangerousGetHandle(), encoded, encoded.Length, out NativeSize cell))
ThrowWin32($"GetTextExtentPoint32A failed for CP932 0x{code:x4}");
return new GlyphMask(
width, height, stride,
metrics.GlyphOrigin.X, metrics.GlyphOrigin.Y,
metrics.CellIncrementX, metrics.CellIncrementY,
cell.Width, cell.Height,
coverage);
}
private SafeGdiObjectHandle GetOrCreateFont(GlyphRasterRequest request)
{
var key = new FontKey(
request.FontFace, request.PixelHeight, request.RequestedWidth, request.Weight);
if (_fonts.TryGetValue(key, out SafeGdiObjectHandle? cached)) return cached;
var logFont = new LogFontA
{
Height = -request.PixelHeight,
Width = request.RequestedWidth,
Weight = request.Weight,
CharSet = DefaultCharset,
FaceName = EncodeFaceName(request.FontFace),
};
IntPtr raw = NativeMethods.CreateFontIndirectA(ref logFont);
if (raw == IntPtr.Zero)
ThrowWin32($"CreateFontIndirectA failed for '{request.FontFace}'");
var created = new SafeGdiObjectHandle(raw);
_fonts.Set(key, created);
return created;
}
private static byte[] EncodeFaceName(string face)
{
byte[] encoded;
try
{
encoded = Cp932.GetBytes(face);
}
catch (EncoderFallbackException error)
{
throw new ArgumentException($"Font face '{face}' is not representable in CP932.", nameof(face), error);
}
if (encoded.Length > 31)
throw new ArgumentException("A LOGFONTA face name cannot exceed 31 encoded bytes.", nameof(face));
var result = new byte[32];
encoded.CopyTo(result, 0);
return result;
}
private static byte[] EncodeCp932Code(ushort code)
=> code <= byte.MaxValue
? [(byte)code]
: [(byte)(code >> 8), (byte)code];
private static Encoding CreateCp932()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(
932, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
}
[DoesNotReturn]
private static void ThrowWin32(string message)
=> throw new Win32Exception(Marshal.GetLastPInvokeError(), message);
[StructLayout(LayoutKind.Sequential)]
private struct NativePoint : IEquatable<NativePoint>
{
public int X;
public int Y;
public readonly bool Equals(NativePoint other) => X == other.X && Y == other.Y;
public override readonly bool Equals(object? obj) => obj is NativePoint other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(X, Y);
}
[StructLayout(LayoutKind.Sequential)]
private struct NativeSize
{
public int Width;
public int Height;
}
[StructLayout(LayoutKind.Sequential)]
private struct GlyphMetrics : IEquatable<GlyphMetrics>
{
public uint BlackBoxX;
public uint BlackBoxY;
public NativePoint GlyphOrigin;
public short CellIncrementX;
public short CellIncrementY;
public readonly bool Equals(GlyphMetrics other)
=> BlackBoxX == other.BlackBoxX
&& BlackBoxY == other.BlackBoxY
&& GlyphOrigin.Equals(other.GlyphOrigin)
&& CellIncrementX == other.CellIncrementX
&& CellIncrementY == other.CellIncrementY;
public override readonly bool Equals(object? obj)
=> obj is GlyphMetrics other && Equals(other);
public override readonly int GetHashCode()
=> HashCode.Combine(
BlackBoxX, BlackBoxY, GlyphOrigin, CellIncrementX, CellIncrementY);
}
[StructLayout(LayoutKind.Sequential)]
private struct Fixed
{
public ushort Fraction;
public short Value;
public static Fixed One => new() { Value = 1 };
}
[StructLayout(LayoutKind.Sequential)]
private struct Mat2
{
public Fixed M11;
public Fixed M12;
public Fixed M21;
public Fixed M22;
public static Mat2 Identity => new() { M11 = Fixed.One, M22 = Fixed.One };
}
[StructLayout(LayoutKind.Sequential)]
private struct LogFontA
{
public int Height;
public int Width;
public int Escapement;
public int Orientation;
public int Weight;
public byte Italic;
public byte Underline;
public byte StrikeOut;
public byte CharSet;
public byte OutPrecision;
public byte ClipPrecision;
public byte Quality;
public byte PitchAndFamily;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] FaceName;
}
private sealed class SafeDeviceContextHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeDeviceContextHandle(IntPtr handle) : base(ownsHandle: true) => SetHandle(handle);
protected override bool ReleaseHandle() => NativeMethods.DeleteDC(handle);
}
private sealed class SafeGdiObjectHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeGdiObjectHandle(IntPtr handle) : base(ownsHandle: true) => SetHandle(handle);
protected override bool ReleaseHandle() => NativeMethods.DeleteObject(handle);
}
private static class NativeMethods
{
[DllImport("kernel32.dll")]
public static extern uint GetACP();
[DllImport("gdi32.dll", EntryPoint = "CreateICA", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern IntPtr CreateICA(
string driver, string? device, string? output, IntPtr initializationData);
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC(IntPtr deviceContext);
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(IntPtr gdiObject);
[DllImport("gdi32.dll", EntryPoint = "CreateFontIndirectA", SetLastError = true)]
public static extern IntPtr CreateFontIndirectA(ref LogFontA logFont);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr deviceContext, IntPtr gdiObject);
[DllImport("gdi32.dll", EntryPoint = "GetGlyphOutlineA", SetLastError = true)]
public static extern uint GetGlyphOutlineA(
IntPtr deviceContext,
uint character,
uint format,
out GlyphMetrics metrics,
uint bufferSize,
[Out] byte[]? buffer,
ref Mat2 transform);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32A", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetTextExtentPoint32A(
IntPtr deviceContext,
byte[] text,
int length,
out NativeSize size);
}
}

View File

@@ -110,3 +110,15 @@ public interface IGlyphMaskRasterizer
{
GlyphMask Rasterize(GlyphRasterRequest request);
}
public sealed record GlyphRasterizerBackendInfo(
string Id,
string DisplayName,
GlyphRasterPolicy Policy,
bool NativePixelExact,
string Detail);
public interface IIdentifiedGlyphMaskRasterizer : IGlyphMaskRasterizer
{
GlyphRasterizerBackendInfo BackendInfo { get; }
}

View File

@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Cli", "Age.Cli\Age.Cli.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Tests", "Age.Engine.Tests\Age.Engine.Tests.csproj", "{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Text.Windows", "Age.Engine.Text.Windows\Age.Engine.Text.Windows.csproj", "{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -30,5 +32,9 @@ Global
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.Build.0 = Release|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal