Compare commits
5 Commits
1016_part3
...
des_page_f
| Author | SHA1 | Date |
|---|---|---|
|
|
5f26576d6b | |
|
|
ca102a67c7 | |
|
|
a7cc266702 | |
|
|
767bca9e26 | |
|
|
ac1f739353 |
|
|
@ -9,10 +9,23 @@ public sealed partial class DirectExecutionBackend
|
||||||
{
|
{
|
||||||
private const int HostStackStateBytes = 3 * sizeof(ulong);
|
private const int HostStackStateBytes = 3 * sizeof(ulong);
|
||||||
|
|
||||||
private static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
|
internal static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsWindows() &&
|
||||||
|
QueryVirtualMemoryInformation(-1, (void*)stackPointer, 0, out var allocation,
|
||||||
|
(nuint)sizeof(StackMemoryRegionInformation), null) != 0 &&
|
||||||
|
((allocation.Flags & 1) == 0 || allocation.CommitSize == allocation.RegionSize) &&
|
||||||
|
allocation.RegionSize != 0 && allocation.RegionSize <= ulong.MaxValue - allocation.AllocationBase &&
|
||||||
|
stackPointer >= allocation.AllocationBase && stackPointer - allocation.AllocationBase < allocation.RegionSize)
|
||||||
|
{
|
||||||
|
bottom = allocation.AllocationBase;
|
||||||
|
top = bottom + allocation.RegionSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bottom = top = 0;
|
bottom = top = 0;
|
||||||
if (VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
var queried = VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64));
|
||||||
|
if (queried == 0 ||
|
||||||
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
|
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -176,4 +189,18 @@ public sealed partial class DirectExecutionBackend
|
||||||
|
|
||||||
[DllImport("kernel32.dll", ExactSpelling = true)]
|
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||||
private static extern void GetCurrentThreadStackLimits(out ulong lowLimit, out ulong highLimit);
|
private static extern void GetCurrentThreadStackLimits(out ulong lowLimit, out ulong highLimit);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct StackMemoryRegionInformation
|
||||||
|
{
|
||||||
|
public ulong AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public uint Flags;
|
||||||
|
public ulong RegionSize;
|
||||||
|
public ulong CommitSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("api-ms-win-core-memory-l1-1-4.dll", ExactSpelling = true)]
|
||||||
|
private static extern unsafe int QueryVirtualMemoryInformation(nint process, void* address, int informationClass,
|
||||||
|
out StackMemoryRegionInformation information, nuint informationSize, nuint* returnSize);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ public sealed class GuestGpuMemory : IDisposable
|
||||||
private readonly PageGuard _pages;
|
private readonly PageGuard _pages;
|
||||||
private readonly ReaderWriterLockSlim _spansLock = new();
|
private readonly ReaderWriterLockSlim _spansLock = new();
|
||||||
private readonly SpanSet _spans = new();
|
private readonly SpanSet _spans = new();
|
||||||
|
private long _spanVersion;
|
||||||
|
|
||||||
|
public long SpanVersion => Interlocked.Read(ref _spanVersion);
|
||||||
private sealed record GpuAttachment(IGpuQueueRelay? Gpu, IGpuTickScheduler? Scheduler);
|
private sealed record GpuAttachment(IGpuQueueRelay? Gpu, IGpuTickScheduler? Scheduler);
|
||||||
|
|
||||||
private readonly object _attachGate = new();
|
private readonly object _attachGate = new();
|
||||||
|
|
@ -204,6 +207,7 @@ public sealed class GuestGpuMemory : IDisposable
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_spans.Add(address, size);
|
_spans.Add(address, size);
|
||||||
|
Interlocked.Increment(ref _spanVersion);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -276,6 +280,7 @@ public sealed class GuestGpuMemory : IDisposable
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_spans.Remove(address, size);
|
_spans.Remove(address, size);
|
||||||
|
Interlocked.Increment(ref _spanVersion);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -590,7 +590,16 @@ public static class AjmExports
|
||||||
return ctx.SetReturn(OrbisAjmErrorJobCreation);
|
return ctx.SetReturn(OrbisAjmErrorJobCreation);
|
||||||
}
|
}
|
||||||
|
|
||||||
var status = TryGetInstance(instanceId, out _) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
|
var status = TryGetInstance(instanceId, out var instance) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
|
||||||
|
Span<byte> gapless = stackalloc byte[AjmSidebandGaplessDecodeBytes];
|
||||||
|
if (status == 0 && gaplessAddress != 0 && ctx.Memory.TryRead(gaplessAddress, gapless))
|
||||||
|
{
|
||||||
|
instance!.Atrac9?.SetGapless(
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(gapless),
|
||||||
|
BinaryPrimitives.ReadUInt16LittleEndian(gapless[4..]),
|
||||||
|
reset != 0);
|
||||||
|
}
|
||||||
|
|
||||||
WriteBasicResult(ctx, resultAddress, status);
|
WriteBasicResult(ctx, resultAddress, status);
|
||||||
Trace(
|
Trace(
|
||||||
$"batch_job_set_gapless_decode instance=0x{instanceId:X8} " +
|
$"batch_job_set_gapless_decode instance=0x{instanceId:X8} " +
|
||||||
|
|
@ -1163,6 +1172,10 @@ public static class AjmExports
|
||||||
|
|
||||||
if ((flags & AjmJobSidebandFlagGaplessDecode) != 0 && (ulong)(offset + AjmSidebandGaplessDecodeBytes) <= size)
|
if ((flags & AjmJobSidebandFlagGaplessDecode) != 0 && (ulong)(offset + AjmSidebandGaplessDecodeBytes) <= size)
|
||||||
{
|
{
|
||||||
|
var gapless = instance?.Atrac9?.Gapless ?? default;
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(sideband[offset..], gapless.TotalSamples);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(sideband[(offset + 4)..], gapless.SkipSamples);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(sideband[(offset + 6)..], gapless.SkippedSamples);
|
||||||
offset += AjmSidebandGaplessDecodeBytes;
|
offset += AjmSidebandGaplessDecodeBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ internal readonly record struct Atrac9DecodeResult(
|
||||||
ulong TotalDecodedSamples,
|
ulong TotalDecodedSamples,
|
||||||
uint Frames);
|
uint Frames);
|
||||||
|
|
||||||
|
internal readonly record struct Atrac9Gapless(uint TotalSamples, ushort SkipSamples, ushort SkippedSamples);
|
||||||
|
|
||||||
internal sealed class Atrac9DecodeState
|
internal sealed class Atrac9DecodeState
|
||||||
{
|
{
|
||||||
internal const int ResultNotInitialized = 0x00000001;
|
internal const int ResultNotInitialized = 0x00000001;
|
||||||
|
|
@ -47,6 +49,33 @@ internal sealed class Atrac9DecodeState
|
||||||
private int _containerHeaderLength;
|
private int _containerHeaderLength;
|
||||||
private int _compressedLength;
|
private int _compressedLength;
|
||||||
private ulong _totalDecodedSamples;
|
private ulong _totalDecodedSamples;
|
||||||
|
private Atrac9Gapless _gaplessInit;
|
||||||
|
private Atrac9Gapless _gapless;
|
||||||
|
|
||||||
|
public Atrac9Gapless Gapless
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _gapless;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGapless(uint totalSamples, ushort skipSamples, bool reset)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_gaplessInit = new Atrac9Gapless(totalSamples, skipSamples, 0);
|
||||||
|
if (reset || _gapless == default)
|
||||||
|
{
|
||||||
|
_gapless = _gaplessInit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasSampleLimit => _gaplessInit.TotalSamples is not 0 and not uint.MaxValue;
|
||||||
|
|
||||||
public Atrac9Config? Config
|
public Atrac9Config? Config
|
||||||
{
|
{
|
||||||
|
|
@ -82,6 +111,7 @@ internal sealed class Atrac9DecodeState
|
||||||
_compressedLength = 0;
|
_compressedLength = 0;
|
||||||
_totalDecodedSamples = 0;
|
_totalDecodedSamples = 0;
|
||||||
_containerHeaderLength = 0;
|
_containerHeaderLength = 0;
|
||||||
|
_gapless = _gaplessInit;
|
||||||
Trace(
|
Trace(
|
||||||
$"initialized config={Convert.ToHexString(normalizedConfig)} channels={config.ChannelCount} " +
|
$"initialized config={Convert.ToHexString(normalizedConfig)} channels={config.ChannelCount} " +
|
||||||
$"rate={config.SampleRate} frame_samples={config.FrameSamples} " +
|
$"rate={config.SampleRate} frame_samples={config.FrameSamples} " +
|
||||||
|
|
@ -114,6 +144,7 @@ internal sealed class Atrac9DecodeState
|
||||||
_compressedLength = 0;
|
_compressedLength = 0;
|
||||||
_totalDecodedSamples = 0;
|
_totalDecodedSamples = 0;
|
||||||
_containerHeaderLength = 0;
|
_containerHeaderLength = 0;
|
||||||
|
_gapless = _gaplessInit;
|
||||||
if (_compressed is not null)
|
if (_compressed is not null)
|
||||||
{
|
{
|
||||||
Array.Clear(_compressed);
|
Array.Clear(_compressed);
|
||||||
|
|
@ -153,7 +184,6 @@ internal sealed class Atrac9DecodeState
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytesPerSample = GetBytesPerSample(encoding);
|
var bytesPerSample = GetBytesPerSample(encoding);
|
||||||
var outputBytesPerSuperframe = checked(config.SuperframeSamples * channels * bytesPerSample);
|
|
||||||
var consumed = 0;
|
var consumed = 0;
|
||||||
var written = 0;
|
var written = 0;
|
||||||
uint frames = 0;
|
uint frames = 0;
|
||||||
|
|
@ -162,6 +192,11 @@ internal sealed class Atrac9DecodeState
|
||||||
while (_compressedLength == config.SuperframeBytes ||
|
while (_compressedLength == config.SuperframeBytes ||
|
||||||
consumed < input.Length)
|
consumed < input.Length)
|
||||||
{
|
{
|
||||||
|
if (HasSampleLimit && _gapless.TotalSamples == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Titles that stream whole .at9 files hand AJM the RIFF/WAVE
|
// Titles that stream whole .at9 files hand AJM the RIFF/WAVE
|
||||||
// container rather than a pointer into its `data` chunk, so the
|
// container rather than a pointer into its `data` chunk, so the
|
||||||
// stream has to be advanced past the header before the first
|
// stream has to be advanced past the header before the first
|
||||||
|
|
@ -202,7 +237,15 @@ internal sealed class Atrac9DecodeState
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.Length - written < outputBytesPerSuperframe)
|
var skip = Math.Min(config.SuperframeSamples, (int)_gapless.SkipSamples);
|
||||||
|
var samples = config.SuperframeSamples - skip;
|
||||||
|
if (HasSampleLimit)
|
||||||
|
{
|
||||||
|
samples = (int)Math.Min((uint)samples, _gapless.TotalSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputBytes = checked(samples * channels * bytesPerSample);
|
||||||
|
if (output.Length - written < outputBytes)
|
||||||
{
|
{
|
||||||
status |= ResultNotEnoughRoom;
|
status |= ResultNotEnoughRoom;
|
||||||
break;
|
break;
|
||||||
|
|
@ -231,14 +274,19 @@ internal sealed class Atrac9DecodeState
|
||||||
|
|
||||||
WriteInterleaved(
|
WriteInterleaved(
|
||||||
_planarPcm,
|
_planarPcm,
|
||||||
output.Slice(written, outputBytesPerSuperframe),
|
output.Slice(written, outputBytes),
|
||||||
config.SuperframeSamples,
|
skip,
|
||||||
|
samples,
|
||||||
channels,
|
channels,
|
||||||
encoding);
|
encoding);
|
||||||
|
|
||||||
written += outputBytesPerSuperframe;
|
_gapless = new Atrac9Gapless(
|
||||||
|
HasSampleLimit ? _gapless.TotalSamples - (uint)samples : _gapless.TotalSamples,
|
||||||
|
(ushort)(_gapless.SkipSamples - skip),
|
||||||
|
(ushort)Math.Min(ushort.MaxValue, _gapless.SkippedSamples + config.SuperframeSamples - samples));
|
||||||
|
written += outputBytes;
|
||||||
_compressedLength = 0;
|
_compressedLength = 0;
|
||||||
_totalDecodedSamples += unchecked((uint)config.SuperframeSamples);
|
_totalDecodedSamples += unchecked((uint)samples);
|
||||||
frames += unchecked((uint)config.FramesPerSuperframe);
|
frames += unchecked((uint)config.FramesPerSuperframe);
|
||||||
|
|
||||||
if (!multipleFrames)
|
if (!multipleFrames)
|
||||||
|
|
@ -353,12 +401,13 @@ internal sealed class Atrac9DecodeState
|
||||||
private static void WriteInterleaved(
|
private static void WriteInterleaved(
|
||||||
short[][] source,
|
short[][] source,
|
||||||
Span<byte> destination,
|
Span<byte> destination,
|
||||||
|
int firstSample,
|
||||||
int samples,
|
int samples,
|
||||||
int channels,
|
int channels,
|
||||||
Atrac9PcmEncoding encoding)
|
Atrac9PcmEncoding encoding)
|
||||||
{
|
{
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
for (var sample = 0; sample < samples; sample++)
|
for (var sample = firstSample; sample < firstSample + samples; sample++)
|
||||||
{
|
{
|
||||||
for (var channel = 0; channel < channels; channel++)
|
for (var channel = 0; channel < channels; channel++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,14 @@ public static class AudioOut2Exports
|
||||||
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
||||||
private const int AttributeEntrySize = 0x18;
|
private const int AttributeEntrySize = 0x18;
|
||||||
private const uint PortAttributeIdPcm = 0;
|
private const uint PortAttributeIdPcm = 0;
|
||||||
|
private const uint PortAttributeIdGain = 1;
|
||||||
|
private const uint PortAttributeIdPassthrough = 5;
|
||||||
|
private const uint PortAttributeIdAmbisonics = 8;
|
||||||
|
internal const uint AmbisonicsNone = uint.MaxValue;
|
||||||
|
internal const uint AmbisonicsAcnBase = 64;
|
||||||
|
internal const uint PassthroughLeft = 1;
|
||||||
|
internal const uint PassthroughRight = 2;
|
||||||
|
private const float HalfPower = 0.70710677f;
|
||||||
private const ushort PortStateOutputConnectedPrimary = 0x01;
|
private const ushort PortStateOutputConnectedPrimary = 0x01;
|
||||||
|
|
||||||
// A host device that already holds this much audio is pacing the context; anything beyond the
|
// A host device that already holds this much audio is pacing the context; anything beyond the
|
||||||
|
|
@ -131,6 +139,9 @@ public static class AudioOut2Exports
|
||||||
return ahead <= 0 ? 0 : (uint)Math.Min((ahead + GrainTicks - 1) / GrainTicks, QueueDepth);
|
return ahead <= 0 ? 0 : (uint)Math.Min((ahead + GrainTicks - 1) / GrainTicks, QueueDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double DeviceSlotFreeMilliseconds =>
|
||||||
|
DeviceSlotFreeThreshold(GrainSamples, Frequency, QueueDepth);
|
||||||
|
|
||||||
// Grains the device holds beyond the cushion, at most QueueDepth.
|
// Grains the device holds beyond the cushion, at most QueueDepth.
|
||||||
public uint DeviceQueuedGrains(int queuedMilliseconds)
|
public uint DeviceQueuedGrains(int queuedMilliseconds)
|
||||||
{
|
{
|
||||||
|
|
@ -197,6 +208,9 @@ public static class AudioOut2Exports
|
||||||
public uint SamplingFrequency { get; }
|
public uint SamplingFrequency { get; }
|
||||||
public uint GrainSamples { get; }
|
public uint GrainSamples { get; }
|
||||||
public ulong PcmAddress;
|
public ulong PcmAddress;
|
||||||
|
public float Gain = 1f;
|
||||||
|
public uint Ambisonics = AmbisonicsNone;
|
||||||
|
public uint Passthrough;
|
||||||
|
|
||||||
public int PcmPending;
|
public int PcmPending;
|
||||||
|
|
||||||
|
|
@ -419,6 +433,14 @@ public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
context.DevicePaced = true;
|
context.DevicePaced = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (submitted || advancedGrain == 1)
|
||||||
|
{
|
||||||
|
if (blocking != 0)
|
||||||
|
{
|
||||||
|
WaitForDeviceSlot(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Push is the blocking point of a grain: it waits for the grain an Advance claimed, or
|
// Push is the blocking point of a grain: it waits for the grain an Advance claimed, or
|
||||||
|
|
@ -545,6 +567,7 @@ public static class AudioOut2Exports
|
||||||
|
|
||||||
Span<byte> entry = stackalloc byte[AttributeEntrySize];
|
Span<byte> entry = stackalloc byte[AttributeEntrySize];
|
||||||
Span<byte> pcm = stackalloc byte[8];
|
Span<byte> pcm = stackalloc byte[8];
|
||||||
|
Span<byte> word = stackalloc byte[sizeof(uint)];
|
||||||
for (uint i = 0; i < attributeCount; i++)
|
for (uint i = 0; i < attributeCount; i++)
|
||||||
{
|
{
|
||||||
if (!ctx.Memory.TryRead(attributesAddress + (i * AttributeEntrySize), entry))
|
if (!ctx.Memory.TryRead(attributesAddress + (i * AttributeEntrySize), entry))
|
||||||
|
|
@ -555,7 +578,41 @@ public static class AudioOut2Exports
|
||||||
var attributeId = BinaryPrimitives.ReadUInt32LittleEndian(entry);
|
var attributeId = BinaryPrimitives.ReadUInt32LittleEndian(entry);
|
||||||
var valueAddress = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x08..]);
|
var valueAddress = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x08..]);
|
||||||
var valueSize = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x10..]);
|
var valueSize = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x10..]);
|
||||||
if (attributeId != PortAttributeIdPcm || valueAddress == 0 || valueSize < 8)
|
if (valueAddress == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeId is PortAttributeIdGain or PortAttributeIdPassthrough or PortAttributeIdAmbisonics)
|
||||||
|
{
|
||||||
|
if (valueSize < sizeof(uint) || !ctx.Memory.TryRead(valueAddress, word))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = BinaryPrimitives.ReadUInt32LittleEndian(word);
|
||||||
|
switch (attributeId)
|
||||||
|
{
|
||||||
|
case PortAttributeIdGain:
|
||||||
|
var gain = BitConverter.UInt32BitsToSingle(value);
|
||||||
|
if (float.IsFinite(gain))
|
||||||
|
{
|
||||||
|
port.Gain = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case PortAttributeIdPassthrough:
|
||||||
|
port.Passthrough = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
port.Ambisonics = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeId != PortAttributeIdPcm || valueSize < 8)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1050,6 +1107,28 @@ public static class AudioOut2Exports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static double DeviceSlotFreeThreshold(uint grainSamples, uint frequency, uint queueDepth) =>
|
||||||
|
DeviceCushionMilliseconds + (grainSamples * 1000.0 / frequency * Math.Max((long)queueDepth - 1, 0));
|
||||||
|
|
||||||
|
private static void WaitForDeviceSlot(ContextState context)
|
||||||
|
{
|
||||||
|
var backend = ResolveContextBackend(context, out _);
|
||||||
|
if (backend is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var limit = context.DeviceSlotFreeMilliseconds;
|
||||||
|
var deadline = Stopwatch.GetTimestamp() + (Stopwatch.Frequency / 4);
|
||||||
|
int queued;
|
||||||
|
while ((queued = backend.QueuedMilliseconds) > limit &&
|
||||||
|
Stopwatch.GetTimestamp() < deadline &&
|
||||||
|
!HostSessionControl.IsShutdownRequested)
|
||||||
|
{
|
||||||
|
Thread.Sleep((int)Math.Clamp(Math.Ceiling(queued - limit), 1, 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static uint QueuedGrains(ContextState context)
|
private static uint QueuedGrains(ContextState context)
|
||||||
{
|
{
|
||||||
if (context.DevicePaced)
|
if (context.DevicePaced)
|
||||||
|
|
@ -1087,7 +1166,9 @@ public static class AudioOut2Exports
|
||||||
if (port.ContextHandle != context.Handle ||
|
if (port.ContextHandle != context.Handle ||
|
||||||
port.PcmAddress == 0 ||
|
port.PcmAddress == 0 ||
|
||||||
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
|
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
|
||||||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
|
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat) ||
|
||||||
|
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
|
||||||
|
out var leftGain, out var rightGain))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1111,7 +1192,8 @@ public static class AudioOut2Exports
|
||||||
ch,
|
ch,
|
||||||
bps,
|
bps,
|
||||||
isFloat,
|
isFloat,
|
||||||
additive: mixedPorts > 0);
|
leftGain,
|
||||||
|
rightGain);
|
||||||
mixedPorts++;
|
mixedPorts++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1162,20 +1244,69 @@ public static class AudioOut2Exports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsMainOrBgmPort(ushort portType)
|
internal static bool RoutesToSpeakers(ushort portType) => (portType & 0xFF) is 0 or 1 or 2;
|
||||||
|
|
||||||
|
internal static (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) =>
|
||||||
|
Ports.TryGetValue(portHandle, out var port) ? (port.Gain, port.Ambisonics, port.Passthrough) : null;
|
||||||
|
|
||||||
|
internal static bool TryGetStereoGains(
|
||||||
|
ushort portType,
|
||||||
|
int channels,
|
||||||
|
float gain,
|
||||||
|
uint ambisonics,
|
||||||
|
uint passthrough,
|
||||||
|
out float left,
|
||||||
|
out float right)
|
||||||
{
|
{
|
||||||
var kind = portType & 0xFF;
|
left = 0f;
|
||||||
return kind is 0 or 1;
|
right = 0f;
|
||||||
|
if (!RoutesToSpeakers(portType) || gain == 0f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsObjectPort(portType) || channels != 1)
|
||||||
|
{
|
||||||
|
left = gain;
|
||||||
|
right = gain;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ambisonics == AmbisonicsNone)
|
||||||
|
{
|
||||||
|
left = passthrough == PassthroughRight ? 0f : passthrough == PassthroughLeft ? gain : gain * HalfPower;
|
||||||
|
right = passthrough == PassthroughLeft ? 0f : passthrough == PassthroughRight ? gain : gain * HalfPower;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var acn = ambisonics >= AmbisonicsAcnBase;
|
||||||
|
var component = acn ? ambisonics - AmbisonicsAcnBase : ambisonics;
|
||||||
|
if (component == 0)
|
||||||
|
{
|
||||||
|
left = acn ? gain * HalfPower : gain;
|
||||||
|
right = left;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component == (acn ? 1u : 2u))
|
||||||
|
{
|
||||||
|
left = gain * HalfPower;
|
||||||
|
right = -left;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MixPortIntoStereo(
|
internal static void MixPortIntoStereo(
|
||||||
ReadOnlySpan<byte> source,
|
ReadOnlySpan<byte> source,
|
||||||
Span<float> mix,
|
Span<float> mix,
|
||||||
int frames,
|
int frames,
|
||||||
int channels,
|
int channels,
|
||||||
int bytesPerSample,
|
int bytesPerSample,
|
||||||
bool isFloat,
|
bool isFloat,
|
||||||
bool additive)
|
float leftGain,
|
||||||
|
float rightGain)
|
||||||
{
|
{
|
||||||
var frameSize = channels * bytesPerSample;
|
var frameSize = channels * bytesPerSample;
|
||||||
for (var frame = 0; frame < frames; frame++)
|
for (var frame = 0; frame < frames; frame++)
|
||||||
|
|
@ -1204,16 +1335,8 @@ public static class AudioOut2Exports
|
||||||
: ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
|
: ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additive)
|
mix[frame * 2] += left * leftGain;
|
||||||
{
|
mix[(frame * 2) + 1] += right * rightGain;
|
||||||
mix[frame * 2] += left;
|
|
||||||
mix[(frame * 2) + 1] += right;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mix[frame * 2] = left;
|
|
||||||
mix[(frame * 2) + 1] = right;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,19 @@ namespace SharpEmu.Libs.Diagnostics;
|
||||||
|
|
||||||
internal static class DccWriterTrace
|
internal static class DccWriterTrace
|
||||||
{
|
{
|
||||||
private const int MaxReports = 64;
|
private const int MaxReports = 256;
|
||||||
private const int MaxFrames = 12;
|
private const int MaxFrames = 12;
|
||||||
|
|
||||||
private static readonly bool DccEnabled =
|
private static readonly bool DccEnabled =
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_DCC_WRITERS") == "1";
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_DCC_WRITERS") == "1";
|
||||||
|
|
||||||
private static readonly ulong TargetAddress = ParseAddress(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_WRITER_ADDRESS"));
|
private static readonly ulong[] TargetAddresses = (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_WRITER_ADDRESS") ?? string.Empty)
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(ParseAddress)
|
||||||
|
.Where(address => address != 0)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
public static readonly bool Enabled = DccEnabled || TargetAddress != 0;
|
public static readonly bool Enabled = DccEnabled || TargetAddresses.Length != 0;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
public static ulong CurrentProgram;
|
public static ulong CurrentProgram;
|
||||||
|
|
@ -43,7 +47,7 @@ internal static class DccWriterTrace
|
||||||
|
|
||||||
public static void Record(IGuestImageCache images, ulong address, ulong size)
|
public static void Record(IGuestImageCache images, ulong address, ulong size)
|
||||||
{
|
{
|
||||||
var targeted = TargetAddress != 0 && TargetAddress >= address && TargetAddress - address < size;
|
var targeted = TargetAddresses.Any(target => target >= address && target - address < size);
|
||||||
if (_reported.Count >= MaxReports || (!targeted && !(DccEnabled && images.OverlapsDccMetadata(address, size))) ||
|
if (_reported.Count >= MaxReports || (!targeted && !(DccEnabled && images.OverlapsDccMetadata(address, size))) ||
|
||||||
!_reported.Add((address, size, CurrentProgram)))
|
!_reported.Add((address, size, CurrentProgram)))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,30 @@ internal static class GpuReadTrace
|
||||||
public static string? CurrentStage;
|
public static string? CurrentStage;
|
||||||
|
|
||||||
private static readonly HashSet<(ulong Shader, ulong Page, bool Table)> _reported = new();
|
private static readonly HashSet<(ulong Shader, ulong Page, bool Table)> _reported = new();
|
||||||
|
private static readonly Dictionary<ulong, (long Reads, long Changes, uint Last, uint First, ulong Shader)> _values = new();
|
||||||
|
private static long _nextValueReport = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
public static void RecordValue(ulong address, uint word)
|
||||||
|
{
|
||||||
|
lock (_values)
|
||||||
|
{
|
||||||
|
_values[address] = _values.TryGetValue(address, out var entry)
|
||||||
|
? (entry.Reads + 1, entry.Changes + (entry.Last != word ? 1 : 0), word, entry.First, entry.Shader)
|
||||||
|
: (1, 0, word, word, CurrentShader);
|
||||||
|
var now = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
if (now < _nextValueReport)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextValueReport = now + System.Diagnostics.Stopwatch.Frequency * 5;
|
||||||
|
foreach (var (key, value) in _values.OrderByDescending(pair => pair.Value.Reads).Take(16))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[GPU][SYNC_READ_VALUE] address=0x{key:X16} reads={value.Reads} changes={value.Changes} first=0x{value.First:X8} last=0x{value.Last:X8} shader=0x{value.Shader:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void Record(ulong address, bool table)
|
public static void Record(ulong address, bool table)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,72 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public const ulong MaxHostCopyWords = 64 * 1024;
|
||||||
|
|
||||||
|
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
|
||||||
|
{
|
||||||
|
if (destination == 0 || source == 0 || sourceWords == 0 || words == 0 || ((destination | source) & 3) != 0 ||
|
||||||
|
words > MaxHostCopyWords || sourceWords > MaxHostCopyWords)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = words * sizeof(uint);
|
||||||
|
var sourceSize = Math.Min(sourceWords, words) * sizeof(uint);
|
||||||
|
if (size > ulong.MaxValue - destination || sourceSize > ulong.MaxValue - source ||
|
||||||
|
(source < destination + size && destination < source + sourceSize))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var images = RequireImageCache();
|
||||||
|
var sourceRegion = images.QueryRegion(source, sourceSize);
|
||||||
|
var destinationRegion = images.QueryRegion(destination, size);
|
||||||
|
if (HasGpuDirtyBytes(source, sourceSize) || HasGpuDirtyBytes(destination, size) ||
|
||||||
|
sourceRegion.GpuImageBytes || destinationRegion.GpuImageBytes)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = System.Buffers.ArrayPool<byte>.Shared.Rent((int)sourceSize);
|
||||||
|
var chunk = System.Buffers.ArrayPool<byte>.Shared.Rent((int)Math.Min(size, 64UL * 1024));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_backing.TryReadBacking(source, pattern.AsSpan(0, (int)sourceSize)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationRegion.ImageBytes)
|
||||||
|
{
|
||||||
|
images.InvalidateMemory(destination, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkLength = (ulong)(chunk.Length - chunk.Length % sizeof(uint));
|
||||||
|
for (ulong offset = 0; offset < size;)
|
||||||
|
{
|
||||||
|
var length = (int)Math.Min(size - offset, chunkLength);
|
||||||
|
for (var filled = 0; filled < length;)
|
||||||
|
{
|
||||||
|
var patternOffset = (int)((offset + (ulong)filled) % sourceSize);
|
||||||
|
var copied = Math.Min(length - filled, (int)sourceSize - patternOffset);
|
||||||
|
pattern.AsSpan(patternOffset, copied).CopyTo(chunk.AsSpan(filled, copied));
|
||||||
|
filled += copied;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteHostMemory(destination + offset, chunk.AsSpan(0, length));
|
||||||
|
offset += (ulong)length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
System.Buffers.ArrayPool<byte>.Shared.Return(pattern);
|
||||||
|
System.Buffers.ArrayPool<byte>.Shared.Return(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void CopyBuffer(ulong dstVaddr, ulong srcVaddr, ulong size, bool dstGds, bool srcGds)
|
public void CopyBuffer(ulong dstVaddr, ulong srcVaddr, ulong size, bool dstGds, bool srcGds)
|
||||||
{
|
{
|
||||||
var dstMemory = !dstGds;
|
var dstMemory = !dstGds;
|
||||||
|
|
@ -557,7 +623,21 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
||||||
private ulong _bdaTouchTick = ulong.MaxValue;
|
private ulong _bdaTouchTick = ulong.MaxValue;
|
||||||
private ulong _bdaTouchMapping;
|
private ulong _bdaTouchMapping;
|
||||||
|
|
||||||
|
public static ulong MappingKey(IReadOnlyCollection<GuestSpan> spans)
|
||||||
|
{
|
||||||
|
var mapping = (ulong)spans.Count;
|
||||||
|
foreach (var span in spans)
|
||||||
|
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
public void PrepareBda(IEnumerable<GuestSpan> mapped)
|
public void PrepareBda(IEnumerable<GuestSpan> mapped)
|
||||||
|
{
|
||||||
|
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
|
||||||
|
PrepareBda(spans, MappingKey(spans));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareBda(IReadOnlyCollection<GuestSpan> mapped, ulong mapping)
|
||||||
{
|
{
|
||||||
var traceAddress = GuestGpuMemoryHook.TraceAddress;
|
var traceAddress = GuestGpuMemoryHook.TraceAddress;
|
||||||
var traceCovered = false;
|
var traceCovered = false;
|
||||||
|
|
@ -572,10 +652,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
|
var spans = mapped;
|
||||||
var mapping = (ulong)spans.Count;
|
|
||||||
foreach (var span in spans)
|
|
||||||
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
|
|
||||||
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
|
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
|
||||||
{
|
{
|
||||||
foreach (var span in spans)
|
foreach (var span in spans)
|
||||||
|
|
|
||||||
|
|
@ -1196,4 +1196,10 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
||||||
_ = (address, size, fillValue);
|
_ = (address, size, fillValue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IRenderHost.TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
|
||||||
|
{
|
||||||
|
_ = (destination, source, sourceWords, words);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,104 @@ public sealed record BoundedFill(
|
||||||
FillWord[]? Pattern = null,
|
FillWord[]? Pattern = null,
|
||||||
FillWord? PatternLength = null);
|
FillWord? PatternLength = null);
|
||||||
|
|
||||||
|
public sealed record BoundedCopy(
|
||||||
|
uint GroupScalarRegister,
|
||||||
|
FillWord Count,
|
||||||
|
FillWord Modulus,
|
||||||
|
FillWord[] Source,
|
||||||
|
FillWord[] Destination);
|
||||||
|
|
||||||
public static class BoundedFillDetector
|
public static class BoundedFillDetector
|
||||||
{
|
{
|
||||||
|
private static readonly string[] ModuloCopyOpcodes =
|
||||||
|
[
|
||||||
|
"VLshlAddU32", "SBufferLoadDword", "VCmpxGtU32", "SCbranchExecz", "SBufferLoadDword",
|
||||||
|
"VCvtF32U32", "SCmpLgU32", "SCselectB64", "VRcpIflagF32", "VMulF32", "VCvtU32F32", "VMadU64U32", "VCmpNeU32", "VSubI32",
|
||||||
|
"VCndmaskB32", "VMulHiU32", "VSubI32", "VAddI32", "VCndmaskB32", "VMulHiU32", "VMulLoU32", "VSubI32", "VCmpGeU32",
|
||||||
|
"VCmpLeU32", "SAndB64", "VAddcU32", "VAddCoCiU32", "VCndmaskB32", "VMulLoU32", "VSubI32",
|
||||||
|
"BufferLoadFormatX", "BufferStoreFormatX", "SEndpgm",
|
||||||
|
];
|
||||||
|
|
||||||
|
public static BoundedCopy? DetectCopy(Gen5ShaderProgram program)
|
||||||
|
{
|
||||||
|
var instructions = program.Instructions
|
||||||
|
.Where(instruction => instruction.Opcode is not ("SWaitcnt" or "STtraceData" or "SInstPrefetch") &&
|
||||||
|
!(instruction.Opcode == "SMovB32" && instruction.Destinations is [{ Kind: Gen5OperandKind.ScalarRegister, Value: MarkerRegister }]))
|
||||||
|
.ToArray();
|
||||||
|
if (!instructions.Select(instruction => instruction.Opcode).SequenceEqual(ModuloCopyOpcodes))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var countLoad = instructions[1];
|
||||||
|
var modulusLoad = instructions[4];
|
||||||
|
if (instructions[0].Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } index] ||
|
||||||
|
instructions[0].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } group, { Kind: Gen5OperandKind.EncodedConstant, Value: Shift64 },
|
||||||
|
{ Kind: Gen5OperandKind.VectorRegister, Value: ThreadIndexRegister }] ||
|
||||||
|
countLoad.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } count] ||
|
||||||
|
modulusLoad.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } modulus] ||
|
||||||
|
countLoad.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } constants, _] ||
|
||||||
|
modulusLoad.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } modulusConstants, _] ||
|
||||||
|
modulusConstants.Value != constants.Value ||
|
||||||
|
instructions[2].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } limit, { Kind: Gen5OperandKind.VectorRegister } compared] ||
|
||||||
|
limit.Value != count.Value || compared.Value != index.Value ||
|
||||||
|
instructions[5].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } converted] || converted.Value != modulus.Value ||
|
||||||
|
instructions[6].Sources is not [{ Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }, { Kind: Gen5OperandKind.ScalarRegister } tested] ||
|
||||||
|
tested.Value != modulus.Value)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var at in new[] { 11, 20, 23, 28 })
|
||||||
|
{
|
||||||
|
if (instructions[at].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } multiplier, ..] || multiplier.Value != modulus.Value)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var load = instructions[30];
|
||||||
|
var store = instructions[31];
|
||||||
|
if (instructions[29].Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } remainder] ||
|
||||||
|
instructions[29].Sources is not [{ Kind: Gen5OperandKind.VectorRegister } dividend, { Kind: Gen5OperandKind.VectorRegister }] ||
|
||||||
|
dividend.Value != index.Value ||
|
||||||
|
load.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } loadControl ||
|
||||||
|
store.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } storeControl ||
|
||||||
|
load.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }] ||
|
||||||
|
store.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }] ||
|
||||||
|
loadControl.VectorAddress != remainder.Value || storeControl.VectorAddress != index.Value ||
|
||||||
|
storeControl.VectorData != loadControl.VectorData)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var branch = instructions[3];
|
||||||
|
if (branch.Pc + sizeof(uint) + (uint)((short)(branch.Words[0] & 0xFFFF) * sizeof(uint)) != instructions[^1].Pc)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourceRegisters = Enumerable.Range(0, 4)
|
||||||
|
.SelectMany(word => new[] { loadControl.ScalarResource + (uint)word, storeControl.ScalarResource + (uint)word });
|
||||||
|
var constantRegisters = Enumerable.Range(0, 4).Select(word => constants.Value + (uint)word);
|
||||||
|
if (resourceRegisters.Any(WrittenScalars(instructions, instructions.Length).Contains) ||
|
||||||
|
constantRegisters.Any(WrittenScalars(instructions, 4).Contains) ||
|
||||||
|
WrittenScalars(instructions[5..29], 24).Contains(modulus.Value) ||
|
||||||
|
countLoad.Control is not Gen5ScalarMemoryControl { DynamicOffsetRegister: null, ImmediateOffsetBytes: >= 0 } countControl ||
|
||||||
|
modulusLoad.Control is not Gen5ScalarMemoryControl { DynamicOffsetRegister: null, ImmediateOffsetBytes: >= 0 } modulusControl)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FillWord User(uint register) => new(FillWordSource.UserData, register, 0, 0);
|
||||||
|
return new BoundedCopy(
|
||||||
|
group.Value,
|
||||||
|
new FillWord(FillWordSource.BufferResource, constants.Value, 0, countControl.ImmediateOffsetBytes),
|
||||||
|
new FillWord(FillWordSource.BufferResource, constants.Value, 0, modulusControl.ImmediateOffsetBytes),
|
||||||
|
[.. Enumerable.Range(0, 4).Select(word => User(loadControl.ScalarResource + (uint)word))],
|
||||||
|
[.. Enumerable.Range(0, 4).Select(word => User(storeControl.ScalarResource + (uint)word))]);
|
||||||
|
}
|
||||||
|
|
||||||
private const uint Shift64 = 134;
|
private const uint Shift64 = 134;
|
||||||
private const uint ZeroConstant = 128;
|
private const uint ZeroConstant = 128;
|
||||||
private const uint NullScalar = 125;
|
private const uint NullScalar = 125;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
@ -141,6 +142,7 @@ internal sealed class ShaderPrewarmList : IDisposable
|
||||||
{
|
{
|
||||||
public const string FileName = "shader-prewarm.bin";
|
public const string FileName = "shader-prewarm.bin";
|
||||||
public const string StampFileName = "shader-prewarm.stamp";
|
public const string StampFileName = "shader-prewarm.stamp";
|
||||||
|
public const string ProgressFileName = "shader-prewarm.progress";
|
||||||
|
|
||||||
private const uint Magic = 0x57504553;
|
private const uint Magic = 0x57504553;
|
||||||
private const uint FormatVersion = 1;
|
private const uint FormatVersion = 1;
|
||||||
|
|
@ -151,16 +153,19 @@ internal sealed class ShaderPrewarmList : IDisposable
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private readonly FileStream _file;
|
private readonly FileStream _file;
|
||||||
private readonly string _stampPath;
|
private readonly string _stampPath;
|
||||||
|
private readonly string _progressPath;
|
||||||
|
private readonly object _progressGate = new();
|
||||||
private readonly HashSet<(ulong Hash, uint CodeSize, ulong Address)> _codes = [];
|
private readonly HashSet<(ulong Hash, uint CodeSize, ulong Address)> _codes = [];
|
||||||
private readonly HashSet<ulong> _computes = [];
|
private readonly HashSet<ulong> _computes = [];
|
||||||
private readonly Dictionary<(ulong Hash, uint CodeSize, ulong Address), ShaderCodeCapture> _loadedCodes = new();
|
private readonly Dictionary<(ulong Hash, uint CodeSize, ulong Address), ShaderCodeCapture> _loadedCodes = new();
|
||||||
private readonly List<ComputePrewarmRecord> _loadedComputes = [];
|
private readonly List<ComputePrewarmRecord> _loadedComputes = [];
|
||||||
private bool _failed;
|
private bool _failed;
|
||||||
|
|
||||||
private ShaderPrewarmList(FileStream file, string stampPath)
|
private ShaderPrewarmList(FileStream file, string stampPath, string progressPath)
|
||||||
{
|
{
|
||||||
_file = file;
|
_file = file;
|
||||||
_stampPath = stampPath;
|
_stampPath = stampPath;
|
||||||
|
_progressPath = progressPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int LoadedComputeCount => _loadedComputes.Count;
|
public int LoadedComputeCount => _loadedComputes.Count;
|
||||||
|
|
@ -172,7 +177,7 @@ internal sealed class ShaderPrewarmList : IDisposable
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
var path = Path.Combine(directory, FileName);
|
var path = Path.Combine(directory, FileName);
|
||||||
var file = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
var file = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||||
var list = new ShaderPrewarmList(file, Path.Combine(directory, StampFileName));
|
var list = new ShaderPrewarmList(file, Path.Combine(directory, StampFileName), Path.Combine(directory, ProgressFileName));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
list.Load();
|
list.Load();
|
||||||
|
|
@ -230,6 +235,93 @@ internal sealed class ShaderPrewarmList : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ulong Identity(ComputePrewarmRecord record) => XxHash3.HashToUInt64(SerializeCompute(record));
|
||||||
|
|
||||||
|
public HashSet<ulong> ReadProgress(string stamp)
|
||||||
|
{
|
||||||
|
var completed = new HashSet<ulong>();
|
||||||
|
lock (_progressGate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_progressPath))
|
||||||
|
{
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = File.ReadAllLines(_progressPath);
|
||||||
|
if (lines.Length == 0 || lines[0] != ProgressKey(stamp))
|
||||||
|
{
|
||||||
|
File.Delete(_progressPath);
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 1; index < lines.Length; index++)
|
||||||
|
{
|
||||||
|
if (ulong.TryParse(lines[index], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var identity))
|
||||||
|
{
|
||||||
|
completed.Add(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
completed.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendProgress(string stamp, IReadOnlyCollection<ulong> identities)
|
||||||
|
{
|
||||||
|
if (identities.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_progressGate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lines = new List<string>(identities.Count + 1);
|
||||||
|
if (!File.Exists(_progressPath))
|
||||||
|
{
|
||||||
|
lines.Add(ProgressKey(stamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var identity in identities)
|
||||||
|
{
|
||||||
|
lines.Add(identity.ToString("X16", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
File.AppendAllLines(_progressPath, lines);
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm progress write failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearProgress()
|
||||||
|
{
|
||||||
|
lock (_progressGate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(_progressPath);
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm progress clear failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProgressKey(string stamp) =>
|
||||||
|
XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(stamp)).ToString("X16", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
public void RecordCompute(ShaderCodeCapture code, ComputePrewarmRecord record)
|
public void RecordCompute(ShaderCodeCapture code, ComputePrewarmRecord record)
|
||||||
{
|
{
|
||||||
var payload = SerializeCompute(record);
|
var payload = SerializeCompute(record);
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ internal sealed class ProgramSourceEntry
|
||||||
public required bool HasBitwiseExclusiveOr { get; init; }
|
public required bool HasBitwiseExclusiveOr { get; init; }
|
||||||
public ConstantFill? ConstantFill { get; init; }
|
public ConstantFill? ConstantFill { get; init; }
|
||||||
public BoundedFill? BoundedFill { get; init; }
|
public BoundedFill? BoundedFill { get; init; }
|
||||||
|
public BoundedCopy? BoundedCopy { get; init; }
|
||||||
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
|
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
|
||||||
public ShaderVertexInput[] VertexInputs { get; init; } = [];
|
public ShaderVertexInput[] VertexInputs { get; init; } = [];
|
||||||
public List<ProgramPermutation> Permutations { get; } = new(8);
|
public List<ProgramPermutation> Permutations { get; } = new(8);
|
||||||
|
|
@ -383,6 +384,7 @@ internal sealed class ShaderProgramCache
|
||||||
HasBitwiseExclusiveOr = exclusiveOr,
|
HasBitwiseExclusiveOr = exclusiveOr,
|
||||||
ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
|
ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
|
||||||
BoundedFill = source.Stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null,
|
BoundedFill = source.Stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null,
|
||||||
|
BoundedCopy = source.Stage == ShaderStage.Compute ? BoundedFillDetector.DetectCopy(program) : null,
|
||||||
EmbeddedFetch = fetch,
|
EmbeddedFetch = fetch,
|
||||||
VertexInputs = vertexInputs,
|
VertexInputs = vertexInputs,
|
||||||
};
|
};
|
||||||
|
|
@ -837,6 +839,7 @@ internal sealed class ShaderProgramCache
|
||||||
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
|
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
|
||||||
ConstantFill = entry.ConstantFill,
|
ConstantFill = entry.ConstantFill,
|
||||||
BoundedFill = entry.BoundedFill,
|
BoundedFill = entry.BoundedFill,
|
||||||
|
BoundedCopy = entry.BoundedCopy,
|
||||||
Buffers = buffers,
|
Buffers = buffers,
|
||||||
Images = images,
|
Images = images,
|
||||||
SamplerCount = info.Samplers.Count,
|
SamplerCount = info.Samplers.Count,
|
||||||
|
|
|
||||||
|
|
@ -188,5 +188,7 @@ public interface IRenderHost
|
||||||
|
|
||||||
bool TryFillDccMetadata(ulong address, ulong size, uint fillValue);
|
bool TryFillDccMetadata(ulong address, ulong size, uint fillValue);
|
||||||
|
|
||||||
|
bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words);
|
||||||
|
|
||||||
Exception Fatal(string message);
|
Exception Fatal(string message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,12 @@ public sealed partial class RenderExecutor
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (indirectArgumentsAddress == 0 && TryConsumeBoundedCopy(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||||
|
{
|
||||||
|
_host.ResetBindings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||||
{
|
{
|
||||||
_host.ResetBindings();
|
_host.ResetBindings();
|
||||||
|
|
@ -552,6 +558,71 @@ public sealed partial class RenderExecutor
|
||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryConsumeBoundedCopy(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
|
||||||
|
{
|
||||||
|
var program = input.Stage.Program!;
|
||||||
|
if (program.BoundedCopy is not { } copy || !BoundedCopyEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (program.UserDataBase != 0 || copy.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
|
||||||
|
input.ThreadIdCount < 1 || input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 ||
|
||||||
|
groupsX == 0 || groupsY != 1 || groupsZ != 1)
|
||||||
|
{
|
||||||
|
return RefuseBoundedFill(program, $"copy shape dispatch={groupsX}x{groupsY}x{groupsZ} local={input.ThreadsX}x{input.ThreadsY}x{input.ThreadsZ}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = input.Stage.Resources.UserData;
|
||||||
|
Span<uint> sourceWords = stackalloc uint[4];
|
||||||
|
Span<uint> destinationWords = stackalloc uint[4];
|
||||||
|
for (var word = 0; word < 4; word++)
|
||||||
|
{
|
||||||
|
if (!TryResolveFillWord(copy.Source[word], userData, out sourceWords[word]) ||
|
||||||
|
!TryResolveFillWord(copy.Destination[word], userData, out destinationWords[word]))
|
||||||
|
{
|
||||||
|
return RefuseBoundedFill(program, $"copy descriptor word {word} unreadable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryResolveFillWord(copy.Count, userData, out var count) || !TryResolveFillWord(copy.Modulus, userData, out var modulus))
|
||||||
|
{
|
||||||
|
return RefuseBoundedFill(program, "copy range unreadable");
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = BufferDescriptorWords.From(sourceWords);
|
||||||
|
var destination = BufferDescriptorWords.From(destinationWords);
|
||||||
|
if (!IsPlainWordBuffer(source) || !IsPlainWordBuffer(destination) || source.Format != destination.Format ||
|
||||||
|
modulus == 0 || modulus > source.RecordCount)
|
||||||
|
{
|
||||||
|
return RefuseBoundedFill(program,
|
||||||
|
$"copy src=0x{source.Address:X}/{source.Format}/{source.Stride}/{source.OutOfBounds} dst=0x{destination.Address:X}/{destination.Format}/{destination.Stride}/{destination.OutOfBounds} " +
|
||||||
|
$"modulus={modulus} records={source.RecordCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var threads = (dispatchInitiator & DispatchInitiatorUseThreadDimensions) != 0 ? groupsX : (ulong)groupsX * input.ThreadsX;
|
||||||
|
var words = Math.Min(Math.Min((ulong)count, threads), destination.RecordCount);
|
||||||
|
if (words == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var consumed = _host.TryCopyWordsOnHost(destination.Address, source.Address, modulus, words);
|
||||||
|
if (RenderTrace.Enabled)
|
||||||
|
{
|
||||||
|
RenderTrace.Write(
|
||||||
|
$"Bounded copy: shader=0x{program.Hash:X16} src=0x{source.Address:X16} dst=0x{destination.Address:X16} words={words} modulus={modulus} consumed={consumed}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed || RefuseBoundedFill(program, $"copy refused by the host dst=0x{destination.Address:X} words={words} modulus={modulus}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly bool BoundedCopyEnabled = Environment.GetEnvironmentVariable("SHARPEMU_HOST_BOUNDED_COPY") != "0";
|
||||||
|
|
||||||
|
private static bool IsPlainWordBuffer(BufferDescriptorWords descriptor) =>
|
||||||
|
descriptor.Stride == sizeof(uint) && !descriptor.SwizzleEnabled && !descriptor.AddThreadId && descriptor.OutOfBounds == 0 &&
|
||||||
|
descriptor.Type == 0 && (descriptor.Address & 3) == 0 && descriptor.Format is Format32UInt or Format32SInt or Format32Float;
|
||||||
|
|
||||||
private static bool RefuseBoundedFill(ShaderProgramInfo program, string reason)
|
private static bool RefuseBoundedFill(ShaderProgramInfo program, string reason)
|
||||||
{
|
{
|
||||||
Diagnostics.DccWriterTrace.Refuse(program.Hash, reason);
|
Diagnostics.DccWriterTrace.Refuse(program.Hash, reason);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ public class ShaderProgramInfo
|
||||||
public bool HasBitwiseExclusiveOr { get; init; }
|
public bool HasBitwiseExclusiveOr { get; init; }
|
||||||
public Pipelines.ConstantFill? ConstantFill { get; init; }
|
public Pipelines.ConstantFill? ConstantFill { get; init; }
|
||||||
public Pipelines.BoundedFill? BoundedFill { get; init; }
|
public Pipelines.BoundedFill? BoundedFill { get; init; }
|
||||||
|
public Pipelines.BoundedCopy? BoundedCopy { get; init; }
|
||||||
public BufferResourceInfo[] Buffers { get; init; } = [];
|
public BufferResourceInfo[] Buffers { get; init; } = [];
|
||||||
public ImageResourceInfo[] Images { get; init; } = [];
|
public ImageResourceInfo[] Images { get; init; } = [];
|
||||||
public int SamplerCount { get; init; }
|
public int SamplerCount { get; init; }
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureStarted(1280, 720);
|
EnsureStarted(1280, 720);
|
||||||
|
WaitForShaderPrewarm("first GPU submission");
|
||||||
// Queue publication precedes device initialization; only the render thread consumes it.
|
// Queue publication precedes device initialization; only the render thread consumes it.
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
|
|
@ -117,6 +118,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
_ = width;
|
_ = width;
|
||||||
_ = height;
|
_ = height;
|
||||||
_ = pitchInPixel;
|
_ = pitchInPixel;
|
||||||
|
WaitForShaderPrewarm("first flip");
|
||||||
if (!IsKnownDisplayBuffer(address) || !TryGetActivePresenter(out var presenter))
|
if (!IsKnownDisplayBuffer(address) || !TryGetActivePresenter(out var presenter))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -402,8 +402,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
}
|
}
|
||||||
using var profileScope = BufferUploadProfile.BeginSweep(vertexProgramHash, pixelProgramHash, computeProgramHash);
|
using var profileScope = BufferUploadProfile.BeginSweep(vertexProgramHash, pixelProgramHash, computeProgramHash);
|
||||||
var memory = GuestGpuMemoryHook.Current ?? throw SubmissionScheduler.Fatal("A device-address program needs the guest GPU memory registry.");
|
var memory = GuestGpuMemoryHook.Current ?? throw SubmissionScheduler.Fatal("A device-address program needs the guest GPU memory registry.");
|
||||||
var spans = new List<GuestSpan>();
|
var spans = DeviceAddressSpans(memory);
|
||||||
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
|
|
||||||
var traceAddress = GuestGpuMemoryHook.TraceAddress;
|
var traceAddress = GuestGpuMemoryHook.TraceAddress;
|
||||||
if (traceAddress != 0)
|
if (traceAddress != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -420,7 +419,29 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
if (traceAddress != 0 && !memory.Covers(traceAddress, 1))
|
if (traceAddress != 0 && !memory.Covers(traceAddress, 1))
|
||||||
GuestGpuMemoryHook.Trace(traceAddress, 1,
|
GuestGpuMemoryHook.Trace(traceAddress, 1,
|
||||||
$"device-address-mapping-check readable={_guestMemory.CanRead(traceAddress, 1)} backed={_guestBacking.IsBackedView(traceAddress)}");
|
$"device-address-mapping-check readable={_guestMemory.CanRead(traceAddress, 1)} backed={_guestBacking.IsBackedView(traceAddress)}");
|
||||||
_bufferCache.PrepareBda(spans);
|
_bufferCache.PrepareBda(spans, _bdaSpanMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<GuestSpan>? _bdaSpans;
|
||||||
|
private GuestGpuMemory? _bdaSpanMemory;
|
||||||
|
private long _bdaSpanVersion = -1;
|
||||||
|
private ulong _bdaSpanMapping;
|
||||||
|
|
||||||
|
private List<GuestSpan> DeviceAddressSpans(GuestGpuMemory memory)
|
||||||
|
{
|
||||||
|
var version = memory.SpanVersion;
|
||||||
|
if (_bdaSpans is { } cached && ReferenceEquals(_bdaSpanMemory, memory) && _bdaSpanVersion == version)
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spans = new List<GuestSpan>();
|
||||||
|
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
|
||||||
|
_bdaSpans = spans;
|
||||||
|
_bdaSpanMemory = memory;
|
||||||
|
_bdaSpanVersion = version;
|
||||||
|
_bdaSpanMapping = GuestBufferCache.MappingKey(spans);
|
||||||
|
return spans;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindResources(IPreparedBindings prepared)
|
public void BindResources(IPreparedBindings prepared)
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
{
|
{
|
||||||
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
|
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
|
||||||
word = 0;
|
word = 0;
|
||||||
|
var synchronized = false;
|
||||||
if (IsCleanReadPage(address, sizeof(uint)))
|
if (IsCleanReadPage(address, sizeof(uint)))
|
||||||
{
|
{
|
||||||
if (TryGetAliasPointer(address, sizeof(uint), out var alias))
|
if (TryGetAliasPointer(address, sizeof(uint), out var alias))
|
||||||
|
|
@ -132,6 +133,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -146,6 +149,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
if (Diagnostics.GpuReadTrace.Enabled && synchronized)
|
||||||
|
{
|
||||||
|
Diagnostics.GpuReadTrace.RecordValue(address, word);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,8 +157,73 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
_guestFlipVersionSequence = 0;
|
_guestFlipVersionSequence = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly bool _shaderPrewarmWaitDisabled =
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM_WAIT") == "0";
|
||||||
|
private static bool _shaderPrewarmDecided;
|
||||||
|
private static bool _shaderPrewarmRunning;
|
||||||
|
private static bool _shaderPrewarmReleased;
|
||||||
|
private static int _shaderPrewarmHoldReported;
|
||||||
|
private static int _shaderPrewarmProgress;
|
||||||
|
private static int _shaderPrewarmTotal;
|
||||||
|
|
||||||
|
private static void SetShaderPrewarmState(bool running)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_shaderPrewarmDecided = true;
|
||||||
|
_shaderPrewarmRunning = running;
|
||||||
|
Volatile.Write(ref _shaderPrewarmReleased, !running);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void WaitForShaderPrewarm(string holder)
|
||||||
|
{
|
||||||
|
if (_shaderPrewarmWaitDisabled || Volatile.Read(ref _shaderPrewarmReleased))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var started = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
var reported = started;
|
||||||
|
var reporter = false;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
while (_thread is not null && (!_shaderPrewarmDecided || _shaderPrewarmRunning) &&
|
||||||
|
!_closed && _presenterStartupFailure is null &&
|
||||||
|
!HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _presenterCloseRequested))
|
||||||
|
{
|
||||||
|
if (!reporter && _shaderPrewarmRunning &&
|
||||||
|
Interlocked.Exchange(ref _shaderPrewarmHoldReported, 1) == 0)
|
||||||
|
{
|
||||||
|
reporter = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Shader prewarm: holding the game at its {holder} until it finishes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Monitor.Wait(_gate, 1000);
|
||||||
|
if (reporter && System.Diagnostics.Stopwatch.GetElapsedTime(reported) >= TimeSpan.FromSeconds(5))
|
||||||
|
{
|
||||||
|
reported = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
var total = Volatile.Read(ref _shaderPrewarmTotal);
|
||||||
|
var done = Volatile.Read(ref _shaderPrewarmProgress);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Shader prewarm: {done}/{total} ({(total == 0 ? 0 : done * 100 / total)}%)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reporter)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(string.Create(
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
$"[LOADER][INFO] Shader prewarm: game released after {System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalSeconds:F1}s."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void HideSplashScreen()
|
public static void HideSplashScreen()
|
||||||
{
|
{
|
||||||
|
WaitForShaderPrewarm("splash screen");
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_splashHidden = true;
|
_splashHidden = true;
|
||||||
|
|
|
||||||
|
|
@ -1200,5 +1200,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
_bufferCache.FillDccMetadata(address, size, fillValue);
|
_bufferCache.FillDccMetadata(address, size, fillValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words) =>
|
||||||
|
_bufferCache.TryCopyWordsOnHost(destination, source, sourceWords, words);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
private sealed partial class Presenter
|
private sealed partial class Presenter
|
||||||
{
|
{
|
||||||
private const int MaxLoggedPrewarmFailures = 8;
|
private const int MaxLoggedPrewarmFailures = 8;
|
||||||
|
private const int PrewarmProgressBatch = 256;
|
||||||
|
|
||||||
private ShaderPrewarmList? _shaderPrewarmList;
|
private ShaderPrewarmList? _shaderPrewarmList;
|
||||||
private ConcurrentDictionary<string, byte>? _prewarmedShaderIdentities;
|
private ConcurrentDictionary<string, byte>? _prewarmedShaderIdentities;
|
||||||
|
|
@ -27,16 +28,25 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
private int _shaderPrewarmCompiled;
|
private int _shaderPrewarmCompiled;
|
||||||
private int _shaderPrewarmFailed;
|
private int _shaderPrewarmFailed;
|
||||||
private int _shaderPrewarmRuntimeHits;
|
private int _shaderPrewarmRuntimeHits;
|
||||||
|
private readonly ConcurrentQueue<ulong> _shaderPrewarmCompleted = new();
|
||||||
|
|
||||||
ShaderPrewarmList? IShaderPipelineHost.ShaderPrewarm => Volatile.Read(ref _shaderPrewarmList);
|
ShaderPrewarmList? IShaderPipelineHost.ShaderPrewarm => Volatile.Read(ref _shaderPrewarmList);
|
||||||
|
|
||||||
private void StartShaderPrewarm()
|
private void StartShaderPrewarm()
|
||||||
|
{
|
||||||
|
if (!StartShaderPrewarmCore())
|
||||||
|
{
|
||||||
|
SetShaderPrewarmState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool StartShaderPrewarmCore()
|
||||||
{
|
{
|
||||||
if (_shaderPrewarmList is not null ||
|
if (_shaderPrewarmList is not null ||
|
||||||
_pipelineCacheShardDirectory is not null ||
|
_pipelineCacheShardDirectory is not null ||
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM") == "0")
|
Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM") == "0")
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath(
|
var directory = Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath(
|
||||||
|
|
@ -44,7 +54,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")));
|
Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")));
|
||||||
if (string.IsNullOrEmpty(directory) || ShaderPrewarmList.Open(directory) is not { } list)
|
if (string.IsNullOrEmpty(directory) || ShaderPrewarmList.Open(directory) is not { } list)
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_prewarmedShaderIdentities = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
|
_prewarmedShaderIdentities = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
|
||||||
|
|
@ -57,7 +67,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
{
|
{
|
||||||
list.WriteStamp(stamp);
|
list.WriteStamp(stamp);
|
||||||
_shaderPrewarmFinished = true;
|
_shaderPrewarmFinished = true;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.IsStampCurrent(stamp))
|
if (list.IsStampCurrent(stamp))
|
||||||
|
|
@ -65,18 +75,31 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
_shaderPrewarmFinished = true;
|
_shaderPrewarmFinished = true;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Shader prewarm skipped: compute={list.LoadedComputeCount} reason=same-build-and-driver");
|
$"[LOADER][INFO] Shader prewarm skipped: compute={list.LoadedComputeCount} reason=same-build-and-driver");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var work = new ConcurrentQueue<(ComputePrewarmRecord Record, ShaderCodeCapture Code)>(list.LoadedComputes());
|
var completed = list.ReadProgress(stamp);
|
||||||
|
var pending = list.LoadedComputes()
|
||||||
|
.Where(item => !completed.Contains(ShaderPrewarmList.Identity(item.Record)))
|
||||||
|
.ToList();
|
||||||
|
var started = Stopwatch.GetTimestamp();
|
||||||
|
if (pending.Count == 0)
|
||||||
|
{
|
||||||
|
FinishShaderPrewarm(list, stamp, started);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var work = new ConcurrentQueue<(ComputePrewarmRecord Record, ShaderCodeCapture Code)>(pending);
|
||||||
var workerCount = Math.Clamp(Environment.ProcessorCount / 4, 1, 4);
|
var workerCount = Math.Clamp(Environment.ProcessorCount / 4, 1, 4);
|
||||||
var remaining = workerCount;
|
var remaining = workerCount;
|
||||||
var started = Stopwatch.GetTimestamp();
|
|
||||||
var host = (IShaderPipelineHost)this;
|
var host = (IShaderPipelineHost)this;
|
||||||
var sharedInt64Atomics = host.SharedInt64AtomicsEnabled;
|
var sharedInt64Atomics = host.SharedInt64AtomicsEnabled;
|
||||||
var execGuardElision = host.ExecGuardElisionEnabled;
|
var execGuardElision = host.ExecGuardElisionEnabled;
|
||||||
|
Volatile.Write(ref _shaderPrewarmTotal, pending.Count);
|
||||||
|
Volatile.Write(ref _shaderPrewarmProgress, 0);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Shader prewarm started: compute={work.Count} workers={workerCount}");
|
$"[LOADER][INFO] Shader prewarm started: compute={pending.Count} resumed={completed.Count} workers={workerCount}");
|
||||||
|
SetShaderPrewarmState(true);
|
||||||
_shaderPrewarmThreads = new Thread[workerCount];
|
_shaderPrewarmThreads = new Thread[workerCount];
|
||||||
for (var index = 0; index < workerCount; index++)
|
for (var index = 0; index < workerCount; index++)
|
||||||
{
|
{
|
||||||
|
|
@ -85,8 +108,14 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
while (!_shaderPrewarmStopping && work.TryDequeue(out var item))
|
while (!_shaderPrewarmStopping && work.TryDequeue(out var item))
|
||||||
{
|
{
|
||||||
PrewarmComputePipeline(item.Record, item.Code, compiler, sharedInt64Atomics, execGuardElision);
|
PrewarmComputePipeline(item.Record, item.Code, compiler, sharedInt64Atomics, execGuardElision);
|
||||||
|
_shaderPrewarmCompleted.Enqueue(ShaderPrewarmList.Identity(item.Record));
|
||||||
|
if (Interlocked.Increment(ref _shaderPrewarmProgress) % PrewarmProgressBatch == 0)
|
||||||
|
{
|
||||||
|
FlushShaderPrewarmProgress(list, stamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlushShaderPrewarmProgress(list, stamp);
|
||||||
if (Interlocked.Decrement(ref remaining) == 0)
|
if (Interlocked.Decrement(ref remaining) == 0)
|
||||||
{
|
{
|
||||||
FinishShaderPrewarm(list, stamp, started);
|
FinishShaderPrewarm(list, stamp, started);
|
||||||
|
|
@ -99,6 +128,19 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
};
|
};
|
||||||
_shaderPrewarmThreads[index].Start();
|
_shaderPrewarmThreads[index].Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlushShaderPrewarmProgress(ShaderPrewarmList list, string stamp)
|
||||||
|
{
|
||||||
|
var batch = new List<ulong>();
|
||||||
|
while (_shaderPrewarmCompleted.TryDequeue(out var identity))
|
||||||
|
{
|
||||||
|
batch.Add(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.AppendProgress(stamp, batch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ShaderPrewarmStampPart(Type type) =>
|
private static string ShaderPrewarmStampPart(Type type) =>
|
||||||
|
|
@ -109,8 +151,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
'|',
|
'|',
|
||||||
ShaderPrewarmStampPart(typeof(Gen5ShaderTranslator)),
|
ShaderPrewarmStampPart(typeof(Gen5ShaderTranslator)),
|
||||||
ShaderPrewarmStampPart(typeof(Gen5SpirvTranslator)),
|
ShaderPrewarmStampPart(typeof(Gen5SpirvTranslator)),
|
||||||
ShaderPrewarmStampPart(compiler.GetType()),
|
compiler.GetType().FullName,
|
||||||
ShaderPrewarmStampPart(typeof(ShaderProgramCache)),
|
|
||||||
DriverCacheSignature().TrimEnd('\n'));
|
DriverCacheSignature().TrimEnd('\n'));
|
||||||
|
|
||||||
private void PrewarmComputePipeline(
|
private void PrewarmComputePipeline(
|
||||||
|
|
@ -182,11 +223,14 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
var matched = runtime.Keys.Count(prewarmed.ContainsKey);
|
var matched = runtime.Keys.Count(prewarmed.ContainsKey);
|
||||||
if (_shaderPrewarmStopping)
|
if (_shaderPrewarmStopping)
|
||||||
{
|
{
|
||||||
|
SetShaderPrewarmState(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.WriteStamp(stamp);
|
list.WriteStamp(stamp);
|
||||||
|
list.ClearProgress();
|
||||||
_shaderPrewarmFinished = true;
|
_shaderPrewarmFinished = true;
|
||||||
|
SetShaderPrewarmState(false);
|
||||||
_pipelineCacheDirty = true;
|
_pipelineCacheDirty = true;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Shader prewarm done: compiled={Volatile.Read(ref _shaderPrewarmCompiled)} " +
|
$"[LOADER][INFO] Shader prewarm done: compiled={Volatile.Read(ref _shaderPrewarmCompiled)} " +
|
||||||
|
|
@ -218,6 +262,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
||||||
private void StopShaderPrewarm()
|
private void StopShaderPrewarm()
|
||||||
{
|
{
|
||||||
_shaderPrewarmStopping = true;
|
_shaderPrewarmStopping = true;
|
||||||
|
SetShaderPrewarmState(false);
|
||||||
foreach (var thread in _shaderPrewarmThreads)
|
foreach (var thread in _shaderPrewarmThreads)
|
||||||
{
|
{
|
||||||
thread.Join(TimeSpan.FromSeconds(10));
|
thread.Join(TimeSpan.FromSeconds(10));
|
||||||
|
|
|
||||||
|
|
@ -473,6 +473,19 @@ public static class ResourceMaterializer
|
||||||
return value.ToString();
|
return value.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<uint> NulledSampledFormats = new();
|
||||||
|
|
||||||
|
private static void ReportNulledSampledFormat(uint format)
|
||||||
|
{
|
||||||
|
lock (NulledSampledFormats)
|
||||||
|
{
|
||||||
|
if (NulledSampledFormats.Add(format))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[GPU][WARN] A sampled image descriptor uses unsupported format {format}; it is bound as a null texture.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool NullImageDescriptor(ReadOnlySpan<uint> descriptor) =>
|
private static bool NullImageDescriptor(ReadOnlySpan<uint> descriptor) =>
|
||||||
descriptor[0] == 0 && (descriptor[1] & 0xFF) == 0;
|
descriptor[0] == 0 && (descriptor[1] & 0xFF) == 0;
|
||||||
|
|
||||||
|
|
@ -1054,7 +1067,19 @@ public static class ResourceMaterializer
|
||||||
numericClass = ImageNumericClass.Uint;
|
numericClass = ImageNumericClass.Uint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (numericClass == ImageNumericClass.Unsupported || (baseImage.DepthCompare && numericClass != ImageNumericClass.Float))
|
else if (numericClass == ImageNumericClass.Unsupported)
|
||||||
|
{
|
||||||
|
ReportNulledSampledFormat(format);
|
||||||
|
Array.Clear(descriptor);
|
||||||
|
images[index] = image with
|
||||||
|
{
|
||||||
|
NumericClass = ImageNumericClass.Float,
|
||||||
|
Dimension = ImageDimension.Dim2D,
|
||||||
|
Cube = false,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (baseImage.DepthCompare && numericClass != ImageNumericClass.Float)
|
||||||
{
|
{
|
||||||
return Fail($"sampled image descriptor {index} uses unsupported format {format}");
|
return Fail($"sampled image descriptor {index} uses unsupported format {format}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
public sealed class Atrac9GaplessTests
|
||||||
|
{
|
||||||
|
private static readonly byte[] MonoConfig = [0xFE, 0x70, 0x17, 0xE0];
|
||||||
|
private const int SuperframeBytes = 192;
|
||||||
|
private const int SuperframeSamples = 256;
|
||||||
|
private static readonly byte[] SilentFrame = [0x00, 0x84, 0x00, 0x40];
|
||||||
|
|
||||||
|
private static Atrac9DecodeState CreateDecoder()
|
||||||
|
{
|
||||||
|
var decoder = new Atrac9DecodeState();
|
||||||
|
Assert.True(decoder.TryInitialize(MonoConfig));
|
||||||
|
return decoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Atrac9DecodeResult DecodeSuperframes(Atrac9DecodeState decoder, int superframes, int outputSamples)
|
||||||
|
{
|
||||||
|
var input = new byte[SuperframeBytes * superframes];
|
||||||
|
for (var superframe = 0; superframe < superframes; superframe++)
|
||||||
|
{
|
||||||
|
SilentFrame.CopyTo(input, superframe * SuperframeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = new byte[outputSamples * sizeof(short)];
|
||||||
|
return decoder.Decode(input, output, Atrac9PcmEncoding.Signed16, requestedChannels: 0, multipleFrames: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithoutAWindowEverySuperframeSampleIsWritten()
|
||||||
|
{
|
||||||
|
var decoder = CreateDecoder();
|
||||||
|
|
||||||
|
var result = DecodeSuperframes(decoder, 2, SuperframeSamples * 2);
|
||||||
|
|
||||||
|
Assert.Equal(SuperframeSamples * 2 * sizeof(short), result.OutputWritten);
|
||||||
|
Assert.Equal((ulong)(SuperframeSamples * 2), result.TotalDecodedSamples);
|
||||||
|
Assert.Equal(default, decoder.Gapless);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheWindowSkipsTheLeadInAndStopsAtTheTotal()
|
||||||
|
{
|
||||||
|
var decoder = CreateDecoder();
|
||||||
|
decoder.SetGapless(totalSamples: 500, skipSamples: 100, reset: false);
|
||||||
|
|
||||||
|
var first = DecodeSuperframes(decoder, 1, SuperframeSamples);
|
||||||
|
Assert.Equal(156 * sizeof(short), first.OutputWritten);
|
||||||
|
Assert.Equal(new Atrac9Gapless(344, 0, 100), decoder.Gapless);
|
||||||
|
|
||||||
|
var rest = DecodeSuperframes(decoder, 3, SuperframeSamples * 3);
|
||||||
|
Assert.Equal((256 + 88) * sizeof(short), rest.OutputWritten);
|
||||||
|
Assert.Equal(SuperframeBytes * 2, rest.InputConsumed);
|
||||||
|
Assert.Equal(new Atrac9Gapless(0, 0, 100 + 256 - 88), decoder.Gapless);
|
||||||
|
Assert.Equal(500ul, rest.TotalDecodedSamples);
|
||||||
|
|
||||||
|
var ended = DecodeSuperframes(decoder, 1, SuperframeSamples);
|
||||||
|
Assert.Equal(0, ended.OutputWritten);
|
||||||
|
Assert.Equal(0, ended.InputConsumed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ALeadInLongerThanASuperframeWritesNothingForIt()
|
||||||
|
{
|
||||||
|
var decoder = CreateDecoder();
|
||||||
|
decoder.SetGapless(totalSamples: 0, skipSamples: 300, reset: false);
|
||||||
|
|
||||||
|
var first = DecodeSuperframes(decoder, 1, 0);
|
||||||
|
Assert.Equal(0, first.OutputWritten);
|
||||||
|
Assert.Equal(SuperframeBytes, first.InputConsumed);
|
||||||
|
Assert.Equal(new Atrac9Gapless(0, 44, 256), decoder.Gapless);
|
||||||
|
|
||||||
|
var second = DecodeSuperframes(decoder, 1, SuperframeSamples);
|
||||||
|
Assert.Equal((256 - 44) * sizeof(short), second.OutputWritten);
|
||||||
|
Assert.Equal(new Atrac9Gapless(0, 0, 300), decoder.Gapless);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheRoomCheckUsesTheTrimmedSize()
|
||||||
|
{
|
||||||
|
var decoder = CreateDecoder();
|
||||||
|
decoder.SetGapless(totalSamples: 100, skipSamples: 0, reset: false);
|
||||||
|
|
||||||
|
var result = DecodeSuperframes(decoder, 1, 100);
|
||||||
|
|
||||||
|
Assert.Equal(100 * sizeof(short), result.OutputWritten);
|
||||||
|
Assert.Equal(0, result.Status & Atrac9DecodeState.ResultNotEnoughRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetRestoresTheWindowAndASetWithoutResetKeepsProgress()
|
||||||
|
{
|
||||||
|
var decoder = CreateDecoder();
|
||||||
|
decoder.SetGapless(totalSamples: 500, skipSamples: 100, reset: false);
|
||||||
|
DecodeSuperframes(decoder, 1, SuperframeSamples);
|
||||||
|
|
||||||
|
decoder.SetGapless(totalSamples: 500, skipSamples: 100, reset: false);
|
||||||
|
Assert.Equal(new Atrac9Gapless(344, 0, 100), decoder.Gapless);
|
||||||
|
|
||||||
|
decoder.Reset();
|
||||||
|
Assert.Equal(new Atrac9Gapless(500, 100, 0), decoder.Gapless);
|
||||||
|
|
||||||
|
DecodeSuperframes(decoder, 1, SuperframeSamples);
|
||||||
|
decoder.SetGapless(totalSamples: 2000, skipSamples: 10, reset: true);
|
||||||
|
Assert.Equal(new Atrac9Gapless(2000, 10, 0), decoder.Gapless);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
public sealed class AudioOut2MixTests
|
||||||
|
{
|
||||||
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
|
private const ushort ObjectMain = 0x100;
|
||||||
|
private const float HalfPower = 0.70710677f;
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData((ushort)0, true)]
|
||||||
|
[InlineData((ushort)1, true)]
|
||||||
|
[InlineData((ushort)2, true)]
|
||||||
|
[InlineData(ObjectMain, true)]
|
||||||
|
[InlineData((ushort)3, false)]
|
||||||
|
[InlineData((ushort)4, false)]
|
||||||
|
[InlineData((ushort)5, false)]
|
||||||
|
[InlineData((ushort)6, false)]
|
||||||
|
public void OnlyMainBgmAndVoicePortsReachTheSpeakers(ushort portType, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, AudioOut2Exports.RoutesToSpeakers(portType));
|
||||||
|
Assert.Equal(expected, AudioOut2Exports.TryGetStereoGains(portType, 2, 1f, AudioOut2Exports.AmbisonicsNone, 0, out _, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(64u, HalfPower, HalfPower)]
|
||||||
|
[InlineData(65u, HalfPower, -HalfPower)]
|
||||||
|
[InlineData(0u, 1f, 1f)]
|
||||||
|
[InlineData(2u, HalfPower, -HalfPower)]
|
||||||
|
public void FirstOrderAmbisonicsDecodeToStereoCardioids(uint ambisonics, float left, float right)
|
||||||
|
{
|
||||||
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 0.5f, ambisonics, 0, out var leftGain, out var rightGain));
|
||||||
|
Assert.Equal(left * 0.5f, leftGain, 5);
|
||||||
|
Assert.Equal(right * 0.5f, rightGain, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(66u)]
|
||||||
|
[InlineData(67u)]
|
||||||
|
[InlineData(99u)]
|
||||||
|
[InlineData(1u)]
|
||||||
|
[InlineData(3u)]
|
||||||
|
[InlineData(15u)]
|
||||||
|
public void HigherAmbisonicComponentsDoNotFeedStereo(uint ambisonics)
|
||||||
|
{
|
||||||
|
Assert.False(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, ambisonics, 0, out _, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlainObjectsPlayCentredOrPassThroughToOneSide()
|
||||||
|
{
|
||||||
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, 0, out var left, out var right));
|
||||||
|
Assert.Equal(HalfPower, left, 5);
|
||||||
|
Assert.Equal(HalfPower, right, 5);
|
||||||
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, AudioOut2Exports.PassthroughLeft, out left, out right));
|
||||||
|
Assert.Equal((1f, 0f), (left, right));
|
||||||
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, AudioOut2Exports.PassthroughRight, out left, out right));
|
||||||
|
Assert.Equal((0f, 1f), (left, right));
|
||||||
|
Assert.False(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 0f, AudioOut2Exports.AmbisonicsNone, 0, out _, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FifthOrderSceneDecodesASourceToItsSide()
|
||||||
|
{
|
||||||
|
const int frames = 4;
|
||||||
|
var leftSource = DecodeScene(frames, azimuthSine: 1f);
|
||||||
|
var centreSource = DecodeScene(frames, azimuthSine: 0f);
|
||||||
|
|
||||||
|
Assert.Equal(2f * HalfPower, leftSource[0], 4);
|
||||||
|
Assert.Equal(0f, leftSource[1], 4);
|
||||||
|
Assert.Equal(HalfPower, centreSource[0], 4);
|
||||||
|
Assert.Equal(HalfPower, centreSource[1], 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetAttributesStoresGainAmbisonicsAndPassthrough()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
var param = new byte[0x40];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(param, ObjectMain);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param.AsSpan(4), 0x100);
|
||||||
|
Assert.True(memory.TryWrite(MemoryBase, param));
|
||||||
|
ctx[CpuRegister.Rdi] = 0;
|
||||||
|
ctx[CpuRegister.Rsi] = MemoryBase;
|
||||||
|
ctx[CpuRegister.Rdx] = MemoryBase + 0x80;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2PortCreate(ctx));
|
||||||
|
var handle = BinaryPrimitives.ReadUInt64LittleEndian(Read(memory, MemoryBase + 0x80, 8));
|
||||||
|
|
||||||
|
var values = new byte[12];
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(values, 0.25f);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(values.AsSpan(4), 65);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(values.AsSpan(8), AudioOut2Exports.PassthroughLeft);
|
||||||
|
Assert.True(memory.TryWrite(MemoryBase + 0x100, values));
|
||||||
|
var attributes = new byte[0x18 * 3];
|
||||||
|
WriteAttribute(attributes, 0, 1, MemoryBase + 0x100);
|
||||||
|
WriteAttribute(attributes, 1, 8, MemoryBase + 0x104);
|
||||||
|
WriteAttribute(attributes, 2, 5, MemoryBase + 0x108);
|
||||||
|
Assert.True(memory.TryWrite(MemoryBase + 0x200, attributes));
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = MemoryBase + 0x200;
|
||||||
|
ctx[CpuRegister.Rdx] = 3;
|
||||||
|
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2PortSetAttributes(ctx));
|
||||||
|
|
||||||
|
Assert.Equal((0.25f, 65u, AudioOut2Exports.PassthroughLeft), AudioOut2Exports.PortMixAttributes(handle));
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
AudioOut2Exports.AudioOut2PortDestroy(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[] DecodeScene(int frames, float azimuthSine)
|
||||||
|
{
|
||||||
|
var mix = new float[frames * 2];
|
||||||
|
var source = new byte[frames * sizeof(float)];
|
||||||
|
for (var acn = 0u; acn < 36; acn++)
|
||||||
|
{
|
||||||
|
var component = acn switch
|
||||||
|
{
|
||||||
|
0 => 1f,
|
||||||
|
1 => azimuthSine,
|
||||||
|
_ => 0.6f,
|
||||||
|
};
|
||||||
|
for (var frame = 0; frame < frames; frame++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(source.AsSpan(frame * sizeof(float)), component);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsAcnBase + acn, 0, out var left, out var right))
|
||||||
|
{
|
||||||
|
AudioOut2Exports.MixPortIntoStereo(source, mix, frames, 1, sizeof(float), true, left, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteAttribute(byte[] attributes, int index, uint id, ulong value)
|
||||||
|
{
|
||||||
|
var entry = attributes.AsSpan(index * 0x18, 0x18);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(entry, id);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(entry[0x08..], value);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(entry[0x10..], sizeof(uint));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] Read(FakeCpuMemory memory, ulong address, int length)
|
||||||
|
{
|
||||||
|
var bytes = new byte[length];
|
||||||
|
Assert.True(memory.TryRead(address, bytes));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
public sealed class AudioOut2PacingTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(256u, 48000u, 1u, 40.0)]
|
||||||
|
[InlineData(256u, 48000u, 2u, 45.333)]
|
||||||
|
[InlineData(256u, 48000u, 4u, 56.0)]
|
||||||
|
[InlineData(1024u, 48000u, 4u, 104.0)]
|
||||||
|
public void ABlockingPushWaitsUntilTheDeviceHoldsTheCushionAndOneSlotLessThanTheQueue(
|
||||||
|
uint grainSamples, uint frequency, uint queueDepth, double expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, AudioOut2Exports.DeviceSlotFreeThreshold(grainSamples, frequency, queueDepth), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheThresholdStaysFarBelowTheHostQueueCap()
|
||||||
|
{
|
||||||
|
const double hostCapMilliseconds = 128 * 1024 * 1000.0 / (48000 * 4);
|
||||||
|
Assert.True(AudioOut2Exports.DeviceSlotFreeThreshold(256, 48000, 2) < hostCapMilliseconds / 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Cpu.Native;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Cpu;
|
||||||
|
|
||||||
|
public sealed class GuestStackBoundsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public unsafe void AllocationQueryHandlesProtectionSplitsAndFallsBackForPartialCommit()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
|
|
||||||
|
var pageSize = (nuint)Environment.SystemPageSize;
|
||||||
|
var memory = (byte*)HostMemory.Alloc(null, pageSize * 3,
|
||||||
|
HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT, HostMemory.PAGE_READWRITE);
|
||||||
|
Assert.True(memory != null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.True(HostMemory.Protect(memory + pageSize, pageSize, HostMemory.PAGE_READONLY, out _));
|
||||||
|
// VirtualQuery at the last page would return only that page, not the allocation.
|
||||||
|
Assert.True(DirectExecutionBackend.TryGetGuestStackBounds((ulong)(memory + pageSize * 2 + 16),
|
||||||
|
out var bottom, out var top));
|
||||||
|
Assert.Equal((ulong)memory, bottom);
|
||||||
|
Assert.Equal((ulong)(memory + pageSize * 3), top);
|
||||||
|
|
||||||
|
Assert.True(HostMemory.Free(memory, pageSize, 0x4000)); // MEM_DECOMMIT
|
||||||
|
Assert.True(DirectExecutionBackend.TryGetGuestStackBounds((ulong)(memory + pageSize * 2 + 16),
|
||||||
|
out bottom, out top));
|
||||||
|
Assert.Equal((ulong)(memory + pageSize * 2), bottom);
|
||||||
|
Assert.Equal((ulong)(memory + pageSize * 3), top);
|
||||||
|
Assert.False(DirectExecutionBackend.TryGetGuestStackBounds((ulong)memory, out _, out _));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Assert.True(HostMemory.Free(memory, 0, HostMemory.MEM_RELEASE));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(DirectExecutionBackend.TryGetGuestStackBounds(0, out _, out _));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -104,6 +104,43 @@ public sealed class GuestBufferCacheTests : IClassFixture<HeadlessVulkanFixture>
|
||||||
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12 }, harness.Read(address, 4));
|
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12 }, harness.Read(address, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AHostWordCopyRepeatsTheSourceAndReachesARegisteredBuffer()
|
||||||
|
{
|
||||||
|
if (_vulkan is null) return;
|
||||||
|
using var harness = new CacheHarness(_vulkan);
|
||||||
|
var address = harness.MapBacked(Page, ReadWrite);
|
||||||
|
var destination = address + 0x100;
|
||||||
|
harness.Write(address, Bytes(1u, 2u, 3u));
|
||||||
|
var (before, beforeOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(destination, 28, isWritten: false));
|
||||||
|
Assert.Equal(new byte[28], harness.ReadBack(before, beforeOffset, 28));
|
||||||
|
|
||||||
|
Assert.True(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(destination, address, 3, 7)));
|
||||||
|
|
||||||
|
var expected = Bytes(1u, 2u, 3u, 1u, 2u, 3u, 1u);
|
||||||
|
Assert.Equal(expected, harness.Read(destination, 28));
|
||||||
|
Assert.False(harness.Cache.HasGpuDirtyBytes(destination, 28));
|
||||||
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(destination, 28, isWritten: false));
|
||||||
|
Assert.Equal(expected, harness.ReadBack(buffer, offset, 28));
|
||||||
|
harness.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AHostWordCopyIsRefusedForGpuWrittenOrOverlappingRanges()
|
||||||
|
{
|
||||||
|
if (_vulkan is null) return;
|
||||||
|
using var harness = new CacheHarness(_vulkan);
|
||||||
|
var address = harness.MapBacked(Page, ReadWrite);
|
||||||
|
harness.Write(address, Bytes(1u, 2u, 3u, 4u));
|
||||||
|
|
||||||
|
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 8, address, 4, 4)));
|
||||||
|
harness.Worker.Run(() => harness.Cache.ObtainBuffer(address + 0x200, 4, isWritten: true));
|
||||||
|
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x100, address + 0x200, 1, 1)));
|
||||||
|
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x200, address, 1, 1)));
|
||||||
|
Assert.True(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x100, address, 4, 4)));
|
||||||
|
harness.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(false)]
|
[InlineData(false)]
|
||||||
[InlineData(true)]
|
[InlineData(true)]
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,48 @@ public sealed class BoundedFillDetectorTests
|
||||||
Assert.True(fill.Formatted);
|
Assert.True(fill.Formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly uint[] DemonsSoulsModuloCopy =
|
||||||
|
[
|
||||||
|
0xBEFC03FF, 0xA65CD91E, 0xBF960000, 0xBFA00003, 0xD7460002, 0x04010C0C, 0xF4201A84, 0xFA000000,
|
||||||
|
0xBF8CC07F, 0x7DA8046A, 0xBF88002A, 0xF4200304, 0xFA000004, 0xBF8CC07F, 0x7E000C0C, 0xBF070C80,
|
||||||
|
0x8588807E, 0x7E005700, 0x100000FF, 0x4F800000, 0x7E060F00, 0xD5766A00, 0x0202060C, 0x7D8A0280,
|
||||||
|
0x4C020080, 0x02000101, 0xD56A0001, 0x00020700, 0x4C000303, 0x4A020303, 0x02000101, 0xD56A0000,
|
||||||
|
0x00020500, 0xD5690001, 0x0002000C, 0x4C060302, 0x7D8C02F9, 0x06068A02, 0x7D86060C, 0x87EA6A0A,
|
||||||
|
0x50000080, 0xD5286A00, 0x002A00C1, 0xD5010000, 0x002200C1, 0xD5690000, 0x0002000C, 0x4C000102,
|
||||||
|
0xE0002000, 0x80000000, 0xBF8C3F70, 0xE0102000, 0x80010002, 0xBF810000,
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheDemonsSoulsModuloCopyIsRecognized()
|
||||||
|
{
|
||||||
|
var program = Decode(DemonsSoulsModuloCopy);
|
||||||
|
var copy = BoundedFillDetector.DetectCopy(program);
|
||||||
|
|
||||||
|
Assert.NotNull(copy);
|
||||||
|
static FillWord User(uint register) => new(FillWordSource.UserData, register, 0, 0);
|
||||||
|
Assert.Equal(12u, copy.GroupScalarRegister);
|
||||||
|
Assert.Equal(new FillWord(FillWordSource.BufferResource, 8, 0, 0), copy.Count);
|
||||||
|
Assert.Equal(new FillWord(FillWordSource.BufferResource, 8, 0, 4), copy.Modulus);
|
||||||
|
Assert.Equal([User(0), User(1), User(2), User(3)], copy.Source);
|
||||||
|
Assert.Equal([User(4), User(5), User(6), User(7)], copy.Destination);
|
||||||
|
Assert.Null(BoundedFillDetector.Detect(program));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AModuloCopyStoringAtTheRemainderIsRejected()
|
||||||
|
{
|
||||||
|
var words = DemonsSoulsModuloCopy.ToArray();
|
||||||
|
words[Array.IndexOf(words, 0x80010002u)] = 0x80010000u;
|
||||||
|
Assert.Null(BoundedFillDetector.DetectCopy(Decode(words)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FillsAreNotCopies()
|
||||||
|
{
|
||||||
|
Assert.Null(BoundedFillDetector.DetectCopy(Decode(PatternFromUserData)));
|
||||||
|
Assert.Null(BoundedFillDetector.DetectCopy(Decode(ZeroRangeFromConstantBuffer)));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void APatternFillWithAnotherBranchTargetIsRejected()
|
public void APatternFillWithAnotherBranchTargetIsRejected()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,52 @@ public sealed class ShaderPrewarmListTests : IDisposable
|
||||||
Assert.False(list.IsStampCurrent("build-b"));
|
Assert.False(list.IsStampCurrent("build-b"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProgressResumesUnderTheSameStampAndClears()
|
||||||
|
{
|
||||||
|
using (var list = Open())
|
||||||
|
{
|
||||||
|
Assert.Empty(list.ReadProgress("build-a"));
|
||||||
|
list.AppendProgress("build-a", [1UL, 2UL]);
|
||||||
|
list.AppendProgress("build-a", [0xFEDC_BA98_7654_3210UL]);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var reopened = Open())
|
||||||
|
{
|
||||||
|
Assert.Equal(new HashSet<ulong> { 1UL, 2UL, 0xFEDC_BA98_7654_3210UL }, reopened.ReadProgress("build-a"));
|
||||||
|
reopened.ClearProgress();
|
||||||
|
Assert.Empty(reopened.ReadProgress("build-a"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProgressOfAnotherStampIsDiscarded()
|
||||||
|
{
|
||||||
|
using var list = Open();
|
||||||
|
list.AppendProgress("build-a", [7UL]);
|
||||||
|
|
||||||
|
Assert.Empty(list.ReadProgress("build-b"));
|
||||||
|
Assert.Empty(list.ReadProgress("build-a"));
|
||||||
|
list.AppendProgress("build-b", [8UL]);
|
||||||
|
Assert.Equal(new HashSet<ulong> { 8UL }, list.ReadProgress("build-b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ARecordIdentityDependsOnItsSpecialization()
|
||||||
|
{
|
||||||
|
using (var list = Open())
|
||||||
|
{
|
||||||
|
_ = CompileAtRuntime(list, BufferDescriptorWords.Format32UInt);
|
||||||
|
_ = CompileAtRuntime(list, BufferDescriptorWords.Format32x4UInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reloaded = Open();
|
||||||
|
var identities = reloaded.LoadedComputes().Select(item => ShaderPrewarmList.Identity(item.Record)).ToArray();
|
||||||
|
Assert.Equal(2, identities.Length);
|
||||||
|
Assert.NotEqual(identities[0], identities[1]);
|
||||||
|
Assert.Equal(identities[0], ShaderPrewarmList.Identity(reloaded.LoadedComputes()[0].Record));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RecordedReadsReplayAsMergedRanges()
|
public void RecordedReadsReplayAsMergedRanges()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,83 @@ public sealed class RenderExecutorComputeTests : IDisposable
|
||||||
AssertNotDispatched();
|
AssertNotDispatched();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const ulong CopySource = RecordingRenderHost.MemoryBase + 0x71_0000;
|
||||||
|
|
||||||
|
private void ConfigureBoundedCopy(uint count, uint modulus, uint sourceRecords, uint destinationRecords, uint sourceFormat = Format32UInt)
|
||||||
|
{
|
||||||
|
static SharpEmu.Libs.Gpu.Pipelines.FillWord User(uint register) =>
|
||||||
|
new(SharpEmu.Libs.Gpu.Pipelines.FillWordSource.UserData, register, 0, 0);
|
||||||
|
_host.WriteGuest(FillParameters, [.. BitConverter.GetBytes(count), .. BitConverter.GetBytes(modulus)]);
|
||||||
|
var copy = new SharpEmu.Libs.Gpu.Pipelines.BoundedCopy(
|
||||||
|
12,
|
||||||
|
new SharpEmu.Libs.Gpu.Pipelines.FillWord(SharpEmu.Libs.Gpu.Pipelines.FillWordSource.BufferResource, 8, 0, 0),
|
||||||
|
new SharpEmu.Libs.Gpu.Pipelines.FillWord(SharpEmu.Libs.Gpu.Pipelines.FillWordSource.BufferResource, 8, 0, 4),
|
||||||
|
[User(0), User(1), User(2), User(3)],
|
||||||
|
[User(4), User(5), User(6), User(7)]);
|
||||||
|
var program = new ShaderProgramInfo { Stage = ShaderStageKind.Compute, Hash = 0xF333, BoundedCopy = copy };
|
||||||
|
var source = BufferDescriptor(CopySource, 4, sourceRecords, format: sourceFormat);
|
||||||
|
var destination = BufferDescriptor(MetadataAddress, 4, destinationRecords, format: Format32UInt);
|
||||||
|
var parameters = BufferDescriptor(FillParameters, 16, 1, format: 0);
|
||||||
|
var input = ComputeProgram(Stage(program, userData: [.. source, .. destination, .. parameters])).Input;
|
||||||
|
_pipelines.Compute = new ComputeProgram
|
||||||
|
{
|
||||||
|
Program = new ShaderProgram(0x33),
|
||||||
|
Input = new ComputeInputInfo
|
||||||
|
{
|
||||||
|
ThreadsX = 64, ThreadsY = 1, ThreadsZ = 1, GroupIdX = true, ThreadIdCount = 1, WaveSize = 64,
|
||||||
|
WorkgroupRegister = 12, Stage = input.Stage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BoundedCopy_BecomesAHostCopyAndRecordsNothing()
|
||||||
|
{
|
||||||
|
ConfigureBoundedCopy(count: 0x98, modulus: 0x98, sourceRecords: 0x98, destinationRecords: 0x98);
|
||||||
|
_executor.Dispatch(1, Banks(), 3, 1, 1, 0x41);
|
||||||
|
|
||||||
|
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 152 152", _host.Calls);
|
||||||
|
AssertNotDispatched();
|
||||||
|
Assert.Contains("reset_bindings", _host.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BoundedCopy_IsClampedToTheThreadsAndTheDestinationRecords()
|
||||||
|
{
|
||||||
|
ConfigureBoundedCopy(count: 0x1000, modulus: 4, sourceRecords: 4, destinationRecords: 0x400);
|
||||||
|
_executor.Dispatch(1, Banks(), 4, 1, 1, 0x41);
|
||||||
|
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 4 256", _host.Calls);
|
||||||
|
|
||||||
|
ConfigureBoundedCopy(count: 0x1000, modulus: 4, sourceRecords: 4, destinationRecords: 0x20);
|
||||||
|
_executor.Dispatch(1, Banks(), 4, 1, 1, 0x41);
|
||||||
|
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 4 32", _host.Calls);
|
||||||
|
AssertNotDispatched();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0u, 4u, Format32UInt)]
|
||||||
|
[InlineData(8u, 4u, Format32UInt)]
|
||||||
|
[InlineData(4u, 4u, 22u)]
|
||||||
|
public void BoundedCopyThatIsNotAPlainWordCopy_IsDispatched(uint modulus, uint sourceRecords, uint sourceFormat)
|
||||||
|
{
|
||||||
|
ConfigureBoundedCopy(count: 0x40, modulus: modulus, sourceRecords: sourceRecords, destinationRecords: 0x40, sourceFormat: sourceFormat);
|
||||||
|
_executor.Dispatch(1, Banks(), 1, 1, 1, 0x41);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("host_copy", StringComparison.Ordinal));
|
||||||
|
AssertDispatched(1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BoundedCopyTheHostRefuses_IsDispatched()
|
||||||
|
{
|
||||||
|
_host.HostCopyAccepted = false;
|
||||||
|
ConfigureBoundedCopy(count: 0x40, modulus: 0x40, sourceRecords: 0x40, destinationRecords: 0x40);
|
||||||
|
_executor.Dispatch(1, Banks(), 1, 1, 1, 0x41);
|
||||||
|
|
||||||
|
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 64 64", _host.Calls);
|
||||||
|
AssertDispatched(1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private void ConfigurePatternFill(uint count, uint length, uint[] pattern)
|
private void ConfigurePatternFill(uint count, uint length, uint[] pattern)
|
||||||
{
|
{
|
||||||
static SharpEmu.Libs.Gpu.Pipelines.FillWord User(uint register) =>
|
static SharpEmu.Libs.Gpu.Pipelines.FillWord User(uint register) =>
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,14 @@ internal sealed class RecordingRenderHost : IRenderHost
|
||||||
return RegisteredDcc.Contains(address);
|
return RegisteredDcc.Contains(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool HostCopyAccepted { get; set; } = true;
|
||||||
|
|
||||||
|
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
|
||||||
|
{
|
||||||
|
Calls.Add($"host_copy {destination:X} {source:X} {sourceWords} {words}");
|
||||||
|
return HostCopyAccepted;
|
||||||
|
}
|
||||||
|
|
||||||
public Exception Fatal(string message) => new RenderExecutorFatalException(message);
|
public Exception Fatal(string message) => new RenderExecutorFatalException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ public sealed class PresenterStartupTests
|
||||||
Set("_presenterStartupFailure", null);
|
Set("_presenterStartupFailure", null);
|
||||||
Set("_activePresenter", null);
|
Set("_activePresenter", null);
|
||||||
Set("_thread", Thread.CurrentThread);
|
Set("_thread", Thread.CurrentThread);
|
||||||
|
Set("_shaderPrewarmDecided", true);
|
||||||
|
Set("_shaderPrewarmRunning", false);
|
||||||
|
Set("_shaderPrewarmReleased", false);
|
||||||
|
Set("_shaderPrewarmHoldReported", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FieldInfo Field(string name) => PresenterType.GetField(name, BindingFlags.Static | BindingFlags.NonPublic)!;
|
private static FieldInfo Field(string name) => PresenterType.GetField(name, BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||||
|
|
@ -106,6 +110,45 @@ public sealed class PresenterStartupTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void ASubmissionWaitsUntilTheShaderPrewarmIsDecidedAndDone(bool undecided)
|
||||||
|
{
|
||||||
|
var runner = new StreamRunner();
|
||||||
|
var queue = new CommandStreamQueue(runner.Host);
|
||||||
|
Assert.True(runner.Host.Memory.TryWrite(StreamRunner.CommandAddress, new byte[] { 0, 16, 0, 192, 0, 0, 0, 0 }));
|
||||||
|
using var state = new StartupState();
|
||||||
|
state.Set(undecided ? "_shaderPrewarmDecided" : "_shaderPrewarmRunning", !undecided);
|
||||||
|
state.Publish(queue);
|
||||||
|
Exception? failure = null;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try { VulkanVideoPresenter.SubmitCommandStream(runner.Host.Memory, 0, StreamRunner.CommandAddress, 2, 1, null); }
|
||||||
|
catch (Exception exception) { failure = exception; }
|
||||||
|
});
|
||||||
|
thread.Start();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.True(SpinWait.SpinUntil(() => (thread.ThreadState & ThreadState.WaitSleepJoin) != 0, 5000));
|
||||||
|
Assert.False(thread.Join(200));
|
||||||
|
Assert.Equal(0, queue.PendingSubmissionCount);
|
||||||
|
state.Set("_shaderPrewarmDecided", true);
|
||||||
|
state.Set("_shaderPrewarmRunning", false);
|
||||||
|
Assert.True(thread.Join(5000));
|
||||||
|
Assert.Null(failure);
|
||||||
|
Assert.Equal(1, queue.PendingSubmissionCount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (thread.IsAlive)
|
||||||
|
{
|
||||||
|
VulkanVideoPresenter.RequestClose();
|
||||||
|
Assert.True(thread.Join(5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StartupFailureRejectsTheSubmission()
|
public void StartupFailureRejectsTheSubmission()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue