sharpemu/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs

218 lines
9.8 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Font;
using SkiaSharp;
using Xunit;
namespace SharpEmu.Libs.Tests.Font;
public sealed class FontExportsTests
{
private const ulong Base = 0x1_0000_0000;
private const ulong LayoutAddress = Base + 0x100;
private readonly FakeCpuMemory _memory = new(Base, 0x1000);
private readonly CpuContext _ctx;
public FontExportsTests()
{
_ctx = new CpuContext(_memory, Generation.Gen5);
}
[Fact]
public void TextSourceInit_WritesParserAndTextRangeWithoutOverwritingNextObject()
{
const ulong text = Base + 0x300;
const ulong parser = Base + 0x400;
const ulong parserObject = Base + 0x500;
var bytes = new byte[0x68];
Array.Fill(bytes, (byte)0xcc);
Assert.True(_memory.TryWrite(LayoutAddress, bytes));
_ctx[CpuRegister.Rdi] = LayoutAddress;
_ctx[CpuRegister.Rsi] = text;
_ctx[CpuRegister.Rdx] = 9;
_ctx[CpuRegister.Rcx] = parser;
_ctx[CpuRegister.R8] = parserObject;
Assert.Equal(0, FontExports.TextSourceInit(_ctx));
Assert.True(_memory.TryRead(LayoutAddress, bytes));
Assert.Equal(0UL, BinaryPrimitives.ReadUInt64LittleEndian(bytes));
Assert.Equal(text, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x08)));
Assert.Equal(text + 9, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x10)));
Assert.Equal(text, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x18)));
Assert.Equal(parser, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x20)));
Assert.Equal(parserObject, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x28)));
Assert.Equal(0UL, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x30)));
Assert.Equal(0x10UL, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x38)));
Assert.Equal(9UL, BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(0x40)));
Assert.All(bytes[0x48..0x60], value => Assert.Equal((byte)0, value));
Assert.All(bytes[0x60..], value => Assert.Equal((byte)0xcc, value));
}
// SceFontHorizontalLayout is three floats; the sentinel directly after
// them must survive the call.
[Fact]
public void GetHorizontalLayout_WritesExactlyThreeFloats()
{
const uint Sentinel = 0xDEADBEEF;
Span<byte> sentinelBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(sentinelBytes, Sentinel);
Assert.True(_ctx.Memory.TryWrite(LayoutAddress + 12, sentinelBytes));
_ctx[CpuRegister.Rsi] = LayoutAddress;
Assert.Equal(0, FontExports.GetHorizontalLayout(_ctx));
Span<byte> layout = stackalloc byte[16];
Assert.True(_ctx.Memory.TryRead(LayoutAddress, layout));
Assert.Equal(12.0f, BinaryPrimitives.ReadSingleLittleEndian(layout));
Assert.Equal(16.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[4..]));
Assert.Equal(0.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[8..]));
Assert.Equal(Sentinel, BinaryPrimitives.ReadUInt32LittleEndian(layout[12..]));
}
[Fact]
public void GetVerticalLayout_WritesExactlyThreeFloats()
{
const uint Sentinel = 0xDEADBEEF;
Span<byte> sentinelBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(sentinelBytes, Sentinel);
Assert.True(_ctx.Memory.TryWrite(LayoutAddress + 12, sentinelBytes));
_ctx[CpuRegister.Rsi] = LayoutAddress;
Assert.Equal(0, FontExports.GetVerticalLayout(_ctx));
Span<byte> layout = stackalloc byte[16];
Assert.True(_ctx.Memory.TryRead(LayoutAddress, layout));
Assert.Equal(8.0f, BinaryPrimitives.ReadSingleLittleEndian(layout));
Assert.Equal(16.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[4..]));
Assert.Equal(0.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[8..]));
Assert.Equal(Sentinel, BinaryPrimitives.ReadUInt32LittleEndian(layout[12..]));
}
[Fact]
public void GetVerticalLayout_NullBuffer_ReturnsInvalidArgument()
{
_ctx[CpuRegister.Rsi] = 0;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
FontExports.GetVerticalLayout(_ctx));
}
[Fact]
public void DrawGlyph_WritesOnlySetPixelsInsideScissor()
{
const ulong surface = Base + 0x200;
const ulong pixels = Base + 0x300;
Span<byte> descriptor = stackalloc byte[0x28];
BinaryPrimitives.WriteUInt64LittleEndian(descriptor, pixels);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[8..], 8);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[12..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[16..], 8);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[20..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[24..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[28..], 0);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[32..], 6);
BinaryPrimitives.WriteUInt32LittleEndian(descriptor[36..], 1);
Assert.True(_ctx.Memory.TryWrite(surface, descriptor));
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, 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));
}
}