forked from Mirror/Ryujinx
Use pooled memory and avoid memory copies (#6691)
* perf: use ByteMemoryPool * feat: KPageTableBase/KPageTable new methods to read and write `ReadOnlySequence<byte>` * new: add IWritableBlock.Write(ulong, ReadOnlySequence<byte>) with default impl * perf: use GetReadOnlySequence() instead of GetSpan() * perf: make `Parcel` IDisposable, use `ByteMemoryPool` for internal allocation, and make Parcel consumers dispose of it * remove comment about copySize * remove unnecessary Clear()
This commit is contained in:
parent
7070cf6ae5
commit
216026c096
9 changed files with 98 additions and 25 deletions
|
@ -1,8 +1,10 @@
|
||||||
using Ryujinx.Audio.Backends.Common;
|
using Ryujinx.Audio.Backends.Common;
|
||||||
using Ryujinx.Audio.Common;
|
using Ryujinx.Audio.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
|
@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] samples = new byte[frameCount * _bytesPerFrame];
|
using IMemoryOwner<byte> samplesOwner = ByteMemoryPool.Rent(frameCount * _bytesPerFrame);
|
||||||
|
|
||||||
|
Span<byte> samples = samplesOwner.Memory.Span;
|
||||||
|
|
||||||
_ringBuffer.Read(samples, 0, samples.Length);
|
_ringBuffer.Read(samples, 0, samples.Length);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
using Ryujinx.Audio.Backends.Common;
|
using Ryujinx.Audio.Backends.Common;
|
||||||
using Ryujinx.Audio.Backends.SoundIo.Native;
|
using Ryujinx.Audio.Backends.SoundIo.Native;
|
||||||
using Ryujinx.Audio.Common;
|
using Ryujinx.Audio.Common;
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||||
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
|
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
|
||||||
_outputStream.WriteCallback += Update;
|
_outputStream.WriteCallback += Update;
|
||||||
_outputStream.Volume = requestedVolume;
|
_outputStream.Volume = requestedVolume;
|
||||||
// TODO: Setup other callbacks (errors, ect).
|
// TODO: Setup other callbacks (errors, etc.)
|
||||||
|
|
||||||
_outputStream.Open();
|
_outputStream.Open();
|
||||||
}
|
}
|
||||||
|
@ -120,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||||
|
|
||||||
int channelCount = areas.Length;
|
int channelCount = areas.Length;
|
||||||
|
|
||||||
byte[] samples = new byte[frameCount * bytesPerFrame];
|
using IMemoryOwner<byte> samplesOwner = ByteMemoryPool.Rent(frameCount * bytesPerFrame);
|
||||||
|
|
||||||
|
Span<byte> samples = samplesOwner.Memory.Span;
|
||||||
|
|
||||||
_ringBuffer.Read(samples, 0, samples.Length);
|
_ringBuffer.Read(samples, 0, samples.Length);
|
||||||
|
|
||||||
|
|
|
@ -570,7 +570,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
serverProcess.CpuMemory.Write(copyDst, clientProcess.CpuMemory.GetSpan(copySrc, (int)copySize));
|
serverProcess.CpuMemory.Write(copyDst, clientProcess.CpuMemory.GetReadOnlySequence(copySrc, (int)copySize));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientResult != Result.Success)
|
if (clientResult != Result.Success)
|
||||||
|
@ -858,7 +858,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
clientProcess.CpuMemory.Write(copyDst, serverProcess.CpuMemory.GetSpan(copySrc, (int)copySize));
|
clientProcess.CpuMemory.Write(copyDst, serverProcess.CpuMemory.GetReadOnlySequence(copySrc, (int)copySize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using Ryujinx.Memory.Range;
|
using Ryujinx.Memory.Range;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
@ -34,6 +35,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size)
|
||||||
|
{
|
||||||
|
return _cpuMemory.GetReadOnlySequence(va, size);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override ReadOnlySpan<byte> GetSpan(ulong va, int size)
|
protected override ReadOnlySpan<byte> GetSpan(ulong va, int size)
|
||||||
{
|
{
|
||||||
|
@ -247,6 +254,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
_cpuMemory.SignalMemoryTracking(va, size, write);
|
_cpuMemory.SignalMemoryTracking(va, size, write);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override void Write(ulong va, ReadOnlySequence<byte> data)
|
||||||
|
{
|
||||||
|
_cpuMemory.Write(va, data);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override void Write(ulong va, ReadOnlySpan<byte> data)
|
protected override void Write(ulong va, ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,6 +5,7 @@ using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using Ryujinx.Memory.Range;
|
using Ryujinx.Memory.Range;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
@ -1568,7 +1569,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
|
|
||||||
while (size > 0)
|
while (size > 0)
|
||||||
{
|
{
|
||||||
ulong copySize = 0x100000; // Copy chunck size. Any value will do, moderate sizes are recommended.
|
ulong copySize = int.MaxValue;
|
||||||
|
|
||||||
if (copySize > size)
|
if (copySize > size)
|
||||||
{
|
{
|
||||||
|
@ -1577,11 +1578,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
|
|
||||||
if (toServer)
|
if (toServer)
|
||||||
{
|
{
|
||||||
currentProcess.CpuMemory.Write(serverAddress, GetSpan(clientAddress, (int)copySize));
|
currentProcess.CpuMemory.Write(serverAddress, GetReadOnlySequence(clientAddress, (int)copySize));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Write(clientAddress, currentProcess.CpuMemory.GetSpan(serverAddress, (int)copySize));
|
Write(clientAddress, currentProcess.CpuMemory.GetReadOnlySequence(serverAddress, (int)copySize));
|
||||||
}
|
}
|
||||||
|
|
||||||
serverAddress += copySize;
|
serverAddress += copySize;
|
||||||
|
@ -1911,9 +1912,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
Context.Memory.Fill(GetDramAddressFromPa(dstFirstPagePa), unusedSizeBefore, (byte)_ipcFillValue);
|
Context.Memory.Fill(GetDramAddressFromPa(dstFirstPagePa), unusedSizeBefore, (byte)_ipcFillValue);
|
||||||
|
|
||||||
ulong copySize = addressRounded <= endAddr ? addressRounded - address : size;
|
ulong copySize = addressRounded <= endAddr ? addressRounded - address : size;
|
||||||
var data = srcPageTable.GetSpan(addressTruncated + unusedSizeBefore, (int)copySize);
|
var data = srcPageTable.GetReadOnlySequence(addressTruncated + unusedSizeBefore, (int)copySize);
|
||||||
|
|
||||||
Context.Memory.Write(GetDramAddressFromPa(dstFirstPagePa + unusedSizeBefore), data);
|
((IWritableBlock)Context.Memory).Write(GetDramAddressFromPa(dstFirstPagePa + unusedSizeBefore), data);
|
||||||
|
|
||||||
firstPageFillAddress += unusedSizeBefore + copySize;
|
firstPageFillAddress += unusedSizeBefore + copySize;
|
||||||
|
|
||||||
|
@ -1977,9 +1978,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
if (send)
|
if (send)
|
||||||
{
|
{
|
||||||
ulong copySize = endAddr - endAddrTruncated;
|
ulong copySize = endAddr - endAddrTruncated;
|
||||||
var data = srcPageTable.GetSpan(endAddrTruncated, (int)copySize);
|
var data = srcPageTable.GetReadOnlySequence(endAddrTruncated, (int)copySize);
|
||||||
|
|
||||||
Context.Memory.Write(GetDramAddressFromPa(dstLastPagePa), data);
|
((IWritableBlock)Context.Memory).Write(GetDramAddressFromPa(dstLastPagePa), data);
|
||||||
|
|
||||||
lastPageFillAddr += copySize;
|
lastPageFillAddr += copySize;
|
||||||
|
|
||||||
|
@ -2943,6 +2944,18 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
/// <param name="pageList">Page list where the ranges will be added</param>
|
/// <param name="pageList">Page list where the ranges will be added</param>
|
||||||
protected abstract void GetPhysicalRegions(ulong va, ulong size, KPageList pageList);
|
protected abstract void GetPhysicalRegions(ulong va, ulong size, KPageList pageList);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only sequence of data from CPU mapped memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Allows reading non-contiguous memory without first copying it to a newly allocated single contiguous block.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="va">Virtual address of the data</param>
|
||||||
|
/// <param name="size">Size of the data</param>
|
||||||
|
/// <returns>A read-only sequence of the data</returns>
|
||||||
|
/// <exception cref="Ryujinx.Memory.InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
|
||||||
|
protected abstract ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a read-only span of data from CPU mapped memory.
|
/// Gets a read-only span of data from CPU mapped memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2952,7 +2965,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="va">Virtual address of the data</param>
|
/// <param name="va">Virtual address of the data</param>
|
||||||
/// <param name="size">Size of the data</param>
|
/// <param name="size">Size of the data</param>
|
||||||
/// <param name="tracked">True if read tracking is triggered on the span</param>
|
|
||||||
/// <returns>A read-only span of the data</returns>
|
/// <returns>A read-only span of the data</returns>
|
||||||
/// <exception cref="Ryujinx.Memory.InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
|
/// <exception cref="Ryujinx.Memory.InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
|
||||||
protected abstract ReadOnlySpan<byte> GetSpan(ulong va, int size);
|
protected abstract ReadOnlySpan<byte> GetSpan(ulong va, int size);
|
||||||
|
@ -3060,6 +3072,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
/// <param name="size">Size of the region</param>
|
/// <param name="size">Size of the region</param>
|
||||||
protected abstract void SignalMemoryTracking(ulong va, ulong size, bool write);
|
protected abstract void SignalMemoryTracking(ulong va, ulong size, bool write);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes data to CPU mapped memory, with write tracking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="va">Virtual address to write the data into</param>
|
||||||
|
/// <param name="data">Data to be written</param>
|
||||||
|
/// <exception cref="Ryujinx.Memory.InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
|
||||||
|
protected abstract void Write(ulong va, ReadOnlySequence<byte> data);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes data to CPU mapped memory, with write tracking.
|
/// Writes data to CPU mapped memory, with write tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -13,10 +13,10 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
|
|
||||||
ResultCode OnTransact(uint code, uint flags, ReadOnlySpan<byte> inputParcel, Span<byte> outputParcel)
|
ResultCode OnTransact(uint code, uint flags, ReadOnlySpan<byte> inputParcel, Span<byte> outputParcel)
|
||||||
{
|
{
|
||||||
Parcel inputParcelReader = new(inputParcel.ToArray());
|
using Parcel inputParcelReader = new(inputParcel);
|
||||||
|
|
||||||
// TODO: support objects?
|
// TODO: support objects?
|
||||||
Parcel outputParcelWriter = new((uint)(outputParcel.Length - Unsafe.SizeOf<ParcelHeader>()), 0);
|
using Parcel outputParcelWriter = new((uint)(outputParcel.Length - Unsafe.SizeOf<ParcelHeader>()), 0);
|
||||||
|
|
||||||
string inputInterfaceToken = inputParcelReader.ReadInterfaceToken();
|
string inputInterfaceToken = inputParcelReader.ReadInterfaceToken();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.HOS.Services.SurfaceFlinger.Types;
|
using Ryujinx.HLE.HOS.Services.SurfaceFlinger.Types;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
@ -9,13 +11,13 @@ using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
{
|
{
|
||||||
class Parcel
|
sealed class Parcel : IDisposable
|
||||||
{
|
{
|
||||||
private readonly byte[] _rawData;
|
private readonly IMemoryOwner<byte> _rawDataOwner;
|
||||||
|
|
||||||
private Span<byte> Raw => new(_rawData);
|
private Span<byte> Raw => _rawDataOwner.Memory.Span;
|
||||||
|
|
||||||
private ref ParcelHeader Header => ref MemoryMarshal.Cast<byte, ParcelHeader>(_rawData)[0];
|
private ref ParcelHeader Header => ref MemoryMarshal.Cast<byte, ParcelHeader>(Raw)[0];
|
||||||
|
|
||||||
private Span<byte> Payload => Raw.Slice((int)Header.PayloadOffset, (int)Header.PayloadSize);
|
private Span<byte> Payload => Raw.Slice((int)Header.PayloadOffset, (int)Header.PayloadSize);
|
||||||
|
|
||||||
|
@ -24,9 +26,11 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
private int _payloadPosition;
|
private int _payloadPosition;
|
||||||
private int _objectPosition;
|
private int _objectPosition;
|
||||||
|
|
||||||
public Parcel(byte[] rawData)
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
public Parcel(ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
_rawData = rawData;
|
_rawDataOwner = ByteMemoryPool.RentCopy(data);
|
||||||
|
|
||||||
_payloadPosition = 0;
|
_payloadPosition = 0;
|
||||||
_objectPosition = 0;
|
_objectPosition = 0;
|
||||||
|
@ -36,7 +40,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
{
|
{
|
||||||
uint headerSize = (uint)Unsafe.SizeOf<ParcelHeader>();
|
uint headerSize = (uint)Unsafe.SizeOf<ParcelHeader>();
|
||||||
|
|
||||||
_rawData = new byte[BitUtils.AlignUp<uint>(headerSize + payloadSize + objectsSize, 4)];
|
_rawDataOwner = ByteMemoryPool.RentCleared(BitUtils.AlignUp<uint>(headerSize + payloadSize + objectsSize, 4));
|
||||||
|
|
||||||
Header.PayloadSize = payloadSize;
|
Header.PayloadSize = payloadSize;
|
||||||
Header.ObjectsSize = objectsSize;
|
Header.ObjectsSize = objectsSize;
|
||||||
|
@ -132,7 +136,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
|
|
||||||
// TODO: figure out what this value is
|
// TODO: figure out what this value is
|
||||||
|
|
||||||
WriteInplaceObject(new byte[4] { 0, 0, 0, 0 });
|
Span<byte> fourBytes = stackalloc byte[4];
|
||||||
|
|
||||||
|
WriteInplaceObject(fourBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AndroidStrongPointer<T> ReadStrongPointer<T>() where T : unmanaged, IFlattenable
|
public AndroidStrongPointer<T> ReadStrongPointer<T>() where T : unmanaged, IFlattenable
|
||||||
|
@ -219,5 +225,15 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
|
|
||||||
return Raw[..(int)(Header.PayloadSize + Header.ObjectsSize + Unsafe.SizeOf<ParcelHeader>())];
|
return Raw[..(int)(Header.PayloadSize + Header.ObjectsSize + Unsafe.SizeOf<ParcelHeader>())];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_isDisposed)
|
||||||
|
{
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_rawDataOwner.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,7 +250,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
|
||||||
|
|
||||||
context.Device.System.SurfaceFlinger.SetRenderLayer(layerId);
|
context.Device.System.SurfaceFlinger.SetRenderLayer(layerId);
|
||||||
|
|
||||||
Parcel parcel = new(0x28, 0x4);
|
using Parcel parcel = new(0x28, 0x4);
|
||||||
|
|
||||||
parcel.WriteObject(producer, "dispdrv\0");
|
parcel.WriteObject(producer, "dispdrv\0");
|
||||||
|
|
||||||
|
@ -288,7 +288,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
|
||||||
|
|
||||||
context.Device.System.SurfaceFlinger.SetRenderLayer(layerId);
|
context.Device.System.SurfaceFlinger.SetRenderLayer(layerId);
|
||||||
|
|
||||||
Parcel parcel = new(0x28, 0x4);
|
using Parcel parcel = new(0x28, 0x4);
|
||||||
|
|
||||||
parcel.WriteObject(producer, "dispdrv\0");
|
parcel.WriteObject(producer, "dispdrv\0");
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,25 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
|
||||||
namespace Ryujinx.Memory
|
namespace Ryujinx.Memory
|
||||||
{
|
{
|
||||||
public interface IWritableBlock
|
public interface IWritableBlock
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Writes data to CPU mapped memory, with write tracking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="va">Virtual address to write the data into</param>
|
||||||
|
/// <param name="data">Data to be written</param>
|
||||||
|
/// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
|
||||||
|
void Write(ulong va, ReadOnlySequence<byte> data)
|
||||||
|
{
|
||||||
|
foreach (ReadOnlyMemory<byte> segment in data)
|
||||||
|
{
|
||||||
|
Write(va, segment.Span);
|
||||||
|
va += (ulong)segment.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Write(ulong va, ReadOnlySpan<byte> data);
|
void Write(ulong va, ReadOnlySpan<byte> data);
|
||||||
|
|
||||||
void WriteUntracked(ulong va, ReadOnlySpan<byte> data) => Write(va, data);
|
void WriteUntracked(ulong va, ReadOnlySpan<byte> data) => Write(va, data);
|
||||||
|
|
Reference in a new issue