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)); } }