Page fixes (#1023)
* [DeS] Fixes for page faults and stack bounds issues in the emulator and audio fixes * [ShaderPrewarm] Hold the game until the prewarm finishes and resume interrupted runs The prewarm compiled thousands of compute pipelines on background threads while the now faster boot ran beside it, so logos, Bink movies and the first scenes raced the compile and dropped frames. - Hold the guest at its first splash hide, GPU submission or CPU flip until the prewarm is decided and done; the render thread keeps presenting the splash. SHARPEMU_SHADER_PREWARM_WAIT=0 opts out. - Stamp only the shader translators, the compiler type and the driver, so a Libs-only build no longer triggers a full prewarm. - Record finished pipelines in shader-prewarm.progress so an interrupted prewarm resumes with the remaining ones. * [AudioOut2] Block a blocking push only until a queue slot frees A blocking sceAudioOut2ContextPush waited only when the host stream hit its 128 KiB cap, so a title rendering faster than real time kept about 680 ms queued and every sound played that far behind (GTA V: queue=2 context, queued_ms avg=678). The push now waits until the device holds the cushion plus QueueDepth-1 grains, the level GetQueueLevel already reports as full; the host cap stays as the limit for non-blocking bursts. GTA V now holds about 41 ms with no underruns. * [GPU] Run bounded modulo copies of plain words on the host Demon's Souls copies its per-frame constant and descriptor tables with a compute kernel (dst[i] = src[i % N] for i < count). Run on the GPU, the destinations stay GPU-dirty, and every draw whose resource tables read them waits for a readback: about 19% of the render thread in the title scene, for values that never changed. The kernel is now recognized by its instruction sequence. When both descriptors are plain 32-bit words of the same format and neither range is GPU-dirty or GPU image memory, the copy is written to guest memory through the host DMA path and the dispatch is skipped; anything else still runs on the GPU. SHARPEMU_HOST_BOUNDED_COPY=0 turns it off. Title scene with profiling: 7.4-8.6 -> 11.8-15.9 fps. * [Diagnostics] Summarize synchronized table reads and trace several writer addresses SHARPEMU_TRACE_GPU_READS=1 now also reports, every five seconds, the guest words resource planning read through a GPU synchronization: read count, how often the value changed, and the first and last value. SHARPEMU_TRACE_WRITER_ADDRESS accepts a comma-separated list.
This commit is contained in:
parent
57e0da2150
commit
0163750e7e
|
|
@ -9,10 +9,23 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
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;
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -176,4 +189,18 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||
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 ReaderWriterLockSlim _spansLock = 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 readonly object _attachGate = new();
|
||||
|
|
@ -204,6 +207,7 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
try
|
||||
{
|
||||
_spans.Add(address, size);
|
||||
Interlocked.Increment(ref _spanVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -276,6 +280,7 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
try
|
||||
{
|
||||
_spans.Remove(address, size);
|
||||
Interlocked.Increment(ref _spanVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -590,7 +590,16 @@ public static class AjmExports
|
|||
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);
|
||||
Trace(
|
||||
$"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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ internal readonly record struct Atrac9DecodeResult(
|
|||
ulong TotalDecodedSamples,
|
||||
uint Frames);
|
||||
|
||||
internal readonly record struct Atrac9Gapless(uint TotalSamples, ushort SkipSamples, ushort SkippedSamples);
|
||||
|
||||
internal sealed class Atrac9DecodeState
|
||||
{
|
||||
internal const int ResultNotInitialized = 0x00000001;
|
||||
|
|
@ -47,6 +49,33 @@ internal sealed class Atrac9DecodeState
|
|||
private int _containerHeaderLength;
|
||||
private int _compressedLength;
|
||||
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
|
||||
{
|
||||
|
|
@ -82,6 +111,7 @@ internal sealed class Atrac9DecodeState
|
|||
_compressedLength = 0;
|
||||
_totalDecodedSamples = 0;
|
||||
_containerHeaderLength = 0;
|
||||
_gapless = _gaplessInit;
|
||||
Trace(
|
||||
$"initialized config={Convert.ToHexString(normalizedConfig)} channels={config.ChannelCount} " +
|
||||
$"rate={config.SampleRate} frame_samples={config.FrameSamples} " +
|
||||
|
|
@ -114,6 +144,7 @@ internal sealed class Atrac9DecodeState
|
|||
_compressedLength = 0;
|
||||
_totalDecodedSamples = 0;
|
||||
_containerHeaderLength = 0;
|
||||
_gapless = _gaplessInit;
|
||||
if (_compressed is not null)
|
||||
{
|
||||
Array.Clear(_compressed);
|
||||
|
|
@ -153,7 +184,6 @@ internal sealed class Atrac9DecodeState
|
|||
}
|
||||
|
||||
var bytesPerSample = GetBytesPerSample(encoding);
|
||||
var outputBytesPerSuperframe = checked(config.SuperframeSamples * channels * bytesPerSample);
|
||||
var consumed = 0;
|
||||
var written = 0;
|
||||
uint frames = 0;
|
||||
|
|
@ -162,6 +192,11 @@ internal sealed class Atrac9DecodeState
|
|||
while (_compressedLength == config.SuperframeBytes ||
|
||||
consumed < input.Length)
|
||||
{
|
||||
if (HasSampleLimit && _gapless.TotalSamples == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Titles that stream whole .at9 files hand AJM the RIFF/WAVE
|
||||
// container rather than a pointer into its `data` chunk, so the
|
||||
// stream has to be advanced past the header before the first
|
||||
|
|
@ -202,7 +237,15 @@ internal sealed class Atrac9DecodeState
|
|||
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;
|
||||
break;
|
||||
|
|
@ -231,14 +274,19 @@ internal sealed class Atrac9DecodeState
|
|||
|
||||
WriteInterleaved(
|
||||
_planarPcm,
|
||||
output.Slice(written, outputBytesPerSuperframe),
|
||||
config.SuperframeSamples,
|
||||
output.Slice(written, outputBytes),
|
||||
skip,
|
||||
samples,
|
||||
channels,
|
||||
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;
|
||||
_totalDecodedSamples += unchecked((uint)config.SuperframeSamples);
|
||||
_totalDecodedSamples += unchecked((uint)samples);
|
||||
frames += unchecked((uint)config.FramesPerSuperframe);
|
||||
|
||||
if (!multipleFrames)
|
||||
|
|
@ -353,12 +401,13 @@ internal sealed class Atrac9DecodeState
|
|||
private static void WriteInterleaved(
|
||||
short[][] source,
|
||||
Span<byte> destination,
|
||||
int firstSample,
|
||||
int samples,
|
||||
int channels,
|
||||
Atrac9PcmEncoding encoding)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ public static class AudioOut2Exports
|
|||
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
||||
private const int AttributeEntrySize = 0x18;
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
public double DeviceSlotFreeMilliseconds =>
|
||||
DeviceSlotFreeThreshold(GrainSamples, Frequency, QueueDepth);
|
||||
|
||||
// Grains the device holds beyond the cushion, at most QueueDepth.
|
||||
public uint DeviceQueuedGrains(int queuedMilliseconds)
|
||||
{
|
||||
|
|
@ -197,6 +208,9 @@ public static class AudioOut2Exports
|
|||
public uint SamplingFrequency { get; }
|
||||
public uint GrainSamples { get; }
|
||||
public ulong PcmAddress;
|
||||
public float Gain = 1f;
|
||||
public uint Ambisonics = AmbisonicsNone;
|
||||
public uint Passthrough;
|
||||
|
||||
public int PcmPending;
|
||||
|
||||
|
|
@ -419,6 +433,14 @@ public static class AudioOut2Exports
|
|||
{
|
||||
context.DevicePaced = true;
|
||||
}
|
||||
|
||||
if (submitted || advancedGrain == 1)
|
||||
{
|
||||
if (blocking != 0)
|
||||
{
|
||||
WaitForDeviceSlot(context);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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> pcm = stackalloc byte[8];
|
||||
Span<byte> word = stackalloc byte[sizeof(uint)];
|
||||
for (uint i = 0; i < attributeCount; i++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(attributesAddress + (i * AttributeEntrySize), entry))
|
||||
|
|
@ -555,7 +578,41 @@ public static class AudioOut2Exports
|
|||
var attributeId = BinaryPrimitives.ReadUInt32LittleEndian(entry);
|
||||
var valueAddress = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x08..]);
|
||||
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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
if (context.DevicePaced)
|
||||
|
|
@ -1087,7 +1166,9 @@ public static class AudioOut2Exports
|
|||
if (port.ContextHandle != context.Handle ||
|
||||
port.PcmAddress == 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;
|
||||
}
|
||||
|
|
@ -1111,7 +1192,8 @@ public static class AudioOut2Exports
|
|||
ch,
|
||||
bps,
|
||||
isFloat,
|
||||
additive: mixedPorts > 0);
|
||||
leftGain,
|
||||
rightGain);
|
||||
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;
|
||||
return kind is 0 or 1;
|
||||
left = 0f;
|
||||
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,
|
||||
Span<float> mix,
|
||||
int frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat,
|
||||
bool additive)
|
||||
float leftGain,
|
||||
float rightGain)
|
||||
{
|
||||
var frameSize = channels * bytesPerSample;
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
|
|
@ -1204,16 +1335,8 @@ public static class AudioOut2Exports
|
|||
: ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
|
||||
}
|
||||
|
||||
if (additive)
|
||||
{
|
||||
mix[frame * 2] += left;
|
||||
mix[(frame * 2) + 1] += right;
|
||||
}
|
||||
else
|
||||
{
|
||||
mix[frame * 2] = left;
|
||||
mix[(frame * 2) + 1] = right;
|
||||
}
|
||||
mix[frame * 2] += left * leftGain;
|
||||
mix[(frame * 2) + 1] += right * rightGain;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,15 +9,19 @@ namespace SharpEmu.Libs.Diagnostics;
|
|||
|
||||
internal static class DccWriterTrace
|
||||
{
|
||||
private const int MaxReports = 64;
|
||||
private const int MaxReports = 256;
|
||||
private const int MaxFrames = 12;
|
||||
|
||||
private static readonly bool DccEnabled =
|
||||
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]
|
||||
public static ulong CurrentProgram;
|
||||
|
|
@ -43,7 +47,7 @@ internal static class DccWriterTrace
|
|||
|
||||
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))) ||
|
||||
!_reported.Add((address, size, CurrentProgram)))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,30 @@ internal static class GpuReadTrace
|
|||
public static string? CurrentStage;
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -460,6 +460,72 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
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)
|
||||
{
|
||||
var dstMemory = !dstGds;
|
||||
|
|
@ -557,7 +623,21 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private ulong _bdaTouchTick = ulong.MaxValue;
|
||||
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)
|
||||
{
|
||||
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 traceCovered = false;
|
||||
|
|
@ -572,10 +652,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
|
||||
var mapping = (ulong)spans.Count;
|
||||
foreach (var span in spans)
|
||||
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
|
||||
var spans = mapped;
|
||||
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
|
||||
{
|
||||
foreach (var span in spans)
|
||||
|
|
|
|||
|
|
@ -1196,4 +1196,10 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
_ = (address, size, fillValue);
|
||||
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? PatternLength = null);
|
||||
|
||||
public sealed record BoundedCopy(
|
||||
uint GroupScalarRegister,
|
||||
FillWord Count,
|
||||
FillWord Modulus,
|
||||
FillWord[] Source,
|
||||
FillWord[] Destination);
|
||||
|
||||
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 ZeroConstant = 128;
|
||||
private const uint NullScalar = 125;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO.Hashing;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
|
@ -141,6 +142,7 @@ internal sealed class ShaderPrewarmList : IDisposable
|
|||
{
|
||||
public const string FileName = "shader-prewarm.bin";
|
||||
public const string StampFileName = "shader-prewarm.stamp";
|
||||
public const string ProgressFileName = "shader-prewarm.progress";
|
||||
|
||||
private const uint Magic = 0x57504553;
|
||||
private const uint FormatVersion = 1;
|
||||
|
|
@ -151,16 +153,19 @@ internal sealed class ShaderPrewarmList : IDisposable
|
|||
private readonly object _gate = new();
|
||||
private readonly FileStream _file;
|
||||
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> _computes = [];
|
||||
private readonly Dictionary<(ulong Hash, uint CodeSize, ulong Address), ShaderCodeCapture> _loadedCodes = new();
|
||||
private readonly List<ComputePrewarmRecord> _loadedComputes = [];
|
||||
private bool _failed;
|
||||
|
||||
private ShaderPrewarmList(FileStream file, string stampPath)
|
||||
private ShaderPrewarmList(FileStream file, string stampPath, string progressPath)
|
||||
{
|
||||
_file = file;
|
||||
_stampPath = stampPath;
|
||||
_progressPath = progressPath;
|
||||
}
|
||||
|
||||
public int LoadedComputeCount => _loadedComputes.Count;
|
||||
|
|
@ -172,7 +177,7 @@ internal sealed class ShaderPrewarmList : IDisposable
|
|||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(directory, FileName);
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
var payload = SerializeCompute(record);
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ internal sealed class ProgramSourceEntry
|
|||
public required bool HasBitwiseExclusiveOr { get; init; }
|
||||
public ConstantFill? ConstantFill { get; init; }
|
||||
public BoundedFill? BoundedFill { get; init; }
|
||||
public BoundedCopy? BoundedCopy { get; init; }
|
||||
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
|
||||
public ShaderVertexInput[] VertexInputs { get; init; } = [];
|
||||
public List<ProgramPermutation> Permutations { get; } = new(8);
|
||||
|
|
@ -383,6 +384,7 @@ internal sealed class ShaderProgramCache
|
|||
HasBitwiseExclusiveOr = exclusiveOr,
|
||||
ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
|
||||
BoundedFill = source.Stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null,
|
||||
BoundedCopy = source.Stage == ShaderStage.Compute ? BoundedFillDetector.DetectCopy(program) : null,
|
||||
EmbeddedFetch = fetch,
|
||||
VertexInputs = vertexInputs,
|
||||
};
|
||||
|
|
@ -837,6 +839,7 @@ internal sealed class ShaderProgramCache
|
|||
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
|
||||
ConstantFill = entry.ConstantFill,
|
||||
BoundedFill = entry.BoundedFill,
|
||||
BoundedCopy = entry.BoundedCopy,
|
||||
Buffers = buffers,
|
||||
Images = images,
|
||||
SamplerCount = info.Samplers.Count,
|
||||
|
|
|
|||
|
|
@ -188,5 +188,7 @@ public interface IRenderHost
|
|||
|
||||
bool TryFillDccMetadata(ulong address, ulong size, uint fillValue);
|
||||
|
||||
bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words);
|
||||
|
||||
Exception Fatal(string message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,12 @@ public sealed partial class RenderExecutor
|
|||
return;
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 && TryConsumeBoundedCopy(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||
{
|
||||
_host.ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||
{
|
||||
_host.ResetBindings();
|
||||
|
|
@ -552,6 +558,71 @@ public sealed partial class RenderExecutor
|
|||
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)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.Refuse(program.Hash, reason);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public class ShaderProgramInfo
|
|||
public bool HasBitwiseExclusiveOr { get; init; }
|
||||
public Pipelines.ConstantFill? ConstantFill { get; init; }
|
||||
public Pipelines.BoundedFill? BoundedFill { get; init; }
|
||||
public Pipelines.BoundedCopy? BoundedCopy { get; init; }
|
||||
public BufferResourceInfo[] Buffers { get; init; } = [];
|
||||
public ImageResourceInfo[] Images { get; init; } = [];
|
||||
public int SamplerCount { get; init; }
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
EnsureStarted(1280, 720);
|
||||
WaitForShaderPrewarm("first GPU submission");
|
||||
// Queue publication precedes device initialization; only the render thread consumes it.
|
||||
lock (_gate)
|
||||
{
|
||||
|
|
@ -117,6 +118,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_ = width;
|
||||
_ = height;
|
||||
_ = pitchInPixel;
|
||||
WaitForShaderPrewarm("first flip");
|
||||
if (!IsKnownDisplayBuffer(address) || !TryGetActivePresenter(out var presenter))
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -402,8 +402,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
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 spans = new List<GuestSpan>();
|
||||
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
|
||||
var spans = DeviceAddressSpans(memory);
|
||||
var traceAddress = GuestGpuMemoryHook.TraceAddress;
|
||||
if (traceAddress != 0)
|
||||
{
|
||||
|
|
@ -420,7 +419,29 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
if (traceAddress != 0 && !memory.Covers(traceAddress, 1))
|
||||
GuestGpuMemoryHook.Trace(traceAddress, 1,
|
||||
$"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)
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
|
||||
word = 0;
|
||||
var synchronized = false;
|
||||
if (IsCleanReadPage(address, sizeof(uint)))
|
||||
{
|
||||
if (TryGetAliasPointer(address, sizeof(uint), out var alias))
|
||||
|
|
@ -132,6 +133,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
synchronized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -146,6 +149,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
if (Diagnostics.GpuReadTrace.Enabled && synchronized)
|
||||
{
|
||||
Diagnostics.GpuReadTrace.RecordValue(address, word);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,8 +157,73 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_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()
|
||||
{
|
||||
WaitForShaderPrewarm("splash screen");
|
||||
lock (_gate)
|
||||
{
|
||||
_splashHidden = true;
|
||||
|
|
|
|||
|
|
@ -1200,5 +1200,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_bufferCache.FillDccMetadata(address, size, fillValue);
|
||||
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 const int MaxLoggedPrewarmFailures = 8;
|
||||
private const int PrewarmProgressBatch = 256;
|
||||
|
||||
private ShaderPrewarmList? _shaderPrewarmList;
|
||||
private ConcurrentDictionary<string, byte>? _prewarmedShaderIdentities;
|
||||
|
|
@ -27,16 +28,25 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private int _shaderPrewarmCompiled;
|
||||
private int _shaderPrewarmFailed;
|
||||
private int _shaderPrewarmRuntimeHits;
|
||||
private readonly ConcurrentQueue<ulong> _shaderPrewarmCompleted = new();
|
||||
|
||||
ShaderPrewarmList? IShaderPipelineHost.ShaderPrewarm => Volatile.Read(ref _shaderPrewarmList);
|
||||
|
||||
private void StartShaderPrewarm()
|
||||
{
|
||||
if (!StartShaderPrewarmCore())
|
||||
{
|
||||
SetShaderPrewarmState(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool StartShaderPrewarmCore()
|
||||
{
|
||||
if (_shaderPrewarmList is not null ||
|
||||
_pipelineCacheShardDirectory is not null ||
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM") == "0")
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath(
|
||||
|
|
@ -44,7 +54,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")));
|
||||
if (string.IsNullOrEmpty(directory) || ShaderPrewarmList.Open(directory) is not { } list)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_prewarmedShaderIdentities = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
|
||||
|
|
@ -57,7 +67,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
list.WriteStamp(stamp);
|
||||
_shaderPrewarmFinished = true;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (list.IsStampCurrent(stamp))
|
||||
|
|
@ -65,18 +75,31 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_shaderPrewarmFinished = true;
|
||||
Console.Error.WriteLine(
|
||||
$"[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 remaining = workerCount;
|
||||
var started = Stopwatch.GetTimestamp();
|
||||
var host = (IShaderPipelineHost)this;
|
||||
var sharedInt64Atomics = host.SharedInt64AtomicsEnabled;
|
||||
var execGuardElision = host.ExecGuardElisionEnabled;
|
||||
Volatile.Write(ref _shaderPrewarmTotal, pending.Count);
|
||||
Volatile.Write(ref _shaderPrewarmProgress, 0);
|
||||
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];
|
||||
for (var index = 0; index < workerCount; index++)
|
||||
{
|
||||
|
|
@ -85,8 +108,14 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
while (!_shaderPrewarmStopping && work.TryDequeue(out var item))
|
||||
{
|
||||
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)
|
||||
{
|
||||
FinishShaderPrewarm(list, stamp, started);
|
||||
|
|
@ -99,6 +128,19 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
};
|
||||
_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) =>
|
||||
|
|
@ -109,8 +151,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
'|',
|
||||
ShaderPrewarmStampPart(typeof(Gen5ShaderTranslator)),
|
||||
ShaderPrewarmStampPart(typeof(Gen5SpirvTranslator)),
|
||||
ShaderPrewarmStampPart(compiler.GetType()),
|
||||
ShaderPrewarmStampPart(typeof(ShaderProgramCache)),
|
||||
compiler.GetType().FullName,
|
||||
DriverCacheSignature().TrimEnd('\n'));
|
||||
|
||||
private void PrewarmComputePipeline(
|
||||
|
|
@ -182,11 +223,14 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var matched = runtime.Keys.Count(prewarmed.ContainsKey);
|
||||
if (_shaderPrewarmStopping)
|
||||
{
|
||||
SetShaderPrewarmState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
list.WriteStamp(stamp);
|
||||
list.ClearProgress();
|
||||
_shaderPrewarmFinished = true;
|
||||
SetShaderPrewarmState(false);
|
||||
_pipelineCacheDirty = true;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Shader prewarm done: compiled={Volatile.Read(ref _shaderPrewarmCompiled)} " +
|
||||
|
|
@ -218,6 +262,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private void StopShaderPrewarm()
|
||||
{
|
||||
_shaderPrewarmStopping = true;
|
||||
SetShaderPrewarmState(false);
|
||||
foreach (var thread in _shaderPrewarmThreads)
|
||||
{
|
||||
thread.Join(TimeSpan.FromSeconds(10));
|
||||
|
|
|
|||
|
|
@ -473,6 +473,19 @@ public static class ResourceMaterializer
|
|||
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) =>
|
||||
descriptor[0] == 0 && (descriptor[1] & 0xFF) == 0;
|
||||
|
||||
|
|
@ -1054,7 +1067,19 @@ public static class ResourceMaterializer
|
|||
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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
[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]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
|
|
|
|||
|
|
@ -127,6 +127,48 @@ public sealed class BoundedFillDetectorTests
|
|||
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]
|
||||
public void APatternFillWithAnotherBranchTargetIsRejected()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -119,6 +119,52 @@ public sealed class ShaderPrewarmListTests : IDisposable
|
|||
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]
|
||||
public void RecordedReadsReplayAsMergedRanges()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -416,6 +416,83 @@ public sealed class RenderExecutorComputeTests : IDisposable
|
|||
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)
|
||||
{
|
||||
static SharpEmu.Libs.Gpu.Pipelines.FillWord User(uint register) =>
|
||||
|
|
|
|||
|
|
@ -394,6 +394,14 @@ internal sealed class RecordingRenderHost : IRenderHost
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ public sealed class PresenterStartupTests
|
|||
Set("_presenterStartupFailure", null);
|
||||
Set("_activePresenter", null);
|
||||
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)!;
|
||||
|
|
@ -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]
|
||||
public void StartupFailureRejectsTheSubmission()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue