mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 04:08:40 +08:00
Refresh CPU-rewritten guest textures by write generation (#447)
* Track guest CPU write generations * Refresh CPU-rewritten guest textures by write generation
This commit is contained in:
@@ -32,6 +32,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
public int Armed;
|
public int Armed;
|
||||||
public int FirstCpuWriteSeen;
|
public int FirstCpuWriteSeen;
|
||||||
public int PendingFirstCpuWrite;
|
public int PendingFirstCpuWrite;
|
||||||
|
public long WriteGeneration;
|
||||||
public bool TraceLifetime;
|
public bool TraceLifetime;
|
||||||
public long SourceSequence;
|
public long SourceSequence;
|
||||||
public long FirstCpuWriteTraceSequence;
|
public long FirstCpuWriteTraceSequence;
|
||||||
@@ -155,10 +156,21 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
{
|
{
|
||||||
// Never resize an object that is still reachable from the
|
// Never resize an object that is still reachable from the
|
||||||
// signal handler's lock-free snapshot. Retire it and publish
|
// signal handler's lock-free snapshot. Retire it and publish
|
||||||
// a fresh immutable range.
|
// a fresh immutable range, carrying the write generation so
|
||||||
|
// resizes do not hide guest CPU rewrites from cache owners.
|
||||||
|
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
|
||||||
DisarmLocked(range, "replace-range");
|
DisarmLocked(range, "replace-range");
|
||||||
_rangesByAddress.Remove(address);
|
_rangesByAddress.Remove(address);
|
||||||
range = null;
|
range = new TrackedRange
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
ByteCount = byteCount,
|
||||||
|
Start = start,
|
||||||
|
End = start + length,
|
||||||
|
WriteGeneration = writeGeneration,
|
||||||
|
};
|
||||||
|
_rangesByAddress[address] = range;
|
||||||
|
RebuildSnapshotLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range is null)
|
if (range is null)
|
||||||
@@ -272,6 +284,31 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the monotonic first-write generation for a tracked allocation.
|
||||||
|
/// Unlike the consuming dirty flag, this remains changed after another
|
||||||
|
/// cache owner consumes and re-arms the range.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryGetWriteGeneration(ulong address, out long generation)
|
||||||
|
{
|
||||||
|
generation = 0;
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
generation = Volatile.Read(ref range.WriteGeneration);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares pages touched by a managed HLE memory write. Native guest
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||||
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||||
@@ -425,6 +462,10 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||||
|
if (wasArmed)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref range.WriteGeneration);
|
||||||
|
}
|
||||||
if (wasArmed &&
|
if (wasArmed &&
|
||||||
range.TraceLifetime &&
|
range.TraceLifetime &&
|
||||||
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||||
|
|||||||
@@ -7893,9 +7893,13 @@ public static partial class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload-known (not plain availability): the presenter's answer goes
|
||||||
|
// generation-stale when the guest CPU rewrites a CPU-backed image
|
||||||
|
// (video planes, streamed font atlases), which routes this draw back
|
||||||
|
// through the texel copy below so the refresh path re-uploads.
|
||||||
if (!isStorage &&
|
if (!isStorage &&
|
||||||
descriptor.Address != 0 &&
|
descriptor.Address != 0 &&
|
||||||
GuestGpu.Current.IsGpuGuestImageAvailable(
|
GuestGpu.Current.IsGuestImageUploadKnown(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.Format,
|
descriptor.Format,
|
||||||
descriptor.NumberType))
|
descriptor.NumberType))
|
||||||
@@ -8002,6 +8006,19 @@ public static partial class AgcExports
|
|||||||
// (skipping would leave the draw with no pixels and a fallback
|
// (skipping would leave the draw with no pixels and a fallback
|
||||||
// texture for the frame — visible flicker on animated textures).
|
// texture for the frame — visible flicker on animated textures).
|
||||||
var sampler = ToGuestSampler(samplerDescriptor);
|
var sampler = ToGuestSampler(samplerDescriptor);
|
||||||
|
// Track the guest allocation before reading its texels so a CPU
|
||||||
|
// rewrite landing after the copy still bumps the write generation.
|
||||||
|
// The generation rides on the texture and is recorded by the
|
||||||
|
// presenter after upload, where the upload-known skip compares it
|
||||||
|
// against the tracker to force fresh texels for rewritten memory.
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.Track(
|
||||||
|
descriptor.Address,
|
||||||
|
physicalSourceByteCount,
|
||||||
|
source: "agc.decoded-texture");
|
||||||
|
var hasWriteGeneration =
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.TryGetWriteGeneration(
|
||||||
|
descriptor.Address,
|
||||||
|
out var writeGeneration);
|
||||||
if (!_textureCopySkipDisabled &&
|
if (!_textureCopySkipDisabled &&
|
||||||
descriptor.Address != 0 &&
|
descriptor.Address != 0 &&
|
||||||
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
|
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
|
||||||
@@ -8093,7 +8110,8 @@ public static partial class AgcExports
|
|||||||
Pitch: sourceWidth,
|
Pitch: sourceWidth,
|
||||||
TileMode: descriptor.TileMode,
|
TileMode: descriptor.TileMode,
|
||||||
DstSelect: descriptor.DstSelect,
|
DstSelect: descriptor.DstSelect,
|
||||||
Sampler: ToGuestSampler(samplerDescriptor));
|
Sampler: ToGuestSampler(samplerDescriptor),
|
||||||
|
WriteGeneration: hasWriteGeneration ? writeGeneration : -1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ internal sealed record GuestDrawTexture(
|
|||||||
uint Pitch = 0,
|
uint Pitch = 0,
|
||||||
uint TileMode = 0,
|
uint TileMode = 0,
|
||||||
uint DstSelect = 0xFAC,
|
uint DstSelect = 0xFAC,
|
||||||
GuestSampler Sampler = default);
|
GuestSampler Sampler = default,
|
||||||
|
// Guest CPU write-tracker generation of the memory RgbaPixels was read
|
||||||
|
// from; -1 when the range is untracked or the pixels were not read here.
|
||||||
|
long WriteGeneration = -1);
|
||||||
|
|
||||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||||
internal readonly record struct GuestSampler(
|
internal readonly record struct GuestSampler(
|
||||||
|
|||||||
@@ -472,6 +472,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private static readonly Queue<Presentation> _pendingGuestImagePresentations = new();
|
private static readonly Queue<Presentation> _pendingGuestImagePresentations = new();
|
||||||
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
||||||
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
||||||
|
// Write-tracker generation last uploaded for a CPU-backed guest image.
|
||||||
|
// A newer generation in the tracker means the guest CPU rewrote the
|
||||||
|
// memory (video frames, streamed atlases) and the upload-known skip in
|
||||||
|
// draw translation must ship fresh texels instead of reusing the image.
|
||||||
|
private static readonly Dictionary<ulong, long> _cpuBackedUploadGenerations = new();
|
||||||
private static readonly Dictionary<(int Handle, int BufferIndex), long>
|
private static readonly Dictionary<(int Handle, int BufferIndex), long>
|
||||||
_lastOrderedGuestFlipVersions = new();
|
_lastOrderedGuestFlipVersions = new();
|
||||||
private static long _orderedGuestFlipVersionSequence;
|
private static long _orderedGuestFlipVersionSequence;
|
||||||
@@ -811,6 +816,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_pendingGuestImagePresentations.Clear();
|
_pendingGuestImagePresentations.Clear();
|
||||||
_guestImageWorkSequences.Clear();
|
_guestImageWorkSequences.Clear();
|
||||||
_availableGuestImages.Clear();
|
_availableGuestImages.Clear();
|
||||||
|
_cpuBackedUploadGenerations.Clear();
|
||||||
_lastOrderedGuestFlipVersions.Clear();
|
_lastOrderedGuestFlipVersions.Clear();
|
||||||
_orderedGuestFlipVersionSequence = 0;
|
_orderedGuestFlipVersionSequence = 0;
|
||||||
_pendingGuestImageUploads.Clear();
|
_pendingGuestImageUploads.Clear();
|
||||||
@@ -1790,9 +1796,30 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
var known =
|
||||||
|
_availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||||
availableFormat == guestFormat ||
|
availableFormat == guestFormat ||
|
||||||
_pendingGuestImageUploads.ContainsKey((address, guestFormat));
|
_pendingGuestImageUploads.ContainsKey((address, guestFormat));
|
||||||
|
if (!known)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU-backed images (video frames, streamed atlases) go stale when
|
||||||
|
// the guest CPU rewrites the memory after the recorded upload; a
|
||||||
|
// changed write-tracker generation forces a fresh texel copy so
|
||||||
|
// the refresh path can re-upload. GPU-rendered images have no
|
||||||
|
// generation entry and keep the plain availability answer.
|
||||||
|
if (_cpuBackedUploadGenerations.TryGetValue(address, out var uploadedGeneration) &&
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.TryGetWriteGeneration(
|
||||||
|
address,
|
||||||
|
out var currentGeneration) &&
|
||||||
|
currentGeneration != uploadedGeneration)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2990,6 +3017,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public Sampler Sampler;
|
public Sampler Sampler;
|
||||||
public GuestImageResource? GuestImage;
|
public GuestImageResource? GuestImage;
|
||||||
public GuestDepthResource? GuestDepth;
|
public GuestDepthResource? GuestDepth;
|
||||||
|
// Write-tracker generation of the guest memory the staged pixels
|
||||||
|
// were read from; -1 when unknown. Recorded on the guest image
|
||||||
|
// after upload so stale-content skips can be detected.
|
||||||
|
public long WriteGeneration = -1;
|
||||||
// A sampled render-target alias cannot remain bound to the same
|
// A sampled render-target alias cannot remain bound to the same
|
||||||
// image while that image is a color attachment. The per-draw
|
// image while that image is a color attachment. The per-draw
|
||||||
// snapshot uses this source to copy the target's pre-draw contents
|
// snapshot uses this source to copy the target's pre-draw contents
|
||||||
@@ -7004,6 +7035,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
|
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
|
||||||
guestImage.CpuContentFingerprint == fingerprint)
|
guestImage.CpuContentFingerprint == fingerprint)
|
||||||
{
|
{
|
||||||
|
// Content unchanged despite a newer write generation: advance
|
||||||
|
// the recorded generation so later draws can skip the copy
|
||||||
|
// again instead of restaging identical texels every draw.
|
||||||
|
if (texture.WriteGeneration >= 0)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_cpuBackedUploadGenerations[texture.Address] =
|
||||||
|
texture.WriteGeneration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7033,6 +7076,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
GuestImage = guestImage,
|
GuestImage = guestImage,
|
||||||
CpuContentFingerprint = fingerprint,
|
CpuContentFingerprint = fingerprint,
|
||||||
UpdatesCpuContent = true,
|
UpdatesCpuContent = true,
|
||||||
|
WriteGeneration = texture.WriteGeneration,
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -7718,6 +7762,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
SamplerState = texture.Sampler,
|
SamplerState = texture.Sampler,
|
||||||
CpuContentFingerprint = contentFingerprint,
|
CpuContentFingerprint = contentFingerprint,
|
||||||
UpdatesCpuContent = texture.Address != 0,
|
UpdatesCpuContent = texture.Address != 0,
|
||||||
|
WriteGeneration = texture.WriteGeneration,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (texture.Address != 0 &&
|
if (texture.Address != 0 &&
|
||||||
@@ -7750,6 +7795,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_availableGuestImages[texture.Address] = guestFormat;
|
_availableGuestImages[texture.Address] = guestFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (texture.WriteGeneration >= 0)
|
||||||
|
{
|
||||||
|
_cpuBackedUploadGenerations[texture.Address] =
|
||||||
|
texture.WriteGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
_guestImageExtents[texture.Address] =
|
_guestImageExtents[texture.Address] =
|
||||||
(width, height, expectedSize);
|
(width, height, expectedSize);
|
||||||
}
|
}
|
||||||
@@ -11222,6 +11273,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Remove(target.Address);
|
_availableGuestImages.Remove(target.Address);
|
||||||
|
_cpuBackedUploadGenerations.Remove(target.Address);
|
||||||
_guestImageExtents.Remove(target.Address);
|
_guestImageExtents.Remove(target.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11246,6 +11298,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_guestImages.Add(target.Address, retained);
|
_guestImages.Add(target.Address, retained);
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
|
_cpuBackedUploadGenerations.Remove(target.Address);
|
||||||
_guestImageExtents[target.Address] = (
|
_guestImageExtents[target.Address] = (
|
||||||
target.Width,
|
target.Width,
|
||||||
target.Height,
|
target.Height,
|
||||||
@@ -13873,6 +13926,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (texture.UpdatesCpuContent)
|
if (texture.UpdatesCpuContent)
|
||||||
{
|
{
|
||||||
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
||||||
|
if (texture.WriteGeneration >= 0)
|
||||||
|
{
|
||||||
|
_cpuBackedUploadGenerations[guestImage.Address] =
|
||||||
|
texture.WriteGeneration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15190,6 +15248,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Clear();
|
_availableGuestImages.Clear();
|
||||||
|
_cpuBackedUploadGenerations.Clear();
|
||||||
_lastOrderedGuestFlipVersions.Clear();
|
_lastOrderedGuestFlipVersions.Clear();
|
||||||
}
|
}
|
||||||
DestroySwapchainResources();
|
DestroySwapchainResources();
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The write generation lets GPU caches detect guest CPU rewrites even after
|
||||||
|
/// another cache owner consumed the (single) dirty flag: the generation is
|
||||||
|
/// monotonic and survives consume/re-arm cycles and range replacement. These
|
||||||
|
/// invariants back the presenter's stale-upload detection for CPU-rewritten
|
||||||
|
/// images (video planes, streamed font atlases).
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class GuestImageWriteTrackerTests
|
||||||
|
{
|
||||||
|
// The tracker aligns to the guest's 4 KiB pages; the mprotect underneath
|
||||||
|
// operates on host pages, which are 16 KiB on Apple Silicon (the emulator
|
||||||
|
// itself always runs with 4 KiB host pages under Rosetta, but this test
|
||||||
|
// host may not). Align the allocation to the largest host page size so
|
||||||
|
// the kernel's rounding stays inside memory this test owns instead of
|
||||||
|
// spilling onto neighbouring heap pages.
|
||||||
|
private const nuint TrackedByteCount = 4096;
|
||||||
|
private const nuint HostPageAlignment = 16384;
|
||||||
|
|
||||||
|
private static ulong AllocateTrackedPages(out void* allocation)
|
||||||
|
{
|
||||||
|
allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment);
|
||||||
|
return (ulong)allocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerationSurvivesDirtyConsume()
|
||||||
|
{
|
||||||
|
if (!GuestImageWriteTracker.Enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = AllocateTrackedPages(out var allocation);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GuestImageWriteTracker.Track(address, TrackedByteCount);
|
||||||
|
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
|
||||||
|
Assert.Equal(0, generation);
|
||||||
|
|
||||||
|
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
||||||
|
Assert.True(GuestImageWriteTracker.ConsumeDirty(address));
|
||||||
|
|
||||||
|
// Consuming the dirty flag must not roll back the generation:
|
||||||
|
// that is exactly what lets a second cache owner still observe
|
||||||
|
// the rewrite after the first owner consumed the flag.
|
||||||
|
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out generation));
|
||||||
|
Assert.Equal(1, generation);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
GuestImageWriteTracker.Untrack(address);
|
||||||
|
NativeMemory.Free(allocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerationIncrementsOncePerArmedLifetime()
|
||||||
|
{
|
||||||
|
if (!GuestImageWriteTracker.Enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = AllocateTrackedPages(out var allocation);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GuestImageWriteTracker.Track(address, TrackedByteCount);
|
||||||
|
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
||||||
|
// The first fault disarmed the range; later writes are free-running
|
||||||
|
// and must not inflate the generation until the owner re-arms.
|
||||||
|
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
||||||
|
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
|
||||||
|
Assert.Equal(1, generation);
|
||||||
|
|
||||||
|
GuestImageWriteTracker.Rearm(address);
|
||||||
|
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
||||||
|
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out generation));
|
||||||
|
Assert.Equal(2, generation);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
GuestImageWriteTracker.Untrack(address);
|
||||||
|
NativeMemory.Free(allocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerationCarriesAcrossRangeReplacement()
|
||||||
|
{
|
||||||
|
if (!GuestImageWriteTracker.Enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = AllocateTrackedPages(out var allocation);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GuestImageWriteTracker.Track(address, TrackedByteCount);
|
||||||
|
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
||||||
|
|
||||||
|
// Re-registering the same allocation with a different size retires
|
||||||
|
// the range object (the signal handler may still see the old
|
||||||
|
// snapshot) but must carry the generation, otherwise a resize
|
||||||
|
// would hide the rewrite from cache owners.
|
||||||
|
GuestImageWriteTracker.Track(address, 2 * TrackedByteCount);
|
||||||
|
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
|
||||||
|
Assert.Equal(1, generation);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
GuestImageWriteTracker.Untrack(address);
|
||||||
|
NativeMemory.Free(allocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UntrackedAddressHasNoGeneration()
|
||||||
|
{
|
||||||
|
if (!GuestImageWriteTracker.Enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(GuestImageWriteTracker.TryGetWriteGeneration(0xDEAD_0000_0000UL, out _));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user