|
@@ -1,11 +1,15 @@
|
|
|
using System;
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
using System.Collections.Generic;
|
|
|
|
|
+using System.Numerics;
|
|
|
|
|
+using System.Runtime.CompilerServices;
|
|
|
|
|
+using System.Threading;
|
|
|
|
|
|
|
|
namespace Ryujinx.Memory.Tracking
|
|
namespace Ryujinx.Memory.Tracking
|
|
|
{
|
|
{
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// A region handle that tracks a large region using many smaller handles, to provide
|
|
/// A region handle that tracks a large region using many smaller handles, to provide
|
|
|
- /// granular tracking that can be used to track partial updates.
|
|
|
|
|
|
|
+ /// granular tracking that can be used to track partial updates. Backed by a bitmap
|
|
|
|
|
+ /// to improve performance when scanning large regions.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public class MultiRegionHandle : IMultiRegionHandle
|
|
public class MultiRegionHandle : IMultiRegionHandle
|
|
|
{
|
|
{
|
|
@@ -17,6 +21,12 @@ namespace Ryujinx.Memory.Tracking
|
|
|
private readonly ulong Granularity;
|
|
private readonly ulong Granularity;
|
|
|
private readonly ulong Size;
|
|
private readonly ulong Size;
|
|
|
|
|
|
|
|
|
|
+ private ConcurrentBitmap _dirtyBitmap;
|
|
|
|
|
+
|
|
|
|
|
+ private int _sequenceNumber;
|
|
|
|
|
+ private BitMap _sequenceNumberBitmap;
|
|
|
|
|
+ private int _uncheckedHandles;
|
|
|
|
|
+
|
|
|
public bool Dirty { get; private set; } = true;
|
|
public bool Dirty { get; private set; } = true;
|
|
|
|
|
|
|
|
internal MultiRegionHandle(MemoryTracking tracking, ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity)
|
|
internal MultiRegionHandle(MemoryTracking tracking, ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity)
|
|
@@ -24,6 +34,9 @@ namespace Ryujinx.Memory.Tracking
|
|
|
_handles = new RegionHandle[size / granularity];
|
|
_handles = new RegionHandle[size / granularity];
|
|
|
Granularity = granularity;
|
|
Granularity = granularity;
|
|
|
|
|
|
|
|
|
|
+ _dirtyBitmap = new ConcurrentBitmap(_handles.Length, true);
|
|
|
|
|
+ _sequenceNumberBitmap = new BitMap(_handles.Length);
|
|
|
|
|
+
|
|
|
int i = 0;
|
|
int i = 0;
|
|
|
|
|
|
|
|
if (handles != null)
|
|
if (handles != null)
|
|
@@ -40,37 +53,43 @@ namespace Ryujinx.Memory.Tracking
|
|
|
// Fill any gap left before this handle.
|
|
// Fill any gap left before this handle.
|
|
|
while (i < startIndex)
|
|
while (i < startIndex)
|
|
|
{
|
|
{
|
|
|
- RegionHandle fillHandle = tracking.BeginTracking(address + (ulong)i * granularity, granularity);
|
|
|
|
|
|
|
+ RegionHandle fillHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i);
|
|
|
fillHandle.Parent = this;
|
|
fillHandle.Parent = this;
|
|
|
_handles[i++] = fillHandle;
|
|
_handles[i++] = fillHandle;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (handle.Size == granularity)
|
|
|
|
|
- {
|
|
|
|
|
- handle.Parent = this;
|
|
|
|
|
- _handles[i++] = handle;
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
|
|
+ lock (tracking.TrackingLock)
|
|
|
{
|
|
{
|
|
|
- int endIndex = (int)((handle.EndAddress - address) / granularity);
|
|
|
|
|
-
|
|
|
|
|
- while (i < endIndex)
|
|
|
|
|
|
|
+ if (handle is RegionHandle bitHandle && handle.Size == granularity)
|
|
|
{
|
|
{
|
|
|
- RegionHandle splitHandle = tracking.BeginTracking(address + (ulong)i * granularity, granularity);
|
|
|
|
|
- splitHandle.Parent = this;
|
|
|
|
|
|
|
+ handle.Parent = this;
|
|
|
|
|
+
|
|
|
|
|
+ bitHandle.ReplaceBitmap(_dirtyBitmap, i);
|
|
|
|
|
|
|
|
- splitHandle.Reprotect(handle.Dirty);
|
|
|
|
|
|
|
+ _handles[i++] = bitHandle;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ int endIndex = (int)((handle.EndAddress - address) / granularity);
|
|
|
|
|
|
|
|
- RegionSignal signal = handle.PreAction;
|
|
|
|
|
- if (signal != null)
|
|
|
|
|
|
|
+ while (i < endIndex)
|
|
|
{
|
|
{
|
|
|
- splitHandle.RegisterAction(signal);
|
|
|
|
|
|
|
+ RegionHandle splitHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i);
|
|
|
|
|
+ splitHandle.Parent = this;
|
|
|
|
|
+
|
|
|
|
|
+ splitHandle.Reprotect(handle.Dirty);
|
|
|
|
|
+
|
|
|
|
|
+ RegionSignal signal = handle.PreAction;
|
|
|
|
|
+ if (signal != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ splitHandle.RegisterAction(signal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _handles[i++] = splitHandle;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- _handles[i++] = splitHandle;
|
|
|
|
|
|
|
+ handle.Dispose();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- handle.Dispose();
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -78,27 +97,20 @@ namespace Ryujinx.Memory.Tracking
|
|
|
// Fill any remaining space with new handles.
|
|
// Fill any remaining space with new handles.
|
|
|
while (i < _handles.Length)
|
|
while (i < _handles.Length)
|
|
|
{
|
|
{
|
|
|
- RegionHandle handle = tracking.BeginTracking(address + (ulong)i * granularity, granularity);
|
|
|
|
|
|
|
+ RegionHandle handle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i);
|
|
|
handle.Parent = this;
|
|
handle.Parent = this;
|
|
|
_handles[i++] = handle;
|
|
_handles[i++] = handle;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ _uncheckedHandles = _handles.Length;
|
|
|
|
|
+
|
|
|
Address = address;
|
|
Address = address;
|
|
|
Size = size;
|
|
Size = size;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void ForceDirty(ulong address, ulong size)
|
|
|
|
|
|
|
+ public void SignalWrite()
|
|
|
{
|
|
{
|
|
|
Dirty = true;
|
|
Dirty = true;
|
|
|
-
|
|
|
|
|
- int startHandle = (int)((address - Address) / Granularity);
|
|
|
|
|
- int lastHandle = (int)((address + (size - 1) - Address) / Granularity);
|
|
|
|
|
-
|
|
|
|
|
- for (int i = startHandle; i <= lastHandle; i++)
|
|
|
|
|
- {
|
|
|
|
|
- _handles[i].SequenceNumber--;
|
|
|
|
|
- _handles[i].ForceDirty();
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public IEnumerable<RegionHandle> GetHandles()
|
|
public IEnumerable<RegionHandle> GetHandles()
|
|
@@ -106,9 +118,22 @@ namespace Ryujinx.Memory.Tracking
|
|
|
return _handles;
|
|
return _handles;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void SignalWrite()
|
|
|
|
|
|
|
+ public void ForceDirty(ulong address, ulong size)
|
|
|
{
|
|
{
|
|
|
Dirty = true;
|
|
Dirty = true;
|
|
|
|
|
+
|
|
|
|
|
+ int startHandle = (int)((address - Address) / Granularity);
|
|
|
|
|
+ int lastHandle = (int)((address + (size - 1) - Address) / Granularity);
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = startHandle; i <= lastHandle; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_sequenceNumberBitmap.Clear(i))
|
|
|
|
|
+ {
|
|
|
|
|
+ _uncheckedHandles++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _handles[i].ForceDirty();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void QueryModified(Action<ulong, ulong> modifiedAction)
|
|
public void QueryModified(Action<ulong, ulong> modifiedAction)
|
|
@@ -123,39 +148,140 @@ namespace Ryujinx.Memory.Tracking
|
|
|
QueryModified(Address, Size, modifiedAction);
|
|
QueryModified(Address, Size, modifiedAction);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
+ private void ParseDirtyBits(long dirtyBits, ref int baseBit, ref int prevHandle, ref ulong rgStart, ref ulong rgSize, Action<ulong, ulong> modifiedAction)
|
|
|
|
|
+ {
|
|
|
|
|
+ while (dirtyBits != 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ int bit = BitOperations.TrailingZeroCount(dirtyBits);
|
|
|
|
|
+
|
|
|
|
|
+ dirtyBits &= ~(1L << bit);
|
|
|
|
|
+
|
|
|
|
|
+ int handleIndex = baseBit + bit;
|
|
|
|
|
+
|
|
|
|
|
+ RegionHandle handle = _handles[handleIndex];
|
|
|
|
|
+
|
|
|
|
|
+ if (handleIndex != prevHandle + 1)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Submit handles scanned until the gap as dirty
|
|
|
|
|
+ if (rgSize != 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ modifiedAction(rgStart, rgSize);
|
|
|
|
|
+ rgSize = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ rgStart = handle.Address;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (handle.Dirty)
|
|
|
|
|
+ {
|
|
|
|
|
+ rgSize += handle.Size;
|
|
|
|
|
+ handle.Reprotect();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ prevHandle = handleIndex;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ baseBit += ConcurrentBitmap.IntSize;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction)
|
|
public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction)
|
|
|
{
|
|
{
|
|
|
int startHandle = (int)((address - Address) / Granularity);
|
|
int startHandle = (int)((address - Address) / Granularity);
|
|
|
int lastHandle = (int)((address + (size - 1) - Address) / Granularity);
|
|
int lastHandle = (int)((address + (size - 1) - Address) / Granularity);
|
|
|
|
|
|
|
|
ulong rgStart = _handles[startHandle].Address;
|
|
ulong rgStart = _handles[startHandle].Address;
|
|
|
- ulong rgSize = 0;
|
|
|
|
|
|
|
|
|
|
- for (int i = startHandle; i <= lastHandle; i++)
|
|
|
|
|
|
|
+ if (startHandle == lastHandle)
|
|
|
{
|
|
{
|
|
|
- RegionHandle handle = _handles[i];
|
|
|
|
|
|
|
+ RegionHandle handle = _handles[startHandle];
|
|
|
|
|
|
|
|
if (handle.Dirty)
|
|
if (handle.Dirty)
|
|
|
{
|
|
{
|
|
|
- rgSize += handle.Size;
|
|
|
|
|
handle.Reprotect();
|
|
handle.Reprotect();
|
|
|
|
|
+ modifiedAction(rgStart, handle.Size);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ulong rgSize = 0;
|
|
|
|
|
+
|
|
|
|
|
+ long[] masks = _dirtyBitmap.Masks;
|
|
|
|
|
+
|
|
|
|
|
+ int startIndex = startHandle >> ConcurrentBitmap.IntShift;
|
|
|
|
|
+ int startBit = startHandle & ConcurrentBitmap.IntMask;
|
|
|
|
|
+ long startMask = -1L << startBit;
|
|
|
|
|
+
|
|
|
|
|
+ int endIndex = lastHandle >> ConcurrentBitmap.IntShift;
|
|
|
|
|
+ int endBit = lastHandle & ConcurrentBitmap.IntMask;
|
|
|
|
|
+ long endMask = (long)(ulong.MaxValue >> (ConcurrentBitmap.IntMask - endBit));
|
|
|
|
|
+
|
|
|
|
|
+ long startValue = Volatile.Read(ref masks[startIndex]);
|
|
|
|
|
+
|
|
|
|
|
+ int baseBit = startIndex << ConcurrentBitmap.IntShift;
|
|
|
|
|
+ int prevHandle = startHandle - 1;
|
|
|
|
|
+
|
|
|
|
|
+ if (startIndex == endIndex)
|
|
|
|
|
+ {
|
|
|
|
|
+ ParseDirtyBits(startValue & startMask & endMask, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ParseDirtyBits(startValue & startMask, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = startIndex + 1; i < endIndex; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ ParseDirtyBits(Volatile.Read(ref masks[i]), ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
}
|
|
}
|
|
|
- else
|
|
|
|
|
|
|
+
|
|
|
|
|
+ long endValue = Volatile.Read(ref masks[endIndex]);
|
|
|
|
|
+
|
|
|
|
|
+ ParseDirtyBits(endValue & endMask, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (rgSize != 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ modifiedAction(rgStart, rgSize);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
+ private void ParseDirtyBits(long dirtyBits, long mask, int index, long[] seqMasks, ref int baseBit, ref int prevHandle, ref ulong rgStart, ref ulong rgSize, Action<ulong, ulong> modifiedAction)
|
|
|
|
|
+ {
|
|
|
|
|
+ long seqMask = mask & ~seqMasks[index];
|
|
|
|
|
+ dirtyBits &= seqMask;
|
|
|
|
|
+
|
|
|
|
|
+ while (dirtyBits != 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ int bit = BitOperations.TrailingZeroCount(dirtyBits);
|
|
|
|
|
+
|
|
|
|
|
+ dirtyBits &= ~(1L << bit);
|
|
|
|
|
+
|
|
|
|
|
+ int handleIndex = baseBit + bit;
|
|
|
|
|
+
|
|
|
|
|
+ RegionHandle handle = _handles[handleIndex];
|
|
|
|
|
+
|
|
|
|
|
+ if (handleIndex != prevHandle + 1)
|
|
|
{
|
|
{
|
|
|
- // Submit the region scanned so far as dirty
|
|
|
|
|
|
|
+ // Submit handles scanned until the gap as dirty
|
|
|
if (rgSize != 0)
|
|
if (rgSize != 0)
|
|
|
{
|
|
{
|
|
|
modifiedAction(rgStart, rgSize);
|
|
modifiedAction(rgStart, rgSize);
|
|
|
rgSize = 0;
|
|
rgSize = 0;
|
|
|
}
|
|
}
|
|
|
- rgStart = handle.EndAddress;
|
|
|
|
|
|
|
+ rgStart = handle.Address;
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- if (rgSize != 0)
|
|
|
|
|
- {
|
|
|
|
|
- modifiedAction(rgStart, rgSize);
|
|
|
|
|
|
|
+ rgSize += handle.Size;
|
|
|
|
|
+ handle.Reprotect();
|
|
|
|
|
+
|
|
|
|
|
+ prevHandle = handleIndex;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ seqMasks[index] |= mask;
|
|
|
|
|
+ _uncheckedHandles -= BitOperations.PopCount((ulong)seqMask);
|
|
|
|
|
+
|
|
|
|
|
+ baseBit += ConcurrentBitmap.IntSize;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction, int sequenceNumber)
|
|
public void QueryModified(ulong address, ulong size, Action<ulong, ulong> modifiedAction, int sequenceNumber)
|
|
@@ -163,30 +289,76 @@ namespace Ryujinx.Memory.Tracking
|
|
|
int startHandle = (int)((address - Address) / Granularity);
|
|
int startHandle = (int)((address - Address) / Granularity);
|
|
|
int lastHandle = (int)((address + (size - 1) - Address) / Granularity);
|
|
int lastHandle = (int)((address + (size - 1) - Address) / Granularity);
|
|
|
|
|
|
|
|
- ulong rgStart = _handles[startHandle].Address;
|
|
|
|
|
- ulong rgSize = 0;
|
|
|
|
|
|
|
+ ulong rgStart = Address + (ulong)startHandle * Granularity;
|
|
|
|
|
|
|
|
- for (int i = startHandle; i <= lastHandle; i++)
|
|
|
|
|
|
|
+ if (sequenceNumber != _sequenceNumber)
|
|
|
{
|
|
{
|
|
|
- RegionHandle handle = _handles[i];
|
|
|
|
|
-
|
|
|
|
|
- if (sequenceNumber != handle.SequenceNumber && handle.DirtyOrVolatile())
|
|
|
|
|
|
|
+ if (_uncheckedHandles != _handles.Length)
|
|
|
{
|
|
{
|
|
|
- rgSize += handle.Size;
|
|
|
|
|
- handle.Reprotect();
|
|
|
|
|
|
|
+ _sequenceNumberBitmap.Clear();
|
|
|
|
|
+ _uncheckedHandles = _handles.Length;
|
|
|
}
|
|
}
|
|
|
- else
|
|
|
|
|
|
|
+
|
|
|
|
|
+ _sequenceNumber = sequenceNumber;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (startHandle == lastHandle)
|
|
|
|
|
+ {
|
|
|
|
|
+ var handle = _handles[startHandle];
|
|
|
|
|
+ if (_sequenceNumberBitmap.Set(startHandle))
|
|
|
{
|
|
{
|
|
|
- // Submit the region scanned so far as dirty
|
|
|
|
|
- if (rgSize != 0)
|
|
|
|
|
|
|
+ _uncheckedHandles--;
|
|
|
|
|
+
|
|
|
|
|
+ if (handle.DirtyOrVolatile())
|
|
|
{
|
|
{
|
|
|
- modifiedAction(rgStart, rgSize);
|
|
|
|
|
- rgSize = 0;
|
|
|
|
|
|
|
+ handle.Reprotect();
|
|
|
|
|
+
|
|
|
|
|
+ modifiedAction(rgStart, handle.Size);
|
|
|
}
|
|
}
|
|
|
- rgStart = handle.EndAddress;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- handle.SequenceNumber = sequenceNumber;
|
|
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (_uncheckedHandles == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ulong rgSize = 0;
|
|
|
|
|
+
|
|
|
|
|
+ long[] seqMasks = _sequenceNumberBitmap.Masks;
|
|
|
|
|
+ long[] masks = _dirtyBitmap.Masks;
|
|
|
|
|
+
|
|
|
|
|
+ int startIndex = startHandle >> ConcurrentBitmap.IntShift;
|
|
|
|
|
+ int startBit = startHandle & ConcurrentBitmap.IntMask;
|
|
|
|
|
+ long startMask = -1L << startBit;
|
|
|
|
|
+
|
|
|
|
|
+ int endIndex = lastHandle >> ConcurrentBitmap.IntShift;
|
|
|
|
|
+ int endBit = lastHandle & ConcurrentBitmap.IntMask;
|
|
|
|
|
+ long endMask = (long)(ulong.MaxValue >> (ConcurrentBitmap.IntMask - endBit));
|
|
|
|
|
+
|
|
|
|
|
+ long startValue = Volatile.Read(ref masks[startIndex]);
|
|
|
|
|
+
|
|
|
|
|
+ int baseBit = startIndex << ConcurrentBitmap.IntShift;
|
|
|
|
|
+ int prevHandle = startHandle - 1;
|
|
|
|
|
+
|
|
|
|
|
+ if (startIndex == endIndex)
|
|
|
|
|
+ {
|
|
|
|
|
+ ParseDirtyBits(startValue, startMask & endMask, startIndex, seqMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ParseDirtyBits(startValue, startMask, startIndex, seqMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = startIndex + 1; i < endIndex; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ ParseDirtyBits(Volatile.Read(ref masks[i]), -1L, i, seqMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long endValue = Volatile.Read(ref masks[endIndex]);
|
|
|
|
|
+
|
|
|
|
|
+ ParseDirtyBits(endValue, endMask, endIndex, seqMasks, ref baseBit, ref prevHandle, ref rgStart, ref rgSize, modifiedAction);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (rgSize != 0)
|
|
if (rgSize != 0)
|