Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@
**Context:** `src/SharpEmu.Core/Memory/VirtualMemory.cs` (`FindInsertionIndex`)
**Learning:** Standard C# `List<T>` accesses inside high-frequency binary searches introduce unnecessary overhead via indexer property access and bounds checking. The same optimization pattern recently used in `PhysicalVirtualMemory.cs` (commit 980b47b) applies directly to `VirtualMemory.cs`. Bypassing this via `CollectionsMarshal.AsSpan(list)` completely elides these checks, turning the operation into direct O(1) span memory access.
**Action:** When optimizing binary search loops or hot paths over `List<T>`, immediately refactor to use `CollectionsMarshal.AsSpan()` to access elements and `span.Length` for bounds, alongside the `>>> 1` operator for division.

## 2026-09-16 - [VirtualMemory Span Traversal Optimization]
**Context:** src/SharpEmu.Core/Memory/VirtualMemory.cs (`TryValidateRange`)
**Learning:** In C#, iterating over a `Span<T>` using a `while` loop with manual index increments prevents the .NET JIT compiler from automatically eliding array bounds checks, introducing measurable overhead in hot-path memory validation loops.
**Action:** Replace `while` loops with manual index tracking over spans with a standard `for (var i = start; i < span.Length; i++)` loop to guarantee JIT bounds-check elision.
2 changes: 2 additions & 0 deletions patch_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sed -i 's/object Create(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target)/object Create(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target, bool preferLle = false)/' tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs
sed -i 's/return Activator.CreateInstance(modelType, containingType, methodName, shape, typedParameterKinds, libraryName, nid, exportName, target)!;/return Activator.CreateInstance(modelType, containingType, methodName, shape, typedParameterKinds, libraryName, nid, exportName, target, preferLle)!;/' tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs
13 changes: 5 additions & 8 deletions src/SharpEmu.Core/Memory/VirtualMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
return true;
}

/// <remarks>Performance optimization: Uses a standard for-loop bounded by span.Length over Span&lt;T&gt; traversal rather than manual index increments to enable the .NET JIT compiler to completely elide array bounds checks on each region access.</remarks>
private bool TryValidateRange(
ulong virtualAddress,
int length,
Expand All @@ -142,14 +143,9 @@ private bool TryValidateRange(
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_regions);
var currentAddress = virtualAddress;
var remaining = length;
var currentIndex = regionIndex;
while (true)
{
if (currentIndex >= span.Length)
{
return false;
}

for (var currentIndex = regionIndex; currentIndex < span.Length; currentIndex++)
{
ref var region = ref span[currentIndex];
if (currentAddress < region.Region.VirtualAddress ||
currentAddress >= region.EndAddress ||
Expand All @@ -172,8 +168,9 @@ private bool TryValidateRange(
}

currentAddress += (ulong)chunkLength;
currentIndex++;
}

return false;
}

private int FindContainingRegionIndex(ulong virtualAddress)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ public void ExportModelEqualityTests()
var modelType = generatorType.GetNestedType("ExportModel", System.Reflection.BindingFlags.NonPublic);
Assert.NotNull(modelType);

object Create(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target)
object Create(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target, bool preferLle = false)
{
return Activator.CreateInstance(modelType, containingType, methodName, shape, typedParameterKinds, libraryName, nid, exportName, target)!;
return Activator.CreateInstance(modelType, containingType, methodName, shape, typedParameterKinds, libraryName, nid, exportName, target, preferLle)!;
}

var baseModel = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1);
Expand Down Expand Up @@ -237,5 +237,8 @@ object Create(string containingType, string methodName, SysAbiExportShape.Handle

var diffTarget = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 2);
Assert.False(baseModel.Equals(diffTarget));

var diffPreferLle = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1, true);
Assert.False(baseModel.Equals(diffPreferLle));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

using Xunit;

namespace SharpEmu.SourceGenerators.Tests;

public sealed class SysAbiExportGeneratorTests
{
private const string HandlerSource = """
using SharpEmu.HLE;

namespace TestExports;

public static class SampleExports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int WaitSema(CpuContext ctx) => 0;

// NID omitted on purpose: the generator must derive it from the name.
[SysAbiExport(ExportName = "sceKernelSignalSema", Target = Generation.Gen5)]
public static int SignalSema(CpuContext ctx) => 0;

// Parameterless handler shape: must be wrapped to the SysAbiFunction contract.
[SysAbiExport(Nid = "ekNvsT22rsY", ExportName = "sceAudioOutOpen")]
public static int Open() => 0;

// Typed handler shape: the generator emits the SysV register thunk.
[SysAbiExport(Nid = "12wOHk8ywb0", ExportName = "sceKernelPollSema")]
public static int PollSema(CpuContext ctx, uint handle, int needCount) => 0;

// All four integer kinds across all six argument registers.
[SysAbiExport(Nid = "4DM06U2BNEY", ExportName = "sceKernelCancelSema")]
public static int CancelSema(CpuContext ctx, uint a, int b, ulong c, long d, uint e, int f) => 0;

// Guest string marshalling: the thunk reads the pointer before the handler.
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;

// A single fail-closed handler may back a catalog of LLE-preferred exports.
[SysAbiExport(Nid = "5fbPUzoA2fM", ExportName = "sceLleFirst", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
[SysAbiExport(Nid = "L9NfM+f4f1Y", ExportName = "sceLleSecond", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
public static int LleFallback(CpuContext ctx) => -1;
}
""";

[Fact]
public void GeneratedRegistryCompilesAgainstRealHleTypes()
{
var compilation = RoslynTestHost.Compile(HandlerSource);
var (updated, generated) = RoslynTestHost.RunGenerator(compilation);

Assert.NotEqual(string.Empty, generated);
RoslynTestHost.AssertCompiles(updated);
}

[Fact]
public void RegistryContainsDeclaredDerivedAndWrappedExports()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));

// Declared NID passes through verbatim.
Assert.Contains("\"Zxa0VhQVTsk\"", generated, StringComparison.Ordinal);
Assert.Contains("global::TestExports.SampleExports.WaitSema", generated, StringComparison.Ordinal);

// Omitted NID is derived from the export name at compile time.
Assert.Contains("\"4czppHBiriw\"", generated, StringComparison.Ordinal);

// Parameterless handlers are adapted to the SysAbiFunction shape.
Assert.Contains("static _ => global::TestExports.SampleExports.Open()", generated, StringComparison.Ordinal);
}

[Fact]
public void TypedHandlersGetSysVRegisterThunks()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));

// Parameters map positionally to RDI/RSI/... with the same unchecked-cast idiom
// hand-written handlers use; ulong reads the register raw.
Assert.Contains(
"static ctx => global::TestExports.SampleExports.PollSema(ctx, " +
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]))",
generated,
StringComparison.Ordinal);
Assert.Contains(
"static ctx => global::TestExports.SampleExports.CancelSema(ctx, " +
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]), " +
"ctx[global::SharpEmu.HLE.CpuRegister.Rdx], " +
"unchecked((long)ctx[global::SharpEmu.HLE.CpuRegister.Rcx]), " +
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.R8]), " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.R9]))",
generated,
StringComparison.Ordinal);
}

[Fact]
public void GuestCStringParametersAreMarshalledWithAFaultPath()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));

// The string is read from the pointer in RDI before the handler runs, and a
// failed read returns MEMORY_FAULT to the guest without invoking the handler.
Assert.Contains(
"if (!ctx.TryReadNullTerminatedUtf8(ctx[global::SharpEmu.HLE.CpuRegister.Rdi], 4096, out var guestString0))",
generated,
StringComparison.Ordinal);
Assert.Contains(
"return ctx.SetReturn(global::SharpEmu.HLE.OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);",
generated,
StringComparison.Ordinal);
Assert.Contains(
"return global::TestExports.SampleExports.KernelOpen(ctx, guestString0, " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]));",
generated,
StringComparison.Ordinal);
}

[Fact]
public void GenerationFilteringMatchesTheReflectionScanSemantics()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));

// The Add helper reproduces ResolveExportInfo: None inherits the registration
// generation, and exports outside the registration generation are skipped.
Assert.Contains("attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget", generated, StringComparison.Ordinal);
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
}

[Fact]
public void MultipleLlePreferredAttributesShareOneFailClosedHandler()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));

Assert.Contains("\"5fbPUzoA2fM\"", generated, StringComparison.Ordinal);
Assert.Contains("\"L9NfM+f4f1Y\"", generated, StringComparison.Ordinal);
Assert.Equal(
2,
generated.Split("global::TestExports.SampleExports.LleFallback", StringSplitOptions.None).Length - 1);
Assert.Contains(
", true, global::TestExports.SampleExports.LleFallback",
generated,
StringComparison.Ordinal);
}

[Fact]
public void AssemblyWithoutExportsEmitsNoRegistry()
{
// Referencing the analyzer must not mint a colliding
// SharpEmu.Generated.SysAbiExportRegistry type in export-free assemblies.
const string noExports = """
public static class PlainCode
{
public static int Nothing() => 0;
}
""";
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(noExports));

Assert.Equal(string.Empty, generated);
}

[Fact]
public void InvalidHandlersAreSkippedNotEmitted()
{
const string invalid = """
using SharpEmu.HLE;

namespace TestExports;

public static class BrokenExports
{
[SysAbiExport(ExportName = "sceKernelUsleep")]
public static long WrongReturn(CpuContext ctx) => 0;

[SysAbiExport(ExportName = "sceKernelGettimeofday")]
private static int Inaccessible(CpuContext ctx) => 0;
}
""";
var (updated, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(invalid));

Assert.DoesNotContain("WrongReturn", generated, StringComparison.Ordinal);
Assert.DoesNotContain("Inaccessible", generated, StringComparison.Ordinal);
RoslynTestHost.AssertCompiles(updated);
}

[Fact]
public void ExportModelEqualityTests()
{
var generatorType = typeof(SysAbiExportGenerator);
var modelType = generatorType.GetNestedType("ExportModel", System.Reflection.BindingFlags.NonPublic);
Assert.NotNull(modelType);

object Create(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target, bool preferLle = false)
{
return Activator.CreateInstance(modelType, containingType, methodName, shape, typedParameterKinds, libraryName, nid, exportName, target, preferLle)!;
}

var baseModel = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1);

// Equals(object? obj) and Equals(ExportModel? other) identical
var identical = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1);
Assert.True(baseModel.Equals(identical));
Assert.True(baseModel.Equals((object)identical));

// GetHashCode equality
Assert.Equal(baseModel.GetHashCode(), identical.GetHashCode());

// Equals(null)
Assert.False(baseModel.Equals(null));
Assert.False(baseModel.Equals((object?)null));

// Equals(wrong type)
Assert.False(baseModel.Equals(new object()));

// Different fields
var diffContainingType = Create("TypeB", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1);
Assert.False(baseModel.Equals(diffContainingType));

var diffMethodName = Create("TypeA", "MethodB", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1);
Assert.False(baseModel.Equals(diffMethodName));

var diffShape = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.Parameterless, "uint", "libA", "nidA", "expA", 1);
Assert.False(baseModel.Equals(diffShape));

var diffTypedKinds = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "int", "libA", "nidA", "expA", 1);
Assert.False(baseModel.Equals(diffTypedKinds));

var diffLibrary = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libB", "nidA", "expA", 1);
Assert.False(baseModel.Equals(diffLibrary));

var diffNid = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidB", "expA", 1);
Assert.False(baseModel.Equals(diffNid));

var diffExportName = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expB", 1);
Assert.False(baseModel.Equals(diffExportName));

var diffTarget = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 2);
Assert.False(baseModel.Equals(diffTarget));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs
+++ tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs
@@ -232,4 +232,7 @@

var diffTarget = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 2);
Assert.False(baseModel.Equals(diffTarget));
+
+ var diffPreferLle = Create("TypeA", "MethodA", SysAbiExportShape.HandlerShape.ContextOnly, "uint", "libA", "nidA", "expA", 1, true);
+ Assert.False(baseModel.Equals(diffPreferLle));
}
}