Compare commits
1 Commits
1016_part3
...
des_page_f
| Author | SHA1 | Date |
|---|---|---|
|
|
d8616eec01 |
|
|
@ -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
|
||||
|
|
@ -197,6 +205,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;
|
||||
|
||||
|
|
@ -545,6 +556,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 +567,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;
|
||||
}
|
||||
|
|
@ -1087,7 +1133,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 +1159,8 @@ public static class AudioOut2Exports
|
|||
ch,
|
||||
bps,
|
||||
isFloat,
|
||||
additive: mixedPorts > 0);
|
||||
leftGain,
|
||||
rightGain);
|
||||
mixedPorts++;
|
||||
}
|
||||
|
||||
|
|
@ -1162,20 +1211,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 +1302,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -557,7 +557,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 +586,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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,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 _));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue