diff --git a/.jules/bolt.md b/.jules/bolt.md index e86e382be..e85c569e1 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -30,3 +30,8 @@ **Context:** `src/SharpEmu.Core/Memory/VirtualMemory.cs` (`FindInsertionIndex`) **Learning:** Standard C# `List` 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`, 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` 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. diff --git a/patch_test.sh b/patch_test.sh new file mode 100644 index 000000000..093f9727c --- /dev/null +++ b/patch_test.sh @@ -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 diff --git a/src/SharpEmu.Core/Memory/VirtualMemory.cs b/src/SharpEmu.Core/Memory/VirtualMemory.cs index 7496dc42f..813d08235 100644 --- a/src/SharpEmu.Core/Memory/VirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/VirtualMemory.cs @@ -127,6 +127,7 @@ public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) return true; } + /// Performance optimization: Uses a standard for-loop bounded by span.Length over Span<T> traversal rather than manual index increments to enable the .NET JIT compiler to completely elide array bounds checks on each region access. private bool TryValidateRange( ulong virtualAddress, int length, @@ -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 || @@ -172,8 +168,9 @@ private bool TryValidateRange( } currentAddress += (ulong)chunkLength; - currentIndex++; } + + return false; } private int FindContainingRegionIndex(ulong virtualAddress) diff --git a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs index 93a2658f7..604213871 100644 --- a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs +++ b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs @@ -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); @@ -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)); } } diff --git a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs.orig b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs.orig new file mode 100644 index 000000000..4188ea507 --- /dev/null +++ b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs.orig @@ -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)); + } +} diff --git a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs.patch b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs.patch new file mode 100644 index 000000000..95e6daa25 --- /dev/null +++ b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs.patch @@ -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)); + } + }