mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-20 18:06:12 +08:00
[core] speed up guest memory
This commit is contained in:
@@ -9,7 +9,7 @@ namespace SharpEmu.Core.Memory;
|
|||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly object _guestAllocationGate = new();
|
private readonly object _guestAllocationGate = new();
|
||||||
private readonly List<MemoryRegion> _regions = new();
|
private readonly List<MemoryRegion> _regions = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
@@ -78,7 +78,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_regions.Add(new MemoryRegion
|
_regions.Add(new MemoryRegion
|
||||||
{
|
{
|
||||||
@@ -89,6 +90,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Protection = protection
|
Protection = protection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
var allocationKind = executable ? "executable memory" : "data memory";
|
var allocationKind = executable ? "executable memory" : "data memory";
|
||||||
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||||
@@ -202,7 +207,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_regions.Add(new MemoryRegion
|
_regions.Add(new MemoryRegion
|
||||||
{
|
{
|
||||||
@@ -213,6 +219,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Protection = protection
|
Protection = protection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
var allocationKind = reservedOnly
|
var allocationKind = reservedOnly
|
||||||
? "reserved data memory (lazy commit)"
|
? "reserved data memory (lazy commit)"
|
||||||
@@ -315,7 +325,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
lock (_guestAllocationGate)
|
lock (_guestAllocationGate)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -324,6 +335,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_guestAllocationArenaBase = 0;
|
||||||
_guestAllocationOffset = 0;
|
_guestAllocationOffset = 0;
|
||||||
@@ -343,7 +358,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var mapEnd = AlignUp(segmentEnd, PageSize);
|
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||||
var mapSize = checked(mapEnd - mapStart);
|
var mapSize = checked(mapEnd - mapStart);
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var existingRegion = FindRegion(mapStart, mapSize);
|
var existingRegion = FindRegion(mapStart, mapSize);
|
||||||
if (existingRegion == null)
|
if (existingRegion == null)
|
||||||
@@ -376,6 +392,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||||
@@ -425,7 +445,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||||
for (var i = 0; i < _regions.Count; i++)
|
for (var i = 0; i < _regions.Count; i++)
|
||||||
@@ -440,11 +461,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
var requiresExclusiveAccess = false;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -456,48 +483,55 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
if (region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
return false;
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
}
|
|
||||||
|
|
||||||
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
|
||||||
{
|
|
||||||
fixed (byte* destPtr = destination)
|
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fixed (byte* destPtr = destination)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
{
|
{
|
||||||
RestorePageProtections(touchedPages);
|
requiresExclusiveAccess = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requiresExclusiveAccess)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TryReadExclusive(virtualAddress, destination);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
var requiresExclusiveAccess = false;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -509,47 +543,148 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
if (region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
return false;
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
}
|
|
||||||
|
|
||||||
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
|
||||||
{
|
|
||||||
fixed (byte* srcPtr = source)
|
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fixed (byte* srcPtr = source)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
{
|
{
|
||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
requiresExclusiveAccess = true;
|
||||||
if (IsExecutableProtection(oldProtect))
|
break;
|
||||||
{
|
}
|
||||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
|
||||||
}
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requiresExclusiveAccess)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TryWriteExclusive(virtualAddress, source);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RestorePageProtections(touchedPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||||
|
if (IsExecutableProtection(oldProtect))
|
||||||
|
{
|
||||||
|
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
||||||
@@ -561,7 +696,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public void* GetPointer(ulong virtualAddress)
|
public void* GetPointer(ulong virtualAddress)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -573,11 +709,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -588,6 +729,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||||
@@ -611,7 +756,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
var end = address + size;
|
var end = address + size;
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -622,6 +768,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
return overlapEnd != 0;
|
return overlapEnd != 0;
|
||||||
}
|
}
|
||||||
@@ -707,15 +857,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var commitProtection = GetCommitProtection(region);
|
var commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
var pageAddress = startPage;
|
||||||
|
while (pageAddress < endPage)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||||
|
? ulong.MaxValue
|
||||||
|
: info.BaseAddress + info.RegionSize;
|
||||||
|
var rangeEnd = Math.Min(endPage, queriedEnd);
|
||||||
|
if (rangeEnd <= pageAddress)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (info.State == MEM_COMMIT)
|
if (info.State == MEM_COMMIT)
|
||||||
{
|
{
|
||||||
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,10 +885,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
|
var commitSize = rangeEnd - pageAddress;
|
||||||
|
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user