sharpemu/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs
Astell c076513bd6
Astrobot / Demon's soul performance improvement (#955)
* [Gpu] Astro Bot performance: 14 -> ~60 fps at the title scene

- Defer RELEASE_MEM command-buffer flushes instead of submitting per packet
- Resolve indexed indirect draws and dispatches on the GPU
- Async buffer readbacks on a dedicated transfer/compute queue
- Pool image backings, flip snapshots and tiler resources
- One-frame command-stream run-ahead
- Lock-free backed-range and image page-owner checks on CPU writes
- Scalar value cache with generation-stamped reset; memoized invariant phis
- Shader compiler: structured forward-only CFGs with function-scope
  registers, EXEC-full guard elision, bounded V_MOVREL offsets
- Trivial-leaf import dispatch and strchr/strrchr fast paths
- SHARPEMU_GUEST_REFRESH_RATE option

* [Tests] Fix full-suite flaky tests

- Red-zone patcher: when every fixed trampoline candidate is taken, look
  for any free host range within relative-jump reach
  (PhysicalVirtualMemory.EnumerateFreeHostRanges); the test places its
  image in a probed free hole
- Kernel memory tests: probe a free PRT-area address instead of the fixed
  0x1026C00000; the reservation-extent test only rejects picks inside
  the reservation
- Release checks tolerate another thread reusing the freed range
- Allocation tests measure on a dedicated thread and keep the least
  allocating of several rounds (AllocationMeasurement.SteadyState)

* [Gpu] Astro Bot: structured loops, buffer slabs, pad motion

- Shader: emit natural loops as structured SPIR-V loops (OpLoopMerge)
  instead of the block dispatcher, and merge SCC-uniform if/else joins
  as selects so descriptors picked on a branch stay evaluable per draw.
- Shader: decode V_FFBH_I32 and V_MIN3_F16; read the DS GDS bit from
  bit 17 (GFX10) and route GDS append/consume through shared atomics.
- Gpu: sub-allocate small device-local buffers from shared memory slabs.
- Pad: report motion data by default (Astro Bot never imports
  scePadSetMotionSensorState), convert acceleration from m/s^2 to G,
  and add scePadResetOrientation / scePadSetAngularVelocityDeadbandState.

* [CPU] Stop pinning guest threads to host cores by default

Guest affinity packed Demon's Souls' 13 spinning BPE job workers onto 10 host lanes and put its renderer next to them at equal priority, so the renderer and lock holders waited out whole scheduler quanta. Intro 10.6 -> 44.7 fps, menus 11.6 -> 47.5 fps. SHARPEMU_GUEST_AFFINITY=1 restores pinning.

* [Media] Convert host movie frames to YUV on the decoder thread

The render thread converted every decoded Bink frame from BGRA back to YUV 4:2:0 (6.7 ms per frame). The decoder thread now produces the planes and the presenter uploads them as-is. PS Studios logo 33 -> ~50 fps, attract movie 44 -> 60 fps.

* [Agc] Accept a zero destination in sceAgcDcbWriteData

Demon's Souls emits WRITE_DATA with address 0 into a template and patches it after copying. Returning null made the title patch a relocated null pointer, which wrote a stack-derived value into its label and was then parsed as an unknown NOP marker (GPU FATAL).

* [Loader] Patch short red-zone faulting accesses next to branches

A faulting access shorter than a jump could not be relocated when followed by a branch or a branch target, so a handled fault let Windows overwrite the red zone. Spans may now end with a direct branch (run after the stack pointer is restored) or start up to three instructions earlier. Fixes a Demon's Souls crash after the new-game intro movie.

* [GPU] Astro Bot rendering fixes: compute subgroups, DCC clears, PS inputs, texture MIN_LOD

- Compute: pin non-emulated compute pipelines to a 32-lane subgroup
  (VK_EXT_subgroup_size_control) so wave64 groups other than 64
  invocations keep lanes 32..63 active on AMD (striped froxel volume).
- Shader translator: 64-bit AND for the wave32 S_*_SAVEEXEC_B64 mask.
- PS inputs: slots that read one VS parameter share one host input;
  a smooth/flat slot next to a V_INTERP_MOV slot is rebuilt from the
  per-vertex values (black save cards, NaN normals, black props).
- Render targets: drop unwritten color targets of depth passes from both
  the attachments and the pixel outputs, keeping locations aligned.
- DCC: materialize pending zero fast clears before shader access so a
  later attachment bind does not wipe compute writes.
- Textures: honour the T# MIN_LOD with VK_EXT_image_view_min_lod so
  streamed textures never sample unloaded mips (blown-out stadium sky).
- Device: enable VK_KHR_maintenance5 for point lists without PointSize.

* [Audio][GPU] ATRAC9 independent mono streams, BC storage fallback, 32-bit bit counts

- LibAtrac9: decode config header 0x30 streams (PS5 ambisonics), where every
  channel is an independent mono stream interleaved frame by frame.
- Images: when the driver refuses storage usage on a block-compressed image,
  compute writes go through an uncompressed replacement of the same memory.
- SPIR-V: count 64-bit masks with two 32-bit OpBitCount, which Vulkan requires
  without maintenance9.
- Agc: drop the indirect-draw emit debug log; report the draw modifier in the
  unsupported indirect draw error.

* [GPU] Partial texture-pool uploads and cheaper per-draw preparation

- A CPU write into a large layered texture (Demon's Souls' 256-layer BC3
  pools) re-uploaded the whole image, ~750 ms of GPU detiling in one frame.
  Each (mip, layer) piece's guest bytes are hashed at upload; a refresh
  uploads only the pieces that changed.
- Device-address preparation touched and checked every registered buffer for
  each dispatch. Recency touches now run once per retirement tick, and uploads
  only visit buffers in blocks the tracker does not know to be clean; hot
  pages stay dirty, so they are still re-uploaded every time.
- Resource materialization cache: when the only changed words are ones the
  flattened table alone reads (per-frame inline constants), only the table is
  evaluated again.
- A full batch no longer splits an open render pass before 512 draws.
- Lock-free lookups for shader headers, device format properties and the idle
  host movie bridge; the SPIR-V dump toggle is read once per program cache.

* Demon soul - fix texture

* Delete runahead tests

* Screcrow fix for nvidia GPU
2026-09-30 18:14:21 +03:00

467 lines
18 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Memory;
using SharpEmu.Core.Loader;
using SharpEmu.HLE.Host;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
// PhysicalVirtualMemory is the host-backed (identity-mapped) implementation.
// Huge non-executable maps (> 4 GiB) prefer commit-first, then fall back to
// reserve-only + lazy commit when Allocate fails. TryAllocateGuestMemory serves
// a first-fit free-list with coalescing. These tests pin that behaviour through
// fake IHostMemory implementations that refuse full Allocate for huge sizes.
public sealed class PhysicalVirtualMemoryTests
{
[Fact]
public void FreeHostRangesSkipTakenAndTooSmallRegionsAndStayInsideTheWindow()
{
var host = new ScriptedRegionHostMemory(
(0x10000, 0x22000, HostRegionState.Committed),
(0x32000, 0xC000, HostRegionState.Free), // its aligned start 0x40000 is past its end
(0x3E000, 0x12000, HostRegionState.Reserved),
(0x50000, 0x21000, HostRegionState.Free), // holds 0x50000 and nothing else aligned
(0x71000, 0x1F000, HostRegionState.Committed),
(0x90000, 0x40000, HostRegionState.Free));
using var memory = new PhysicalVirtualMemory(host);
Assert.Equal(new ulong[] { 0x50000, 0x90000 },
memory.EnumerateFreeHostRanges(0x10000, 0xA2000, 0x2000, 0x10000).ToArray());
Assert.Equal(new ulong[] { 0x60000 },
memory.EnumerateFreeHostRanges(0x51000, 0x71000, 0x2000, 0x10000).ToArray());
// The window end cuts the last free region below the requested size.
Assert.Empty(memory.EnumerateFreeHostRanges(0x72000, 0x91000, 0x2000, 0x10000));
}
// 1. Lazy commit: a reserve-only region has its pages committed on demand
// when read; freshly committed pages read as zero.
[Fact]
public void LazyReadCommitsPageOnDemandAndReadsZero()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
// > 4 GiB, non-executable; fake host rejects Allocate so reserve-only
// + lazy commit is used.
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
Assert.NotEqual(0UL, address);
// Discard the priming commits AllocateAt issues up front; we want to
// observe the on-demand commit triggered by the read itself.
host.CommitCalls.Clear();
var buffer = new byte[1];
Assert.True(memory.TryRead(address, buffer));
Assert.Equal(0, buffer[0]);
// The touched page (page-aligned to `address`) was committed on demand.
var page = address & ~0xFFFUL;
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
}
[Fact]
public void RepeatedLazyReadUsesCommittedRangeCache()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
host.CommitCalls.Clear();
Span<byte> buffer = stackalloc byte[16];
Assert.True(memory.TryRead(address + 0x100, buffer));
var queryCallsAfterFirstRead = host.QueryCalls;
Assert.True(memory.TryRead(address + 0x108, buffer[..8]));
Assert.Equal(queryCallsAfterFirstRead, host.QueryCalls);
Assert.Single(host.CommitCalls);
}
[Fact]
public void EnsureRangeCommittedBacksOnlyTheMappedWindow()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
host.CommitCalls.Clear();
var mappedAddress = address + 0x0800_0000UL;
Assert.True(memory.TryEnsureRangeCommitted(mappedAddress, 0x3000));
Assert.Equal(
[
(mappedAddress, 0x1000UL, HostPageProtection.ReadWrite),
(mappedAddress + 0x1000, 0x1000UL, HostPageProtection.ReadWrite),
(mappedAddress + 0x2000, 0x1000UL, HostPageProtection.ReadWrite),
],
host.CommitCalls);
}
[Fact]
public void EnsureRangeCommittedRejectsAnUnallocatedWindow()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
Assert.False(memory.TryEnsureRangeCommitted(0x0001_0000, 0x1000));
}
[Fact]
public void TryCopyHandlesOverlappingIdentityMappedRanges()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
Assert.True(memory.TryWrite(address, new byte[] { 1, 2, 3, 4, 5, 6 }));
Assert.True(memory.TryCopy(address + 2, address, 4));
Span<byte> result = stackalloc byte[6];
Assert.True(memory.TryRead(address, result));
Assert.Equal(new byte[] { 1, 2, 1, 2, 3, 4 }, result.ToArray());
}
[Fact]
public void TryCompareDistinguishesMismatchFromAccessFailure()
{
using var host = new ReadableHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, 0x1000, executable: false);
Assert.NotEqual(0UL, address);
Assert.True(memory.TryWrite(address, new byte[] { 1, 2, 3, 4 }));
Assert.True(memory.TryCompare(address, new byte[] { 1, 2, 3, 4 }, out var equal));
Assert.True(equal);
Assert.True(memory.TryCompare(address, new byte[] { 1, 2, 3, 5 }, out equal));
Assert.False(equal);
Assert.False(memory.TryCompare(ulong.MaxValue - 0x1000, new byte[4], out equal));
Assert.False(equal);
}
[Fact]
public void RepeatedTryCopyKeepsSourceAndDestinationCommitRangesCached()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
var source = address + 0x100;
var destination = address + 0x1100;
Assert.True(memory.TryWrite(source, new byte[] { 1, 2, 3, 4 }));
Assert.True(memory.TryWrite(destination, new byte[4]));
host.CommitCalls.Clear();
Assert.True(memory.TryCopy(destination, source, 4));
var queryCallsAfterFirstCopy = host.QueryCalls;
Assert.True(memory.TryCopy(destination, source, 4));
Assert.Equal(queryCallsAfterFirstCopy, host.QueryCalls);
}
// 2. Reserve-only region: GetPointer commits the page before returning it,
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
[Fact]
public unsafe void GetPointerOnReserveOnlyRegionCommitsAndReturnsValidPointer()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
host.CommitCalls.Clear();
var pointer = memory.GetPointer(address + 0x123);
Assert.NotEqual(0UL, (ulong)pointer);
Assert.Equal(address + 0x123, (ulong)pointer);
// GetPointer primes a 32 MiB working-set chunk (page-sized commits
// against this fake host's 4 KiB Query regions). Non-page-aligned
// start makes AlignUp(addr + chunk) cover one extra page.
const ulong lazyPrimeChunkBytes = 0x0200_0000UL;
var page = (address + 0x123) & ~0xFFFUL;
var endPage = (address + 0x123 + lazyPrimeChunkBytes + 0xFFFUL) & ~0xFFFUL;
Assert.Equal((int)((endPage - page) / 0x1000UL), host.CommitCalls.Count);
Assert.Equal((page, 0x1000UL, HostPageProtection.ReadWrite), host.CommitCalls[0]);
Assert.All(
host.CommitCalls,
call => Assert.Equal(0x1000UL, call.Size));
}
[Fact]
public unsafe void GetPointerOnUnmappedAddressReturnsNull()
{
using var host = new LazyZeroedHostMemory();
using var memory = new PhysicalVirtualMemory(host);
Assert.Equal(0UL, (ulong)memory.GetPointer(0x0001_0000));
}
// 3. Free-list reuse: a freed range is served back by first-fit allocation,
// preferring the lowest fitting free range over the larger trailing span.
[Fact]
public void FreedRangeIsReusedByFirstFitAllocation()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var second));
Assert.NotEqual(first, second);
Assert.True(memory.TryFreeGuestMemory(first));
// A smaller allocation must reuse first's freed slot (lowest fitting range),
// not the larger trailing free range.
Assert.True(memory.TryAllocateGuestMemory(0x2000, 0x1000, out var reused));
Assert.Equal(first, reused);
}
// 4. Coalescing: freeing the middle of three adjacent ranges merges both the
// left and right free neighbours in a single TryFreeGuestMemory call,
// restoring the full span for subsequent first-fit reuse.
[Fact]
public void FreeingMiddleRangeCoalescesBothNeighbours()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
// Three adjacent 0x1000 allocations: offsets 0x1000, 0x2000, 0x3000.
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var second));
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var third));
// Free the outer ranges first, leaving two separate free ranges.
Assert.True(memory.TryFreeGuestMemory(first));
Assert.True(memory.TryFreeGuestMemory(third));
// Freeing the middle range must coalesce both neighbours at once.
Assert.True(memory.TryFreeGuestMemory(second));
// The whole arena is now one coalesced free range; a full-arena allocation
// reuses first's base address.
Assert.True(memory.TryAllocateGuestMemory(0x000F_F000, 0x1000, out var coalesced));
Assert.Equal(first, coalesced);
}
/// <summary>
/// Host memory backed by a single real, zero-initialised page. Reserve/Allocate
/// report the page-aligned buffer address so lazy-commit read paths can actually
/// dereference the returned pointer. Query always reports Reserved, so
/// EnsureRangeCommitted issues a Commit on first access.
/// </summary>
private sealed unsafe class LazyZeroedHostMemory : IHostMemory, IDisposable
{
private readonly void* _allocation;
private readonly ulong _address;
private bool _freed;
public LazyZeroedHostMemory()
{
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x3000);
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
}
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
public int QueryCalls { get; private set; }
// Force the commit-first → reserve-only fallback for huge maps.
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
public bool Commit(ulong address, ulong size, HostPageProtection protection)
{
CommitCalls.Add((address, size, protection));
return true;
}
public bool Free(ulong address)
{
// The real buffer is released in Dispose; keep Free a no-op so
// PhysicalVirtualMemory.Clear does not double-free it.
return true;
}
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
QueryCalls++;
var pageAddress = address & ~0xFFFUL;
info = new HostRegionInfo(
pageAddress,
pageAddress,
0x1000,
HostRegionState.Reserved,
0,
HostPageProtection.NoAccess,
0,
0);
return true;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
public void Dispose()
{
if (!_freed)
{
System.Runtime.InteropServices.NativeMemory.Free(_allocation);
_freed = true;
}
}
}
private sealed unsafe class ReadableHostMemory : IHostMemory, IDisposable
{
private readonly void* _allocation;
private readonly ulong _address;
public ReadableHostMemory()
{
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
}
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
_address;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
_address;
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address) => true;
public bool Protect(
ulong address,
ulong size,
HostPageProtection protection,
out uint rawOldProtection)
{
rawOldProtection = 0x04;
return true;
}
public bool ProtectRaw(
ulong address,
ulong size,
uint rawProtection,
out uint rawOldProtection)
{
rawOldProtection = 0x04;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
if (address >= _address && address < _address + 0x1000)
{
info = new HostRegionInfo(
_address,
_address,
0x1000,
HostRegionState.Committed,
RawState: 0x1000,
HostPageProtection.ReadWrite,
RawProtection: 0x04,
RawAllocationProtection: 0x04);
return true;
}
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
public void Dispose() =>
System.Runtime.InteropServices.NativeMemory.Free(_allocation);
}
// Minimal host memory for free-list tests: Allocate honours the desired
// address (or a fallback), everything else succeeds as a no-op. The guest
// allocation arena never dereferences, so no real backing is required.
private sealed class FakeHostMemory : IHostMemory
{
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
Allocate(desiredAddress, size, protection);
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address) => true;
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
}
// Answers Query from a fixed region map; nothing else is used.
private sealed class ScriptedRegionHostMemory : IHostMemory
{
private readonly (ulong Base, ulong Size, HostRegionState State)[] _regions;
public ScriptedRegionHostMemory(params (ulong Base, ulong Size, HostRegionState State)[] regions) => _regions = regions;
public bool Query(ulong address, out HostRegionInfo info)
{
foreach (var (start, size, state) in _regions)
{
if (address >= start && address < start + size)
{
var page = address & ~0xFFFUL;
info = new HostRegionInfo(page, start, start + size - page, state, 0, HostPageProtection.NoAccess, 0, 0);
return true;
}
}
info = default;
return false;
}
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public bool Commit(ulong address, ulong size, HostPageProtection protection) => false;
public bool Free(ulong address) => false;
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection) { rawOldProtection = 0; return false; }
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection) { rawOldProtection = 0; return false; }
public void FlushInstructionCache(ulong address, ulong size) { }
}
}