[Font] Fix font face creation (#1010)

This commit is contained in:
Berk 2026-10-03 05:04:56 +03:00 committed by GitHub
parent ddaf7ffe5b
commit 0c616d80ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 294 additions and 66 deletions

View File

@ -22,6 +22,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" /> <PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" /> <PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" /> <PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<!-- Reuse the font rasterizer already shipped with Avalonia.Skia. -->
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.0" /> <PackageVersion Include="System.IO.Hashing" Version="10.0.0" />
<!-- Transitive of Avalonia.Desktop; pinned. Avalonia 12 requires 0.94.1+. --> <!-- Transitive of Avalonia.Desktop; pinned. Avalonia 12 requires 0.94.1+. -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" /> <PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />

View File

@ -3,8 +3,9 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.InteropServices; using System.Text;
using SharpEmu.HLE; using SharpEmu.HLE;
using SkiaSharp;
namespace SharpEmu.Libs.Font; namespace SharpEmu.Libs.Font;
@ -18,8 +19,9 @@ public static class FontExports
private static readonly Stack<ulong> FreeGlyphs = new(); private static readonly Stack<ulong> FreeGlyphs = new();
private static ulong _librarySelectionAddress; private static ulong _librarySelectionAddress;
private static ulong _rendererSelectionAddress; private static ulong _rendererSelectionAddress;
private static readonly ConcurrentDictionary<ulong, float> FontHeights = new(); private static readonly ConcurrentDictionary<ulong, (float Width, float Height)> FontScales = new();
private static readonly Lazy<byte[]?> BitmapFont = new(LoadBitmapFont); private static readonly ConcurrentDictionary<ulong, FontFace> FontFaces = new();
private static readonly Lazy<FontFace> DefaultFont = new(() => new(SKTypeface.FromFamilyName(null)));
[SysAbiExport( [SysAbiExport(
Nid = "oaJ1BpN2FQk", Nid = "oaJ1BpN2FQk",
@ -185,13 +187,14 @@ public static class FontExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
// Baseline, line advance, decoration extent: scaled to the font's var fontHandle = ctx[CpuRegister.Rdi];
// current pixel height, matching the ascent convention (height * 0.75) var scale = GetFontScale(fontHandle);
// used by glyph metrics and the rasterizer. A font never given a float[] values = [scale.Height * 0.75f, scale.Height, 0];
// scale falls back to the default height of 16, reproducing the if (FontFaces.TryGetValue(fontHandle, out var face))
// original fixed geometry. {
var fontHeight = GetFontHeight(ctx[CpuRegister.Rdi]); var metrics = face.GetMetrics(scale.Width, scale.Height);
var values = new[] { fontHeight * 0.75f, (float)fontHeight, 0.0f }; values = [-metrics.Ascent, metrics.Descent - metrics.Ascent + metrics.Leading, 0];
}
for (var index = 0; index < values.Length; index++) for (var index = 0; index < values.Length; index++)
{ {
if (!TryWriteUInt32( if (!TryWriteUInt32(
@ -228,8 +231,8 @@ public static class FontExports
// interpreted for vertical writing (e.g. CJK text rendered top-to-bottom). // 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 // Scaled to the font's current pixel height; the default height of 16
// reproduces the original fixed geometry. // reproduces the original fixed geometry.
var fontHeight = GetFontHeight(ctx[CpuRegister.Rdi]); var scale = GetFontScale(ctx[CpuRegister.Rdi]);
var values = new[] { fontHeight * 0.5f, (float)fontHeight, 0.0f }; var values = new[] { scale.Width * 0.5f, scale.Width, 0.0f };
for (var index = 0; index < values.Length; index++) for (var index = 0; index < values.Length; index++)
{ {
if (!TryWriteUInt32( if (!TryWriteUInt32(
@ -249,16 +252,64 @@ public static class FontExports
ExportName = "sceFontOpenFontSet", ExportName = "sceFontOpenFontSet",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceFont")] LibraryName = "libSceFont")]
public static int OpenFontSet(CpuContext ctx) => public static int OpenFontSet(CpuContext ctx)
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02); {
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( [SysAbiExport(
Nid = "KXUpebrFk1U", Nid = "KXUpebrFk1U",
ExportName = "sceFontOpenFontMemory", ExportName = "sceFontOpenFontMemory",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceFont")] LibraryName = "libSceFont")]
public static int OpenFontMemory(CpuContext ctx) => public static int OpenFontMemory(CpuContext ctx)
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02); {
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( [SysAbiExport(
Nid = "JzCH3SCFnAU", Nid = "JzCH3SCFnAU",
@ -277,6 +328,8 @@ public static class FontExports
if (setupHandle != 0) if (setupHandle != 0)
{ {
if (FontFaces.TryGetValue(sourceHandle, out var face)) FontFaces[setupHandle] = face;
FontScales[setupHandle] = GetFontScale(sourceHandle);
return ctx.TryWriteUInt64(outputAddress, setupHandle) return ctx.TryWriteUInt64(outputAddress, setupHandle)
? SetSuccess(ctx) ? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -297,6 +350,8 @@ public static class FontExports
} }
_ = TryWriteUInt16(ctx, handle, 0x0F02); _ = TryWriteUInt16(ctx, handle, 0x0F02);
if (FontFaces.TryGetValue(sourceHandle, out var sourceFace)) FontFaces[handle] = sourceFace;
FontScales[handle] = GetFontScale(sourceHandle);
return ctx.TryWriteUInt64(outputAddress, handle) return ctx.TryWriteUInt64(outputAddress, handle)
? SetSuccess(ctx) ? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : 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 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) ? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
@ -495,15 +552,14 @@ public static class FontExports
var surfaceAddress = ctx[CpuRegister.Rdx]; var surfaceAddress = ctx[CpuRegister.Rdx];
var metricsAddress = ctx[CpuRegister.Rcx]; var metricsAddress = ctx[CpuRegister.Rcx];
var resultAddress = ctx[CpuRegister.R8]; var resultAddress = ctx[CpuRegister.R8];
var height = GetFontHeight(ctx[CpuRegister.Rdi]); var font = ctx[CpuRegister.Rdi];
var width = Math.Clamp((height + 1) / 2, 4, 128); var code = (uint)ctx[CpuRegister.Rsi];
var rows = BitmapFont.Value; if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
if (rows is null) var glyph = GetGlyph(font, code);
{ var width = glyph.Width;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
@ -512,8 +568,8 @@ public static class FontExports
ctx.GetXmmRegister(1, out var yBits, out _); ctx.GetXmmRegister(1, out var yBits, out _);
var x = BitConverter.UInt32BitsToSingle((uint)xBits); var x = BitConverter.UInt32BitsToSingle((uint)xBits);
var y = BitConverter.UInt32BitsToSingle((uint)yBits); var y = BitConverter.UInt32BitsToSingle((uint)yBits);
var left = float.IsFinite(x) ? Math.Max((int)x, 0) : 0; var left = float.IsFinite(x) ? (int)MathF.Floor(x + glyph.BearingX) : 0;
var top = float.IsFinite(y) ? Math.Max((int)(y - height * 0.75f), 0) : 0; var top = float.IsFinite(y) ? (int)MathF.Floor(y - glyph.BearingY) : 0;
ulong buffer = 0; ulong buffer = 0;
uint pitch = 0; uint pitch = 0;
@ -530,13 +586,14 @@ public static class FontExports
BinaryPrimitives.WriteUInt32LittleEndian(result[16..], pitch); BinaryPrimitives.WriteUInt32LittleEndian(result[16..], pitch);
result[20] = (byte)pixelSize; result[20] = (byte)pixelSize;
} }
BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)left); BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)Math.Max(left, 0));
BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)top); BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)Math.Max(top, 0));
BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)width); BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)Math.Max(width + Math.Min(left, 0), 0));
BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)height); BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)Math.Max(height + Math.Min(top, 0), 0));
BinaryPrimitives.WriteSingleLittleEndian(result[44..], height * 0.75f); BinaryPrimitives.WriteSingleLittleEndian(result[40..], glyph.BearingX);
BinaryPrimitives.WriteSingleLittleEndian(result[48..], width); BinaryPrimitives.WriteSingleLittleEndian(result[44..], glyph.BearingY);
BinaryPrimitives.WriteSingleLittleEndian(result[52..], width); BinaryPrimitives.WriteSingleLittleEndian(result[48..], glyph.Advance);
BinaryPrimitives.WriteSingleLittleEndian(result[52..], glyph.Advance);
BinaryPrimitives.WriteUInt32LittleEndian(result[56..], (uint)width); BinaryPrimitives.WriteUInt32LittleEndian(result[56..], (uint)width);
BinaryPrimitives.WriteUInt32LittleEndian(result[60..], (uint)height); BinaryPrimitives.WriteUInt32LittleEndian(result[60..], (uint)height);
if (!ctx.Memory.TryWrite(resultAddress, result)) 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
@ -555,23 +612,34 @@ public static class FontExports
private static int SetFontHeight(CpuContext ctx) private static int SetFontHeight(CpuContext ctx)
{ {
ctx.GetXmmRegister(0, out var widthBits, out _);
ctx.GetXmmRegister(1, out var heightBits, out _); ctx.GetXmmRegister(1, out var heightBits, out _);
var width = BitConverter.UInt32BitsToSingle((uint)widthBits);
var height = BitConverter.UInt32BitsToSingle((uint)heightBits); var height = BitConverter.UInt32BitsToSingle((uint)heightBits);
if (float.IsFinite(height) && height > 1) 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); return SetSuccess(ctx);
} }
private static int GetFontHeight(ulong font) => private static (float Width, float Height) GetFontScale(ulong font) =>
FontHeights.TryGetValue(font, out var height) ? (int)height : 16; 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<byte> metrics = stackalloc byte[32]; Span<byte> 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++) for (var index = 0; index < values.Length; index++)
{ {
BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]); BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]);
@ -579,8 +647,8 @@ public static class FontExports
return ctx.Memory.TryWrite(address, metrics); return ctx.Memory.TryWrite(address, metrics);
} }
internal static bool DrawGlyph(CpuContext ctx, ulong surface, uint code, int left, int top, internal static bool DrawGlyph(CpuContext ctx, ulong surface, int left, int top,
int glyphWidth, int glyphHeight, byte[] font) int glyphWidth, int glyphHeight, byte[] coverage)
{ {
if (!ctx.TryReadUInt64(surface, out var buffer) || if (!ctx.TryReadUInt64(surface, out var buffer) ||
!ctx.TryReadUInt32(surface + 8, out var pitch) || !ctx.TryReadUInt32(surface + 8, out var pitch) ||
@ -600,7 +668,6 @@ public static class FontExports
return true; return true;
} }
code = code < 256 ? code : (uint)'?';
var x0 = Math.Max(left, (int)Math.Min(sx0, width)); var x0 = Math.Max(left, (int)Math.Min(sx0, width));
var y0 = Math.Max(top, (int)Math.Min(sy0, height)); var y0 = Math.Max(top, (int)Math.Min(sy0, height));
var x1 = Math.Min(left + glyphWidth, (int)Math.Min(sx1, width)); 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++) for (var xx = x0; xx < x1; xx++)
{ {
var sourceY = (yy - top) * 16 / glyphHeight; var alpha = coverage[(yy - top) * glyphWidth + xx - left];
var sourceX = (xx - left) * 8 / glyphWidth; if (alpha != 0)
if ((font[code * 16 + sourceY] & (0x80 >> sourceX)) != 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)) if (!ctx.Memory.TryWrite(address, line))
@ -635,25 +703,17 @@ public static class FontExports
return true; 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( [SysAbiExport(
Nid = "vzHs3C8lWJk", Nid = "vzHs3C8lWJk",
ExportName = "sceFontCloseFont", ExportName = "sceFontCloseFont",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceFont")] 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( [SysAbiExport(
Nid = "1QjhKxrsOB8", Nid = "1QjhKxrsOB8",

View File

@ -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<ushort> 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);

View File

@ -35,6 +35,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Silk.NET.Vulkan" /> <PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="System.IO.Hashing" /> <PackageReference Include="System.IO.Hashing" />
</ItemGroup> </ItemGroup>

View File

@ -4,6 +4,7 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Font; using SharpEmu.Libs.Font;
using SkiaSharp;
using Xunit; using Xunit;
namespace SharpEmu.Libs.Tests.Font; namespace SharpEmu.Libs.Tests.Font;
@ -117,12 +118,100 @@ public sealed class FontExportsTests
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[36..], 1); BinaryPrimitives.WriteUInt32LittleEndian(descriptor[36..], 1);
Assert.True(_ctx.Memory.TryWrite(surface, descriptor)); Assert.True(_ctx.Memory.TryWrite(surface, descriptor));
var font = new byte[4096]; byte[] coverage = [255, 255, 255, 128, 0, 255, 255, 255];
font['A' * 16] = 0xff; Assert.True(FontExports.DrawGlyph(_ctx, surface, 0, 0, 8, 1, coverage));
Assert.True(FontExports.DrawGlyph(_ctx, surface, 'A', 0, 0, 8, 1, font));
Span<byte> result = stackalloc byte[8]; Span<byte> result = stackalloc byte[8];
Assert.True(_ctx.Memory.TryRead(pixels, result)); 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<byte[]>();
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));
} }
} }