[Perf] Behavior-preserving hot-path allocation/LINQ wins (#264)

* [Perf] Gate event-flag tracing so it allocates nothing when disabled

TraceEventFlag built its interpolated argument (and, on the wait path,
FormatFrameChain + a new StringBuilder(256) in FormatGuestWaitObject plus
~12 guest-memory reads) on every call, then checked the env var inside the
method — so every sceKernelSetEventFlag/Clear/Poll/Wait paid a string
allocation and an Environment.GetEnvironmentVariable P/Invoke even with
tracing off. Cache the flag once in a static readonly bool and gate every
call site, matching the semaphore/event-queue pattern. Behavior is
unchanged when SHARPEMU_LOG_EVENT_FLAG=1.

* [Perf] Hoist IsNoBlockLeaf classification to import-stub setup

The leaf-dispatch path called IsNoBlockLeafImport(nid) — a ~30-literal
string pattern match — on every leaf import. IsLeaf/NidHash were already
precomputed on ImportStubEntry at stub setup; add IsNoBlockLeaf alongside
them and read the field in the hot path. Behavior unchanged.

* [Perf] Cache trace env-var flags read on hot paths

Two SHARPEMU_LOG_* env vars were read via Environment.GetEnvironmentVariable
(a P/Invoke + transient string) on hot paths: SHARPEMU_LOG_FIBER on every
fiber context transfer, and SHARPEMU_LOG_DIRECT_MEMORY on every direct-memory
op (~8 sites). Cache both once — _logFiber alongside the other backend _log*
flags, and _traceDirectMemory behind the existing ShouldTraceDirectMemory
helper. Behavior unchanged.

* [Perf] Avoid per-iteration thread snapshot in the idle pump loop

PumpUntilGuestThreadsIdle allocated a full GuestThreadState[] snapshot
(via LINQ Values.ToArray()) on every spin just to tally three run-state
booleans. Tally them under the lock with an allocation-free helper, and
only materialize the snapshot inside the gated (default-off) diagnostic
dump. SnapshotGuestThreads now uses Values.CopyTo instead of LINQ.
Behavior unchanged.

* [Perf] De-LINQ GetPixelColorExportMask on the per-draw path

GetPixelColorExportMask ran a Select/OfType/Where/Aggregate chain over all
shader instructions, allocating iterators + closures, and is called per
render target (twice per draw via CreateRenderState and HasPixelColorExport).
Replace with a manual scan producing the identical mask — no allocation, and
it removes an authored-LINQ use the repo bans.

* [Perf] De-LINQ per-draw render-target selection in AgcExports

The bound-render-target selection used Where(...).OrderBy(...).ToArray()
(plus a second Where/ToArray fallback) on every translated draw, allocating
LINQ iterators + closures. Replace with an explicit filter into a pre-sized
list + List.Sort by slot; slots are distinct so this matches the stable
OrderBy. Same result, no per-draw LINQ allocations.

* [Perf] De-LINQ per-draw render-target validation in the Vulkan presenter

SubmitOffscreenTranslatedDraw validated its render targets with
targets.Any(...) twice plus targets.Select(a=>a.Address).Distinct().Count()
(a per-draw HashSet), and broadcast a single blend with
Enumerable.Repeat(...).ToArray(). Targets are <= 8, so replace with manual
scans (invalid-target check; combined dimension-mismatch + pairwise
aliasing check) and Array.Fill. Same results, no per-draw LINQ allocations.

* [Perf] Binary-search VirtualQuery region lookup (SortedList)

_mappedRegions was an unordered Dictionary, so TryFindVirtualQueryRegionLocked
scanned every region for containment/next — O(n) per sceKernelVirtualQuery and
O(n^2) when an allocator walks the address space with the findNext flag. Store
regions in a SortedList keyed by base address (every write already uses the
region's own address as the key) and find the containing/next region with a
binary search over the sorted keys. Also drops a now-redundant Values.OrderBy.

Non-overlapping regions assumed (mmap semantics), so only the floor region can
contain the query. Behavior-preserving; worth spot-checking VirtualQuery-heavy
titles.

* [Perf] Remove stray CLI packages.lock.json committed by mistake

git add -A in an earlier commit swept in a regenerated
src/SharpEmu.CLI/packages.lock.json. main tracks no lock files (central
package management, no RestorePackagesWithLockFile), and REUSE.toml no
longer covers packages.lock.json, so the committed file failed the REUSE
Compliance check. Remove it.
This commit is contained in:
Gutemberg Ribeiro
2026-07-16 14:37:28 +01:00
committed by GitHub
parent a8be1daca4
commit c5a82c1065
6 changed files with 193 additions and 79 deletions
@@ -598,7 +598,7 @@ public sealed partial class DirectExecutionBackend
} }
*(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub); *(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub);
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal)) if (_logFiber)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " + $"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " +
@@ -1292,7 +1292,7 @@ public sealed partial class DirectExecutionBackend
} }
int returnValue; int returnValue;
if (IsNoBlockLeafImport(importStubEntry.Nid)) if (importStubEntry.IsNoBlockLeaf)
{ {
cpuContext.ClearRaxWriteFlag(); cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext); returnValue = export.Function(cpuContext);
@@ -48,6 +48,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// hashing are hoisted to stub-setup time. // hashing are hoisted to stub-setup time.
public bool IsLeaf { get; } public bool IsLeaf { get; }
public bool IsNoBlockLeaf { get; }
public bool SuppressStrlenTrace { get; } public bool SuppressStrlenTrace { get; }
public bool IsLoopGuardBoundary { get; } public bool IsLoopGuardBoundary { get; }
@@ -59,6 +61,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
string nid, string nid,
ExportedFunction? export, ExportedFunction? export,
bool isLeaf, bool isLeaf,
bool isNoBlockLeaf,
bool suppressStrlenTrace, bool suppressStrlenTrace,
bool isLoopGuardBoundary, bool isLoopGuardBoundary,
ulong nidHash) ulong nidHash)
@@ -67,6 +70,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Nid = nid; Nid = nid;
Export = export; Export = export;
IsLeaf = isLeaf; IsLeaf = isLeaf;
IsNoBlockLeaf = isNoBlockLeaf;
SuppressStrlenTrace = suppressStrlenTrace; SuppressStrlenTrace = suppressStrlenTrace;
IsLoopGuardBoundary = isLoopGuardBoundary; IsLoopGuardBoundary = isLoopGuardBoundary;
NidHash = nidHash; NidHash = nidHash;
@@ -318,6 +322,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private bool _logUsleep; private bool _logUsleep;
private bool _logFiber;
private bool _logBootstrap; private bool _logBootstrap;
private bool _logAllImports; private bool _logAllImports;
@@ -1067,6 +1073,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_ignoredGuestInt41Count = 0; _ignoredGuestInt41Count = 0;
_logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal); _logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
_logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal); _logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
_logFiber = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal);
_logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal); _logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
_logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal); _logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal);
_logImportFrames = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FRAMES"), "1", StringComparison.Ordinal); _logImportFrames = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FRAMES"), "1", StringComparison.Ordinal);
@@ -1164,6 +1171,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
text2, text2,
resolvedExport, resolvedExport,
IsLeafImport(text2), IsLeafImport(text2),
IsNoBlockLeafImport(text2),
ShouldSuppressStrlenTrace(text2), ShouldSuppressStrlenTrace(text2),
IsImportLoopGuardBoundary(text2), IsImportLoopGuardBoundary(text2),
StableHash64(text2)); StableHash64(text2));
@@ -3264,31 +3272,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{ {
Pump(callerContext, reason); Pump(callerContext, reason);
var threads = SnapshotGuestThreads(); // Tally run states under the lock without allocating a snapshot every
if (threads.Length == 0) // spin (this loop can iterate rapidly); the full snapshot is only
// materialized for the gated diagnostic dump below.
GetGuestThreadActivity(out var threadCount, out var hasReadyThread, out var hasRunningThread, out var hasBlockedThread);
if (threadCount == 0)
{ {
return; return;
} }
var hasReadyThread = false;
var hasRunningThread = false;
var hasBlockedThread = false;
foreach (var thread in threads)
{
switch (thread.State)
{
case GuestThreadRunState.Ready:
hasReadyThread = true;
break;
case GuestThreadRunState.Running:
hasRunningThread = true;
break;
case GuestThreadRunState.Blocked:
hasBlockedThread = true;
break;
}
}
if (hasReadyThread) if (hasReadyThread)
{ {
continue; continue;
@@ -3301,7 +3293,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (_logGuestThreads && Stopwatch.GetTimestamp() >= nextSnapshotTimestamp) if (_logGuestThreads && Stopwatch.GetTimestamp() >= nextSnapshotTimestamp)
{ {
foreach (var thread in threads) foreach (var thread in SnapshotGuestThreads())
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] guest_thread.idle_wait reason={reason} handle=0x{thread.ThreadHandle:X16} " + $"[LOADER][TRACE] guest_thread.idle_wait reason={reason} handle=0x{thread.ThreadHandle:X16} " +
@@ -3321,7 +3313,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{ {
using (LockGate("SnapshotGuestThreads")) using (LockGate("SnapshotGuestThreads"))
{ {
return _guestThreads.Values.ToArray(); var snapshot = new GuestThreadState[_guestThreads.Count];
_guestThreads.Values.CopyTo(snapshot, 0);
return snapshot;
}
}
// Allocation-free run-state tally for the idle spin loop.
private void GetGuestThreadActivity(out int count, out bool hasReady, out bool hasRunning, out bool hasBlocked)
{
hasReady = false;
hasRunning = false;
hasBlocked = false;
using (LockGate("GetGuestThreadActivity"))
{
count = _guestThreads.Count;
foreach (var thread in _guestThreads.Values)
{
switch (thread.State)
{
case GuestThreadRunState.Ready:
hasReady = true;
break;
case GuestThreadRunState.Running:
hasRunning = true;
break;
case GuestThreadRunState.Blocked:
hasBlocked = true;
break;
}
}
} }
} }
+39 -14
View File
@@ -5937,16 +5937,30 @@ public static partial class AgcExports
// draw a multi-render-target G-buffer (up to eight slots) in one pass. // draw a multi-render-target G-buffer (up to eight slots) in one pass.
// Fall back to slot 0 if we cannot match any export to a bound target. // Fall back to slot 0 if we cannot match any export to a bound target.
var allBoundTargets = GetRenderTargets(state.CxRegisters); var allBoundTargets = GetRenderTargets(state.CxRegisters);
var renderTargets = allBoundTargets // At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
.Where(target => HasPixelColorExport(pixelState, target.Slot)) // closure allocations. Slots are distinct, so sorting by slot is stable.
.OrderBy(target => target.Slot) var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
.ToArray(); foreach (var target in allBoundTargets)
if (renderTargets.Length == 0)
{ {
renderTargets = allBoundTargets if (HasPixelColorExport(pixelState, target.Slot))
.Where(target => target.Slot == 0) {
.ToArray(); selectedTargets.Add(target);
}
} }
if (selectedTargets.Count == 0)
{
foreach (var target in allBoundTargets)
{
if (target.Slot == 0)
{
selectedTargets.Add(target);
}
}
}
selectedTargets.Sort(static (left, right) => left.Slot.CompareTo(right.Slot));
var renderTargets = selectedTargets.ToArray();
if (_traceAgcShader && allBoundTargets.Count > 1) if (_traceAgcShader && allBoundTargets.Count > 1)
{ {
TraceAgcShader( TraceAgcShader(
@@ -6398,12 +6412,23 @@ public static partial class AgcExports
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) => private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
GetPixelColorExportMask(state, target) != 0; GetPixelColorExportMask(state, target) != 0;
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) => private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target)
state.Program.Instructions {
.Select(instruction => instruction.Control) // Called per render target (twice per draw via CreateRenderState +
.OfType<Gen5ExportControl>() // HasPixelColorExport); a manual scan avoids the per-call LINQ iterator
.Where(export => export.Target == target) // and closure allocations. Same result as the previous
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu)); // Select/OfType/Where/Aggregate chain.
var mask = 0u;
foreach (var instruction in state.Program.Instructions)
{
if (instruction.Control is Gen5ExportControl export && export.Target == target)
{
mask |= export.EnableMask & 0xFu;
}
}
return mask;
}
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state) private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
{ {
@@ -25,6 +25,11 @@ public static class KernelEventFlagCompatExports
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new(); private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
private static long _nextEventFlagHandle = 1; private static long _nextEventFlagHandle = 1;
// Cached once: gating every call site avoids building the interpolated
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
private static readonly bool _traceEventFlag = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
private sealed class EventFlagState private sealed class EventFlagState
{ {
public required string Name { get; init; } public required string Name { get; init; }
@@ -79,7 +84,7 @@ public static class KernelEventFlagCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}"); if (_traceEventFlag) TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -101,7 +106,7 @@ public static class KernelEventFlagCompatExports
Monitor.PulseAll(state.Gate); Monitor.PulseAll(state.Gate);
} }
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'"); if (_traceEventFlag) TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -124,7 +129,7 @@ public static class KernelEventFlagCompatExports
{ {
state.Bits |= pattern; state.Bits |= pattern;
Monitor.PulseAll(state.Gate); Monitor.PulseAll(state.Gate);
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
} }
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
@@ -148,7 +153,7 @@ public static class KernelEventFlagCompatExports
lock (state.Gate) lock (state.Gate)
{ {
state.Bits &= pattern; state.Bits &= pattern;
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}"); if (_traceEventFlag) TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
} }
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
@@ -189,7 +194,7 @@ public static class KernelEventFlagCompatExports
} }
ApplyClearMode(state, pattern, waitMode); ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}"); if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -287,8 +292,8 @@ public static class KernelEventFlagCompatExports
return true; return true;
}, },
deadline); deadline);
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}"); if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock) if (!requestedBlock)
{ {
var scheduler = GuestThreadExecution.Scheduler; var scheduler = GuestThreadExecution.Scheduler;
@@ -298,7 +303,7 @@ public static class KernelEventFlagCompatExports
} }
state.WaitingThreads++; state.WaitingThreads++;
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
var releaseWaiter = true; var releaseWaiter = true;
try try
{ {
@@ -318,7 +323,7 @@ public static class KernelEventFlagCompatExports
{ {
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false; releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}"); if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult); return SetReturn(ctx, pumpedWaitResult);
} }
@@ -329,7 +334,7 @@ public static class KernelEventFlagCompatExports
releaseWaiter = false; releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0); _ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits); _ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
} }
@@ -346,7 +351,7 @@ public static class KernelEventFlagCompatExports
} }
state.WaitingThreads++; state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
finally finally
@@ -381,7 +386,7 @@ public static class KernelEventFlagCompatExports
state.Bits = setPattern; state.Bits = setPattern;
state.WaitingThreads = 0; state.WaitingThreads = 0;
Monitor.PulseAll(state.Gate); Monitor.PulseAll(state.Gate);
TraceEventFlag( if (_traceEventFlag) TraceEventFlag(
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " + $"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}"); $"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
} }
@@ -476,7 +481,7 @@ public static class KernelEventFlagCompatExports
} }
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
TraceEventFlag( if (_traceEventFlag) TraceEventFlag(
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}"); $"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true; return true;
} }
@@ -568,7 +573,7 @@ public static class KernelEventFlagCompatExports
private static void TraceEventFlag(string message) private static void TraceEventFlag(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal)) if (_traceEventFlag)
{ {
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}"); Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
} }
@@ -102,7 +102,11 @@ public static partial class KernelMemoryCompatExports
private static readonly object _guestMountGate = new(); private static readonly object _guestMountGate = new();
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new(); private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new(); private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new(); // Keyed by (and kept sorted on) region base address so VirtualQuery can find a
// containing/next region with a binary search instead of an O(n) scan. Every
// write uses the region's own Address as the key (see AddMappedRegionSliceLocked
// and the mmap sites), so Values enumerate in ascending address order.
private static readonly SortedList<ulong, MappedRegion> _mappedRegions = new();
private static readonly Dictionary<ulong, string> _mappedRegionNames = new(); private static readonly Dictionary<ulong, string> _mappedRegionNames = new();
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal); private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
@@ -5477,7 +5481,9 @@ public static partial class KernelMemoryCompatExports
var affected = new List<MappedRegion>(); var affected = new List<MappedRegion>();
var cursor = address; var cursor = address;
foreach (var region in _mappedRegions.Values.OrderBy(static region => region.Address)) // _mappedRegions is a SortedList keyed by address, so Values already
// enumerate in ascending address order.
foreach (var region in _mappedRegions.Values)
{ {
if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor) if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor)
{ {
@@ -5638,9 +5644,33 @@ public static partial class KernelMemoryCompatExports
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region) private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
{ {
region = default; region = default;
var foundNext = false; var keys = _mappedRegions.Keys;
foreach (var candidate in _mappedRegions.Values) var values = _mappedRegions.Values;
var count = keys.Count;
// First index whose region address is >= queryAddress.
var lo = 0;
var hi = count;
while (lo < hi)
{ {
var mid = (int)(((uint)lo + (uint)hi) >> 1);
if (keys[mid] < queryAddress)
{
lo = mid + 1;
}
else
{
hi = mid;
}
}
// Regions do not overlap, so only the one with the greatest base address
// <= queryAddress can contain it — index lo when it starts exactly at
// queryAddress, otherwise lo - 1.
var floorIndex = (lo < count && keys[lo] == queryAddress) ? lo : lo - 1;
if (floorIndex >= 0)
{
var candidate = values[floorIndex];
if (TryAddU64(candidate.Address, candidate.Length, out var candidateEnd) && if (TryAddU64(candidate.Address, candidate.Length, out var candidateEnd) &&
queryAddress >= candidate.Address && queryAddress >= candidate.Address &&
queryAddress < candidateEnd) queryAddress < candidateEnd)
@@ -5648,20 +5678,16 @@ public static partial class KernelMemoryCompatExports
region = candidate; region = candidate;
return true; return true;
} }
if (!findNext || candidate.Address < queryAddress)
{
continue;
}
if (!foundNext || candidate.Address < region.Address)
{
region = candidate;
foundNext = true;
}
} }
return foundNext; // findNext: the region with the smallest base address >= queryAddress.
if (findNext && lo < count)
{
region = values[lo];
return true;
}
return false;
} }
private static void TraceDirectMemoryCall( private static void TraceDirectMemoryCall(
@@ -5690,10 +5716,12 @@ public static partial class KernelMemoryCompatExports
$"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} len=0x{length:X16} align=0x{alignment:X16} type=0x{memoryType:X8} out=0x{outAddress:X16} selected=0x{selectedAddress:X16} result={result?.ToString() ?? "<pending>"}"); $"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} len=0x{length:X16} align=0x{alignment:X16} type=0x{memoryType:X8} out=0x{outAddress:X16} selected=0x{selectedAddress:X16} result={result?.ToString() ?? "<pending>"}");
} }
private static bool ShouldTraceDirectMemory() // Cached once so the ~8 direct-memory call sites don't each do a
{ // GetEnvironmentVariable P/Invoke per operation.
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal); private static readonly bool _traceDirectMemory = string.Equals(
} Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal);
private static bool ShouldTraceDirectMemory() => _traceDirectMemory;
private static bool TryReleaseDirectMemoryRangeLocked(ulong start, ulong length) private static bool TryReleaseDirectMemoryRangeLocked(ulong start, ulong length)
{ {
@@ -608,6 +608,42 @@ internal static unsafe class VulkanVideoPresenter
shaderAddress); shaderAddress);
} }
// Manual scans (targets are <= 8) so the per-draw validation does not
// allocate LINQ iterators/closures or a Distinct HashSet.
private static bool AnyRenderTargetInvalid(IReadOnlyList<GuestRenderTarget> targets)
{
foreach (var target in targets)
{
if (target.Address == 0 || target.Width == 0 || target.Height == 0)
{
return true;
}
}
return false;
}
private static bool RenderTargetsMismatchedOrAliased(IReadOnlyList<GuestRenderTarget> targets, GuestRenderTarget first)
{
for (var i = 0; i < targets.Count; i++)
{
if (targets[i].Width != first.Width || targets[i].Height != first.Height)
{
return true;
}
for (var j = i + 1; j < targets.Count; j++)
{
if (targets[i].Address == targets[j].Address)
{
return true;
}
}
}
return false;
}
public static void SubmitOffscreenTranslatedDraw( public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv, byte[] pixelSpirv,
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestDrawTexture> textures,
@@ -627,16 +663,13 @@ internal static unsafe class VulkanVideoPresenter
if (pixelSpirv.Length == 0 || if (pixelSpirv.Length == 0 ||
targets.Count == 0 || targets.Count == 0 ||
targets.Count > 8 || targets.Count > 8 ||
targets.Any(target => AnyRenderTargetInvalid(targets))
target.Address == 0 || target.Width == 0 || target.Height == 0))
{ {
return; return;
} }
var firstTarget = targets[0]; var firstTarget = targets[0];
if (targets.Any(target => if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets."); "[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
@@ -654,9 +687,11 @@ internal static unsafe class VulkanVideoPresenter
var effectiveRenderState = renderState ?? GuestRenderState.Default; var effectiveRenderState = renderState ?? GuestRenderState.Default;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1) if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{ {
var broadcastBlends = new GuestBlendState[targets.Count];
Array.Fill(broadcastBlends, effectiveRenderState.Blends[0]);
effectiveRenderState = effectiveRenderState with effectiveRenderState = effectiveRenderState with
{ {
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(), Blends = broadcastBlends,
}; };
} }
lock (_gate) lock (_gate)