Compare commits
1 Commits
1016_part3
...
font_fixes
| Author | SHA1 | Date |
|---|---|---|
|
|
179b9dcb79 |
|
|
@ -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.Extensions.EXT" 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" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned. Avalonia 12 requires 0.94.1+. -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
|
||||
|
|
|
|||
|
|
@ -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<ulong> FreeGlyphs = new();
|
||||
private static ulong _librarySelectionAddress;
|
||||
private static ulong _rendererSelectionAddress;
|
||||
private static readonly ConcurrentDictionary<ulong, float> FontHeights = new();
|
||||
private static readonly Lazy<byte[]?> BitmapFont = new(LoadBitmapFont);
|
||||
private static readonly ConcurrentDictionary<ulong, (float Width, float Height)> FontScales = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FontFace> FontFaces = new();
|
||||
private static readonly Lazy<FontFace> DefaultFont = new(() => new(SKTypeface.FromFamilyName(null)));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "whrS4oksXc4",
|
||||
|
|
@ -156,13 +158,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(
|
||||
|
|
@ -199,8 +202,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(
|
||||
|
|
@ -220,16 +223,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",
|
||||
|
|
@ -248,6 +299,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);
|
||||
|
|
@ -268,6 +321,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);
|
||||
|
|
@ -307,7 +362,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);
|
||||
}
|
||||
|
|
@ -466,15 +523,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);
|
||||
}
|
||||
|
|
@ -483,8 +539,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;
|
||||
|
|
@ -501,13 +557,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))
|
||||
|
|
@ -516,7 +573,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);
|
||||
}
|
||||
|
|
@ -526,23 +583,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<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++)
|
||||
{
|
||||
BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]);
|
||||
|
|
@ -550,8 +618,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) ||
|
||||
|
|
@ -571,7 +639,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));
|
||||
|
|
@ -591,11 +658,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))
|
||||
|
|
@ -606,25 +674,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",
|
||||
|
|
|
|||
|
|
@ -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.Extensions.EXT" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="System.IO.Hashing" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Font;
|
||||
using SkiaSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Font;
|
||||
|
|
@ -87,12 +88,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<byte> 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<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