[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
@@ -608,6 +608,42 @@ internal static unsafe class VulkanVideoPresenter
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(
byte[] pixelSpirv,
IReadOnlyList<GuestDrawTexture> textures,
@@ -627,16 +663,13 @@ internal static unsafe class VulkanVideoPresenter
if (pixelSpirv.Length == 0 ||
targets.Count == 0 ||
targets.Count > 8 ||
targets.Any(target =>
target.Address == 0 || target.Width == 0 || target.Height == 0))
AnyRenderTargetInvalid(targets))
{
return;
}
var firstTarget = targets[0];
if (targets.Any(target =>
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
{
Console.Error.WriteLine(
"[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;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{
var broadcastBlends = new GuestBlendState[targets.Count];
Array.Fill(broadcastBlends, effectiveRenderState.Blends[0]);
effectiveRenderState = effectiveRenderState with
{
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(),
Blends = broadcastBlends,
};
}
lock (_gate)