[Font] Fix font face creation (#1010)
This commit is contained in:
parent
ddaf7ffe5b
commit
0c616d80ff
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue