From 0c616d80ff1802b69a1e5f17fc973ffd363dbfd9 Mon Sep 17 00:00:00 2001
From: Berk <12572227+par274@users.noreply.github.com>
Date: Sat, 3 Oct 2026 05:04:56 +0300
Subject: [PATCH] [Font] Fix font face creation (#1010)
---
Directory.Packages.props | 3 +
src/SharpEmu.Libs/Font/FontExports.cs | 184 ++++++++++++------
src/SharpEmu.Libs/Font/FontFace.cs | 74 +++++++
src/SharpEmu.Libs/SharpEmu.Libs.csproj | 2 +
.../Font/FontExportsTests.cs | 97 ++++++++-
5 files changed, 294 insertions(+), 66 deletions(-)
create mode 100644 src/SharpEmu.Libs/Font/FontFace.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 16367a1..106aae7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,6 +22,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
+
diff --git a/src/SharpEmu.Libs/Font/FontExports.cs b/src/SharpEmu.Libs/Font/FontExports.cs
index 36bf98d..611b4a4 100644
--- a/src/SharpEmu.Libs/Font/FontExports.cs
+++ b/src/SharpEmu.Libs/Font/FontExports.cs
@@ -3,8 +3,9 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
-using System.Runtime.InteropServices;
+using System.Text;
using SharpEmu.HLE;
+using SkiaSharp;
namespace SharpEmu.Libs.Font;
@@ -18,8 +19,9 @@ public static class FontExports
private static readonly Stack FreeGlyphs = new();
private static ulong _librarySelectionAddress;
private static ulong _rendererSelectionAddress;
- private static readonly ConcurrentDictionary FontHeights = new();
- private static readonly Lazy BitmapFont = new(LoadBitmapFont);
+ private static readonly ConcurrentDictionary FontScales = new();
+ private static readonly ConcurrentDictionary FontFaces = new();
+ private static readonly Lazy DefaultFont = new(() => new(SKTypeface.FromFamilyName(null)));
[SysAbiExport(
Nid = "oaJ1BpN2FQk",
@@ -185,13 +187,14 @@ public static class FontExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
- // Baseline, line advance, decoration extent: scaled to the font's
- // current pixel height, matching the ascent convention (height * 0.75)
- // used by glyph metrics and the rasterizer. A font never given a
- // scale falls back to the default height of 16, reproducing the
- // original fixed geometry.
- var fontHeight = GetFontHeight(ctx[CpuRegister.Rdi]);
- var values = new[] { fontHeight * 0.75f, (float)fontHeight, 0.0f };
+ var fontHandle = ctx[CpuRegister.Rdi];
+ var scale = GetFontScale(fontHandle);
+ float[] values = [scale.Height * 0.75f, scale.Height, 0];
+ if (FontFaces.TryGetValue(fontHandle, out var face))
+ {
+ var metrics = face.GetMetrics(scale.Width, scale.Height);
+ values = [-metrics.Ascent, metrics.Descent - metrics.Ascent + metrics.Leading, 0];
+ }
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
@@ -228,8 +231,8 @@ public static class FontExports
// interpreted for vertical writing (e.g. CJK text rendered top-to-bottom).
// Scaled to the font's current pixel height; the default height of 16
// reproduces the original fixed geometry.
- var fontHeight = GetFontHeight(ctx[CpuRegister.Rdi]);
- var values = new[] { fontHeight * 0.5f, (float)fontHeight, 0.0f };
+ var scale = GetFontScale(ctx[CpuRegister.Rdi]);
+ var values = new[] { scale.Width * 0.5f, scale.Width, 0.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
@@ -249,16 +252,64 @@ public static class FontExports
ExportName = "sceFontOpenFontSet",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
- public static int OpenFontSet(CpuContext ctx) =>
- CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
+ public static int OpenFontSet(CpuContext ctx)
+ {
+ var fontSet = (uint)ctx[CpuRegister.Rsi];
+ var bold = (fontSet & 0xf) >= 7;
+ var italic = ((fontSet >> 20) & 0xf) == 1;
+ var style = bold ? (italic ? SKFontStyle.BoldItalic : SKFontStyle.Bold) :
+ (italic ? SKFontStyle.Italic : SKFontStyle.Normal);
+ return OpenFace(ctx, new(SKTypeface.FromFamilyName(null, style)));
+ }
[SysAbiExport(
Nid = "KXUpebrFk1U",
ExportName = "sceFontOpenFontMemory",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
- public static int OpenFontMemory(CpuContext ctx) =>
- CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
+ public static int OpenFontMemory(CpuContext ctx)
+ {
+ var address = ctx[CpuRegister.Rsi];
+ var size = (uint)ctx[CpuRegister.Rdx];
+ if (address == 0 || size is < 12 or > 64 * 1024 * 1024 || ctx[CpuRegister.R8] == 0)
+ {
+ return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ }
+ if (!ctx.Memory.CanRead(address, size))
+ {
+ return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+ var bytes = new byte[(int)size];
+ if (!ctx.Memory.TryRead(address, bytes))
+ {
+ return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+ uint index = 0;
+ if (ctx[CpuRegister.Rcx] != 0 && !ctx.TryReadUInt32(ctx[CpuRegister.Rcx] + 8, out index))
+ {
+ return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+ if (index > int.MaxValue)
+ {
+ return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ }
+ using var data = SKData.CreateCopy(bytes);
+ var typeface = SKTypeface.FromData(data, (int)index);
+ return typeface is null
+ ? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT)
+ : OpenFace(ctx, new(typeface));
+ }
+
+ private static int OpenFace(CpuContext ctx, FontFace face)
+ {
+ var result = CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
+ if (result == 0 && ctx.TryReadUInt64(ctx[CpuRegister.R8], out var handle))
+ {
+ FontFaces[handle] = face;
+ FontScales.TryRemove(handle, out _);
+ }
+ return result;
+ }
[SysAbiExport(
Nid = "JzCH3SCFnAU",
@@ -277,6 +328,8 @@ public static class FontExports
if (setupHandle != 0)
{
+ if (FontFaces.TryGetValue(sourceHandle, out var face)) FontFaces[setupHandle] = face;
+ FontScales[setupHandle] = GetFontScale(sourceHandle);
return ctx.TryWriteUInt64(outputAddress, setupHandle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -297,6 +350,8 @@ public static class FontExports
}
_ = TryWriteUInt16(ctx, handle, 0x0F02);
+ if (FontFaces.TryGetValue(sourceHandle, out var sourceFace)) FontFaces[handle] = sourceFace;
+ FontScales[handle] = GetFontScale(sourceHandle);
return ctx.TryWriteUInt64(outputAddress, handle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -336,7 +391,9 @@ public static class FontExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
- return WriteGlyphMetrics(ctx, metricsAddress, GetFontHeight(ctx[CpuRegister.Rdi]))
+ var code = (uint)ctx[CpuRegister.Rsi];
+ if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ return WriteGlyphMetrics(ctx, metricsAddress, GetGlyph(ctx[CpuRegister.Rdi], code), GetFontScale(ctx[CpuRegister.Rdi]).Height)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -495,15 +552,14 @@ public static class FontExports
var surfaceAddress = ctx[CpuRegister.Rdx];
var metricsAddress = ctx[CpuRegister.Rcx];
var resultAddress = ctx[CpuRegister.R8];
- var height = GetFontHeight(ctx[CpuRegister.Rdi]);
- var width = Math.Clamp((height + 1) / 2, 4, 128);
- var rows = BitmapFont.Value;
- if (rows is null)
- {
- return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
- }
+ var font = ctx[CpuRegister.Rdi];
+ var code = (uint)ctx[CpuRegister.Rsi];
+ if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ var glyph = GetGlyph(font, code);
+ var width = glyph.Width;
+ var height = glyph.Height;
- if (metricsAddress != 0 && !WriteGlyphMetrics(ctx, metricsAddress, height))
+ if (metricsAddress != 0 && !WriteGlyphMetrics(ctx, metricsAddress, glyph, GetFontScale(font).Height))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -512,8 +568,8 @@ public static class FontExports
ctx.GetXmmRegister(1, out var yBits, out _);
var x = BitConverter.UInt32BitsToSingle((uint)xBits);
var y = BitConverter.UInt32BitsToSingle((uint)yBits);
- var left = float.IsFinite(x) ? Math.Max((int)x, 0) : 0;
- var top = float.IsFinite(y) ? Math.Max((int)(y - height * 0.75f), 0) : 0;
+ var left = float.IsFinite(x) ? (int)MathF.Floor(x + glyph.BearingX) : 0;
+ var top = float.IsFinite(y) ? (int)MathF.Floor(y - glyph.BearingY) : 0;
ulong buffer = 0;
uint pitch = 0;
@@ -530,13 +586,14 @@ public static class FontExports
BinaryPrimitives.WriteUInt32LittleEndian(result[16..], pitch);
result[20] = (byte)pixelSize;
}
- BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)left);
- BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)top);
- BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)width);
- BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)height);
- BinaryPrimitives.WriteSingleLittleEndian(result[44..], height * 0.75f);
- BinaryPrimitives.WriteSingleLittleEndian(result[48..], width);
- BinaryPrimitives.WriteSingleLittleEndian(result[52..], width);
+ BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)Math.Max(left, 0));
+ BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)Math.Max(top, 0));
+ BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)Math.Max(width + Math.Min(left, 0), 0));
+ BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)Math.Max(height + Math.Min(top, 0), 0));
+ BinaryPrimitives.WriteSingleLittleEndian(result[40..], glyph.BearingX);
+ BinaryPrimitives.WriteSingleLittleEndian(result[44..], glyph.BearingY);
+ BinaryPrimitives.WriteSingleLittleEndian(result[48..], glyph.Advance);
+ BinaryPrimitives.WriteSingleLittleEndian(result[52..], glyph.Advance);
BinaryPrimitives.WriteUInt32LittleEndian(result[56..], (uint)width);
BinaryPrimitives.WriteUInt32LittleEndian(result[60..], (uint)height);
if (!ctx.Memory.TryWrite(resultAddress, result))
@@ -545,7 +602,7 @@ public static class FontExports
}
}
- if (surfaceAddress != 0 && !DrawGlyph(ctx, surfaceAddress, (uint)ctx[CpuRegister.Rsi], left, top, width, height, rows))
+ if (surfaceAddress != 0 && !DrawGlyph(ctx, surfaceAddress, left, top, width, height, glyph.Coverage))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -555,23 +612,34 @@ public static class FontExports
private static int SetFontHeight(CpuContext ctx)
{
+ ctx.GetXmmRegister(0, out var widthBits, out _);
ctx.GetXmmRegister(1, out var heightBits, out _);
+ var width = BitConverter.UInt32BitsToSingle((uint)widthBits);
var height = BitConverter.UInt32BitsToSingle((uint)heightBits);
if (float.IsFinite(height) && height > 1)
{
- FontHeights[ctx[CpuRegister.Rdi]] = Math.Clamp((int)(height + 0.5f), 8, 128);
+ FontScales[ctx[CpuRegister.Rdi]] = (
+ float.IsFinite(width) && width > 1 ? Math.Clamp(width, 8, 128) : Math.Clamp(height, 8, 128),
+ Math.Clamp(height, 8, 128));
}
return SetSuccess(ctx);
}
- private static int GetFontHeight(ulong font) =>
- FontHeights.TryGetValue(font, out var height) ? (int)height : 16;
+ private static (float Width, float Height) GetFontScale(ulong font) =>
+ FontScales.TryGetValue(font, out var scale) ? scale : (16, 16);
- private static bool WriteGlyphMetrics(CpuContext ctx, ulong address, int height)
+ private static bool IsValidCode(uint code) => (code & 0xffff0000) == 0x80000000 || Rune.IsValid((int)code);
+
+ private static FontGlyph GetGlyph(ulong font, uint code)
+ {
+ var scale = GetFontScale(font);
+ return (FontFaces.TryGetValue(font, out var face) ? face : DefaultFont.Value).GetGlyph(code, scale.Width, scale.Height);
+ }
+
+ private static bool WriteGlyphMetrics(CpuContext ctx, ulong address, FontGlyph glyph, float height)
{
- var width = Math.Clamp((height + 1) / 2, 4, 128);
Span metrics = stackalloc byte[32];
- float[] values = [width, height, 0, height * 0.75f, width, 0, 0, height];
+ float[] values = [glyph.Width, glyph.Height, glyph.BearingX, glyph.BearingY, glyph.Advance, 0, 0, height];
for (var index = 0; index < values.Length; index++)
{
BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]);
@@ -579,8 +647,8 @@ public static class FontExports
return ctx.Memory.TryWrite(address, metrics);
}
- internal static bool DrawGlyph(CpuContext ctx, ulong surface, uint code, int left, int top,
- int glyphWidth, int glyphHeight, byte[] font)
+ internal static bool DrawGlyph(CpuContext ctx, ulong surface, int left, int top,
+ int glyphWidth, int glyphHeight, byte[] coverage)
{
if (!ctx.TryReadUInt64(surface, out var buffer) ||
!ctx.TryReadUInt32(surface + 8, out var pitch) ||
@@ -600,7 +668,6 @@ public static class FontExports
return true;
}
- code = code < 256 ? code : (uint)'?';
var x0 = Math.Max(left, (int)Math.Min(sx0, width));
var y0 = Math.Max(top, (int)Math.Min(sy0, height));
var x1 = Math.Min(left + glyphWidth, (int)Math.Min(sx1, width));
@@ -620,11 +687,12 @@ public static class FontExports
}
for (var xx = x0; xx < x1; xx++)
{
- var sourceY = (yy - top) * 16 / glyphHeight;
- var sourceX = (xx - left) * 8 / glyphWidth;
- if ((font[code * 16 + sourceY] & (0x80 >> sourceX)) != 0)
+ var alpha = coverage[(yy - top) * glyphWidth + xx - left];
+ if (alpha != 0)
{
- line.AsSpan((xx - x0) * (int)pixelSize, (int)pixelSize).Fill(0xff);
+ var pixel = line.AsSpan((xx - x0) * (int)pixelSize, (int)pixelSize);
+ for (var component = 0; component < pixel.Length; component++)
+ pixel[component] = (byte)(alpha + (pixel[component] * (255 - alpha) + 127) / 255);
}
}
if (!ctx.Memory.TryWrite(address, line))
@@ -635,25 +703,17 @@ public static class FontExports
return true;
}
- private static byte[]? LoadBitmapFont()
- {
- var library = Path.Combine(AppContext.BaseDirectory, "plugins", "avutil-59.dll");
- if (!NativeLibrary.TryLoad(library, out var handle) ||
- !NativeLibrary.TryGetExport(handle, "avpriv_vga16_font", out var data))
- {
- return null;
- }
- var font = new byte[4096];
- Marshal.Copy(data, font, 0, font.Length);
- return font;
- }
-
[SysAbiExport(
Nid = "vzHs3C8lWJk",
ExportName = "sceFontCloseFont",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
- public static int CloseFont(CpuContext ctx) => SetSuccess(ctx);
+ public static int CloseFont(CpuContext ctx)
+ {
+ FontFaces.TryRemove(ctx[CpuRegister.Rdi], out _);
+ FontScales.TryRemove(ctx[CpuRegister.Rdi], out _);
+ return SetSuccess(ctx);
+ }
[SysAbiExport(
Nid = "1QjhKxrsOB8",
diff --git a/src/SharpEmu.Libs/Font/FontFace.cs b/src/SharpEmu.Libs/Font/FontFace.cs
new file mode 100644
index 0000000..33a4b3a
--- /dev/null
+++ b/src/SharpEmu.Libs/Font/FontFace.cs
@@ -0,0 +1,74 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Collections.Concurrent;
+using System.Runtime.InteropServices;
+using SkiaSharp;
+
+namespace SharpEmu.Libs.Font;
+
+internal sealed class FontFace(SKTypeface typeface)
+{
+ private readonly ConcurrentDictionary<(uint Code, float Width, float Height), FontGlyph> _glyphs = new();
+
+ public SKFontMetrics GetMetrics(float width, float height)
+ {
+ using var font = CreateFont(width, height);
+ return font.Metrics;
+ }
+
+ public FontGlyph GetGlyph(uint code, float width, float height)
+ {
+ // ponytail: bounded working set; clearing avoids retaining every glyph at every scale.
+ if (_glyphs.Count >= 1024) _glyphs.Clear();
+ return _glyphs.GetOrAdd((code, width, height), key => Rasterize(key.Code, key.Width, key.Height));
+ }
+
+ private SKFont CreateFont(float width, float height) => new(typeface, height, width / height)
+ {
+ Edging = SKFontEdging.Antialias,
+ Subpixel = false,
+ };
+
+ private FontGlyph Rasterize(uint code, float width, float height)
+ {
+ using var font = CreateFont(width, height);
+ var glyph = (code & 0x80000000) != 0 ? (ushort)code : font.GetGlyph((int)code);
+ if (glyph == 0 && (code & 0x80000000) == 0)
+ {
+ // System font sets span multiple faces. Resolve by Unicode coverage, not language.
+ var fallback = SKFontManager.Default.MatchCharacter(typeface.FamilyName, typeface.FontStyle, null, (int)code);
+ if (fallback is not null)
+ {
+ font.Typeface = fallback;
+ glyph = font.GetGlyph((int)code);
+ }
+ }
+
+ ReadOnlySpan glyphs = [glyph];
+ var advance = font.MeasureText(glyphs, out var bounds);
+ var left = (int)MathF.Floor(bounds.Left);
+ var top = (int)MathF.Floor(bounds.Top);
+ var bitmapWidth = (int)MathF.Ceiling(bounds.Right) - left;
+ var bitmapHeight = (int)MathF.Ceiling(bounds.Bottom) - top;
+ if (bitmapWidth <= 0 || bitmapHeight <= 0)
+ {
+ return new(0, 0, left, -top, advance, []);
+ }
+
+ using var bitmap = new SKBitmap(bitmapWidth, bitmapHeight, SKColorType.Alpha8, SKAlphaType.Premul);
+ using var canvas = new SKCanvas(bitmap);
+ using var paint = new SKPaint { Color = SKColors.White, IsAntialias = true };
+ using var blob = SKTextBlob.Create(MemoryMarshal.AsBytes(glyphs), SKTextEncoding.GlyphId, font);
+ canvas.Clear(SKColors.Transparent);
+ canvas.DrawText(blob!, -left, -top, paint);
+ var coverage = new byte[bitmapWidth * bitmapHeight];
+ for (var row = 0; row < bitmapHeight; row++)
+ {
+ Marshal.Copy(bitmap.GetPixels() + row * bitmap.RowBytes, coverage, row * bitmapWidth, bitmapWidth);
+ }
+ return new(bitmapWidth, bitmapHeight, left, -top, advance, coverage);
+ }
+}
+
+internal sealed record FontGlyph(int Width, int Height, float BearingX, float BearingY, float Advance, byte[] Coverage);
diff --git a/src/SharpEmu.Libs/SharpEmu.Libs.csproj b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
index 1d0c882..85486c5 100644
--- a/src/SharpEmu.Libs/SharpEmu.Libs.csproj
+++ b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
@@ -35,6 +35,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
diff --git a/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs b/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs
index eb21982..a056322 100644
--- a/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs
+++ b/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs
@@ -4,6 +4,7 @@
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Font;
+using SkiaSharp;
using Xunit;
namespace SharpEmu.Libs.Tests.Font;
@@ -117,12 +118,100 @@ public sealed class FontExportsTests
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[36..], 1);
Assert.True(_ctx.Memory.TryWrite(surface, descriptor));
- var font = new byte[4096];
- font['A' * 16] = 0xff;
- Assert.True(FontExports.DrawGlyph(_ctx, surface, 'A', 0, 0, 8, 1, font));
+ byte[] coverage = [255, 255, 255, 128, 0, 255, 255, 255];
+ Assert.True(FontExports.DrawGlyph(_ctx, surface, 0, 0, 8, 1, coverage));
Span result = stackalloc byte[8];
Assert.True(_ctx.Memory.TryRead(pixels, result));
- Assert.Equal(new byte[] { 0, 0, 255, 255, 255, 255, 0, 0 }, result.ToArray());
+ Assert.Equal(new byte[] { 0, 0, 255, 128, 0, 255, 0, 0 }, result.ToArray());
+ }
+
+ [Fact]
+ public void SystemFont_RendersUnicodeAcrossScriptsInsteadOfQuestionMarks()
+ {
+ const ulong surface = Base + 0x200;
+ const ulong pixels = Base + 0x300;
+ _ctx[CpuRegister.R8] = Base + 0x80;
+ Assert.Equal(0, FontExports.OpenFontSet(_ctx));
+ Assert.True(_ctx.TryReadUInt64(Base + 0x80, out var font));
+ _ctx[CpuRegister.Rdi] = surface;
+ _ctx[CpuRegister.Rsi] = pixels;
+ _ctx[CpuRegister.Rdx] = 32;
+ _ctx[CpuRegister.Rcx] = 1;
+ _ctx[CpuRegister.R8] = 32;
+ _ctx[CpuRegister.R9] = 32;
+ Assert.Equal(0, FontExports.RenderSurfaceInit(_ctx));
+ _ctx[CpuRegister.Rdi] = font;
+ _ctx[CpuRegister.Rdx] = surface;
+ _ctx[CpuRegister.Rcx] = Base + 0x100;
+ _ctx.SetXmmRegister(0, BitConverter.SingleToUInt32Bits(4), 0);
+ _ctx.SetXmmRegister(1, BitConverter.SingleToUInt32Bits(24), 0);
+ var bitmaps = new List();
+ try
+ {
+ // Latin extended, Greek, Cyrillic, and characters needing a fallback face.
+ foreach (var code in new uint[] { '?', 0x0130, 0x011E, 0x0141, 0x03A9, 0x0416, 0x05D0, 0x0628 })
+ {
+ Assert.True(_memory.TryWrite(pixels, new byte[1024]));
+ _ctx[CpuRegister.Rsi] = code;
+ _ctx[CpuRegister.R8] = 0;
+ Assert.Equal(0, FontExports.RenderCharGlyphImageHorizontal(_ctx));
+ var bitmap = new byte[1024];
+ Assert.True(_memory.TryRead(pixels, bitmap));
+ Assert.Contains(bitmap, pixel => pixel != 0);
+ Assert.DoesNotContain(bitmaps, previous => previous.SequenceEqual(bitmap));
+ bitmaps.Add(bitmap);
+ _ctx[CpuRegister.Rdx] = Base + 0x180;
+ Assert.Equal(0, FontExports.GetCharGlyphMetrics(_ctx));
+ _ctx[CpuRegister.Rdx] = surface;
+ var renderedMetrics = new byte[32];
+ var queriedMetrics = new byte[32];
+ Assert.True(_memory.TryRead(Base + 0x100, renderedMetrics));
+ Assert.True(_memory.TryRead(Base + 0x180, queriedMetrics));
+ Assert.Equal(renderedMetrics, queriedMetrics);
+ }
+ _ctx[CpuRegister.Rsi] = 0xD800; // A surrogate is not a Unicode scalar.
+ Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, FontExports.RenderCharGlyphImageHorizontal(_ctx));
+ }
+ finally
+ {
+ Assert.Equal(0, FontExports.CloseFont(_ctx));
+ }
+ }
+
+ [Fact]
+ public void OpenFontMemory_UsesGuestFontAndPreservesItInInstances()
+ {
+ using var typeface = SKTypeface.FromFamilyName(null);
+ using var stream = typeface.OpenStream(out var index);
+ var bytes = new byte[stream.Length];
+ Assert.Equal(bytes.Length, stream.Read(bytes, bytes.Length));
+ var memory = new FakeCpuMemory(Base, bytes.Length + 0x2000);
+ var ctx = new CpuContext(memory, Generation.Gen5);
+ Assert.True(memory.TryWrite(Base + 0x1000, bytes));
+ Assert.True(ctx.TryWriteUInt32(Base + 8, (uint)index));
+ ctx[CpuRegister.Rsi] = Base + 0x1000;
+ ctx[CpuRegister.Rdx] = (uint)bytes.Length;
+ ctx[CpuRegister.Rcx] = Base;
+ ctx[CpuRegister.R8] = Base + 0x100;
+ Assert.Equal(0, FontExports.OpenFontMemory(ctx));
+ Assert.True(ctx.TryReadUInt64(Base + 0x100, out var source));
+ ctx[CpuRegister.Rdi] = source;
+ ctx[CpuRegister.Rsi] = Base + 0x500;
+ ctx[CpuRegister.Rdx] = Base + 0x108;
+ Assert.Equal(0, FontExports.OpenFontInstance(ctx));
+ Assert.True(ctx.TryReadUInt64(Base + 0x108, out var instance));
+ Assert.Equal(Base + 0x500, instance);
+ Assert.Equal(0, FontExports.CloseFont(ctx));
+ // The instance still owns the same face after the source handle is closed.
+ ctx[CpuRegister.Rdi] = instance;
+ ctx[CpuRegister.Rsi] = 0x0130;
+ ctx[CpuRegister.Rdx] = Base + 0x200;
+ Assert.Equal(0, FontExports.GetCharGlyphMetrics(ctx));
+ var metrics = new byte[32];
+ Assert.True(memory.TryRead(Base + 0x200, metrics));
+ using var font = new SKFont(typeface, 16);
+ Assert.Equal(font.MeasureText("\u0130"), BinaryPrimitives.ReadSingleLittleEndian(metrics.AsSpan(16)));
+ Assert.Equal(0, FontExports.CloseFont(ctx));
}
}