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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@
**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-08 - Optimized VirtualMemory TryValidateRange Hot Loop
**Context:** `src/SharpEmu.Core/Memory/VirtualMemory.cs`
**Learning:** Guest MMU memory checks running on every simulated read/write can incur high overhead from implicit `List<T>` bounds checking. The synchronization model of `VirtualMemory` uses an explicit `lock(_gate)` around list structural mutations, which allows using `CollectionsMarshal.AsSpan` during reads inside the same lock without risk of tearing.
**Action:** Replace sequential `.Count` checks and indexer access with `.AsSpan()` and `ref var` to bypass index bounds validation and structure boxing.
22 changes: 15 additions & 7 deletions src/SharpEmu.Core/Memory/VirtualMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
return true;
}

/// <remarks>Performance optimization: Elides List bounds checking via CollectionsMarshal.AsSpan for O(1) direct memory access and ref-returns to avoid struct copying during hot path execution.</remarks>
private bool TryValidateRange(
ulong virtualAddress,
int length,
Expand All @@ -131,17 +132,18 @@ private bool TryValidateRange(
return false;
}

var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_regions);
var currentAddress = virtualAddress;
var remaining = length;
var currentIndex = regionIndex;
while (true)
{
if (currentIndex >= _regions.Count)
if (currentIndex >= span.Length)
{
return false;
}

var region = _regions[currentIndex];
ref var region = ref span[currentIndex];
if (currentAddress < region.Region.VirtualAddress ||
currentAddress >= region.EndAddress ||
(region.Region.Protection & requiredProtection) == 0)
Expand All @@ -167,28 +169,32 @@ private bool TryValidateRange(
}
}

/// <remarks>Performance optimization: Elides List bounds checking via CollectionsMarshal.AsSpan for O(1) direct memory access during hot path execution.</remarks>
private int FindContainingRegionIndex(ulong virtualAddress)
{
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_regions);
var insertionIndex = FindInsertionIndex(virtualAddress);
if (insertionIndex < _regions.Count &&
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
if (insertionIndex < span.Length &&
span[insertionIndex].Region.VirtualAddress == virtualAddress)
{
return insertionIndex;
}

var candidateIndex = insertionIndex - 1;
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
return candidateIndex >= 0 && virtualAddress < span[candidateIndex].EndAddress
? candidateIndex
: -1;
}

/// <remarks>Performance optimization: Elides List bounds checking via CollectionsMarshal.AsSpan for O(1) direct memory access and ref-returns to avoid struct copying during hot path execution.</remarks>
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
{
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_regions);
var copied = 0;
var currentAddress = virtualAddress;
while (copied < destination.Length)
{
var region = _regions[regionIndex++];
ref var region = ref span[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
Expand All @@ -197,13 +203,15 @@ private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int r
}
}

/// <remarks>Performance optimization: Elides List bounds checking via CollectionsMarshal.AsSpan for O(1) direct memory access and ref-returns to avoid struct copying during hot path execution.</remarks>
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
{
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_regions);
var copied = 0;
var currentAddress = virtualAddress;
while (copied < source.Length)
{
var region = _regions[regionIndex++];
ref var region = ref span[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
Expand Down
15 changes: 15 additions & 0 deletions src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace SharpEmu.Libs.AvPlayer;

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
public static class AvPlayerExports
{
private const int InvalidParameters = unchecked((int)0x806A0001);
Expand Down Expand Up @@ -1341,6 +1343,19 @@ private static bool TryRemoveUnrealLeadingDotSegments(
return !removedParent || guestPath.Contains('/');
}

public static bool TryGetFallbackPresentationFrame(
out byte[] pixels,
out uint width,
out uint height,
out ulong serial)
{
pixels = Array.Empty<byte>();
width = 0;
height = 0;
serial = 0;
return false;
}

private static bool TryDecodeFileReference(string encoded, out string decoded)
{
decoded = string.Empty;
Expand Down
174 changes: 87 additions & 87 deletions src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2467,7 +2467,7 @@ private static bool TryTakePresentation(long presentedSequence, out Presentation
{
if (_latestPresentation is { } rej &&
rej.GuestImageAddress != 0 &&
rej.Sequence != presentedSequence &&
rej.Sequence != presentedSequence &&
_tracedGuestImagePresentRejections.Add(rej.Sequence))
{
var reason = rej.Sequence == presentedSequence
Expand Down Expand Up @@ -2560,7 +2560,7 @@ private static bool TryTakeHostMovieFrame(

if (Interlocked.Exchange(
ref _tracedAvPlayerFallbackPresentationSerial,
serial) != serial)
(long)serial) != (long)serial)
{
var frameCount = Interlocked.Increment(
ref _avPlayerFallbackPresentationCount);
Expand All @@ -2579,14 +2579,14 @@ private static bool TryTakeHostMovieFrame(
private static long _avPlayerFallbackPresentationCount;
private static readonly HashSet<long> _tracedGuestImagePresentRejections = new();

private static bool HasPendingGuestPresentation(long presentedSequence)
{
lock (_gate)
{
return _pendingGuestImagePresentations.Count > 0 ||
_latestPresentation is { } latest && latest.Sequence > presentedSequence;
}
}
private static bool HasPendingGuestPresentation(long presentedSequence)
{
lock (_gate)
{
return _pendingGuestImagePresentations.Count > 0 ||
_latestPresentation is { } latest && latest.Sequence > presentedSequence;
}
}

private static long EnqueueGuestWorkLocked(object work)
{
Expand Down Expand Up @@ -2713,7 +2713,7 @@ _thread is not null &&
}
else
{
pendingQueue.AddLast(pending);
pendingQueue.AddLast(pending);
}
RecordGuestImageWritersLocked(work, sequence);
_pendingGuestWorkCount++;
Expand Down Expand Up @@ -7081,11 +7081,11 @@ private ShaderModule CreateShaderModule(byte[] code)
var sequence = Interlocked.Increment(ref _shaderModuleDumpSequence);
dumpPath = Path.Combine(dumpDirectory, $"{sequence:D4}.spv");
File.WriteAllBytes(dumpPath, code);

_pendingShaderModuleDumpPath = dumpPath;
}


try
{
fixed (byte* codePointer = code)
Expand Down Expand Up @@ -8956,75 +8956,75 @@ private void DrainGuestImageCpuSync()
List<(ulong Address, uint Width, uint Height, ulong ByteCount)>? extents = null;
if (syncEnabled)
{
_ = Interlocked.Exchange(ref _cpuWrittenGuestImageSyncRequested, 0);
_ = Interlocked.Exchange(ref _cpuWrittenGuestImageSyncRequested, 0);

lock (_gate)
{
if (_guestImageExtents.Count > 0)
lock (_gate)
{
extents = new(_guestImageExtents.Count);
foreach (var entry in _guestImageExtents)
if (_guestImageExtents.Count > 0)
{
extents.Add((
entry.Key,
entry.Value.Width,
entry.Value.Height,
entry.Value.ByteCount));
extents = new(_guestImageExtents.Count);
foreach (var entry in _guestImageExtents)
{
extents.Add((
entry.Key,
entry.Value.Width,
entry.Value.Height,
entry.Value.ByteCount));
}
}
}
}

var memory = _guestMemory;
if (extents is not null)
{
foreach (var (address, width, height, byteCount) in extents)
var memory = _guestMemory;
if (extents is not null)
{
if (!SharpEmu.HLE.GuestImageWriteTracker.ConsumeDirty(address))
foreach (var (address, width, height, byteCount) in extents)
{
continue;
}

(dirtyAddresses ??= []).Add(address);
if (memory is null ||
byteCount == 0 ||
byteCount > 128UL * 1024UL * 1024UL ||
!_guestImages.TryGetValue(address, out var target))
{
continue;
}
if (!SharpEmu.HLE.GuestImageWriteTracker.ConsumeDirty(address))
{
continue;
}

// GPU-only RTs often get Dirty via page-overlap. A full
// plane read/upload per false dirty destroys Dead Cells FPS
// and can stall GTA after intro. Probe 4 KiB first unless
// the surface is already known CPU-backed.
if (!target.IsCpuBacked)
{
var probeLen = (int)Math.Min(byteCount, 4096UL);
var probe = new byte[probeLen];
if (!memory.TryRead(address, probe) ||
probe.AsSpan().IndexOfAnyExcept((byte)0) < 0)
(dirtyAddresses ??= []).Add(address);
if (memory is null ||
byteCount == 0 ||
byteCount > 128UL * 1024UL * 1024UL ||
!_guestImages.TryGetValue(address, out var target))
{
continue;
}

target.IsCpuBacked = true;
}
// GPU-only RTs often get Dirty via page-overlap. A full
// plane read/upload per false dirty destroys Dead Cells FPS
// and can stall GTA after intro. Probe 4 KiB first unless
// the surface is already known CPU-backed.
if (!target.IsCpuBacked)
{
var probeLen = (int)Math.Min(byteCount, 4096UL);
var probe = new byte[probeLen];
if (!memory.TryRead(address, probe) ||
probe.AsSpan().IndexOfAnyExcept((byte)0) < 0)
{
continue;
}

var pixels = new byte[byteCount];
if (!memory.TryRead(address, pixels) ||
pixels.AsSpan().IndexOfAnyExcept((byte)0) < 0)
{
continue;
}
target.IsCpuBacked = true;
}

UploadGuestImageInitialData(target, pixels);
if (Interlocked.Increment(ref _guestImageCpuSyncTraceCount) <= 64)
{
Console.Error.WriteLine(
$"[SYNC] cpu-write-drain addr=0x{address:X} {width}x{height}");
var pixels = new byte[byteCount];
if (!memory.TryRead(address, pixels) ||
pixels.AsSpan().IndexOfAnyExcept((byte)0) < 0)
{
continue;
}

UploadGuestImageInitialData(target, pixels);
if (Interlocked.Increment(ref _guestImageCpuSyncTraceCount) <= 64)
{
Console.Error.WriteLine(
$"[SYNC] cpu-write-drain addr=0x{address:X} {width}x{height}");
}
}
}
}

}

Expand Down Expand Up @@ -10783,19 +10783,19 @@ private static VertexBufferResource CreateVertexBufferResource(
private static VertexBufferResource CreateVertexBufferAlias(
VertexBufferResource shared,
GuestVertexBuffer guestBuffer) => new()
{
Buffer = shared.Buffer,
Memory = shared.Memory,
OwnsBuffer = false,
Size = shared.Size,
Location = guestBuffer.Location,
ComponentCount = guestBuffer.ComponentCount,
DataFormat = guestBuffer.DataFormat,
NumberFormat = guestBuffer.NumberFormat,
Stride = guestBuffer.Stride,
OffsetBytes = guestBuffer.OffsetBytes,
PerInstance = guestBuffer.PerInstance,
};
{
Buffer = shared.Buffer,
Memory = shared.Memory,
OwnsBuffer = false,
Size = shared.Size,
Location = guestBuffer.Location,
ComponentCount = guestBuffer.ComponentCount,
DataFormat = guestBuffer.DataFormat,
NumberFormat = guestBuffer.NumberFormat,
Stride = guestBuffer.Stride,
OffsetBytes = guestBuffer.OffsetBytes,
PerInstance = guestBuffer.PerInstance,
};

private VkBuffer CreateHostBuffer(
ReadOnlySpan<byte> data,
Expand Down Expand Up @@ -18772,15 +18772,15 @@ private void TraceSwapchainReadback()
$"present-{seq:D4}-{_extent.Width}x{_extent.Height}-{_swapchainFormat}.bgra");
File.WriteAllBytes(path, bytes.ToArray());
Console.Error.WriteLine($"[LOADER][TRACE] vk.swapchain_dump path={path}");
// Continuous readback is intentionally opt-in: each 1080p frame
// is several megabytes and synchronously waits for the GPU.
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_DUMP_CONTINUOUS"),
"1",
StringComparison.Ordinal))
{
_tracedPresentedSwapchain = false;
}
// Continuous readback is intentionally opt-in: each 1080p frame
// is several megabytes and synchronously waits for the GPU.
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_DUMP_CONTINUOUS"),
"1",
StringComparison.Ordinal))
{
_tracedPresentedSwapchain = false;
}
}
}
finally
Expand Down
Loading