Revert "[DeS] Fixes for page faults and stack bounds issues in the emulator a…"

This reverts commit ec0198f2d0.
This commit is contained in:
Berk 2026-10-04 05:48:01 +03:00 committed by GitHub
parent cb42e849d7
commit 40068f68b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 36 additions and 592 deletions

View File

@ -9,23 +9,10 @@ public sealed partial class DirectExecutionBackend
{
private const int HostStackStateBytes = 3 * sizeof(ulong);
internal static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
private 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;
var queried = VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64));
if (queried == 0 ||
if (VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
{
return false;
@ -189,18 +176,4 @@ 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);
}

View File

@ -30,9 +30,6 @@ 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();
@ -207,7 +204,6 @@ public sealed class GuestGpuMemory : IDisposable
try
{
_spans.Add(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{
@ -280,7 +276,6 @@ public sealed class GuestGpuMemory : IDisposable
try
{
_spans.Remove(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{

View File

@ -590,16 +590,7 @@ public static class AjmExports
return ctx.SetReturn(OrbisAjmErrorJobCreation);
}
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);
}
var status = TryGetInstance(instanceId, out _) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
WriteBasicResult(ctx, resultAddress, status);
Trace(
$"batch_job_set_gapless_decode instance=0x{instanceId:X8} " +
@ -1172,10 +1163,6 @@ 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;
}

View File

@ -20,8 +20,6 @@ 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;
@ -49,33 +47,6 @@ 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
{
@ -111,7 +82,6 @@ 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} " +
@ -144,7 +114,6 @@ internal sealed class Atrac9DecodeState
_compressedLength = 0;
_totalDecodedSamples = 0;
_containerHeaderLength = 0;
_gapless = _gaplessInit;
if (_compressed is not null)
{
Array.Clear(_compressed);
@ -184,6 +153,7 @@ internal sealed class Atrac9DecodeState
}
var bytesPerSample = GetBytesPerSample(encoding);
var outputBytesPerSuperframe = checked(config.SuperframeSamples * channels * bytesPerSample);
var consumed = 0;
var written = 0;
uint frames = 0;
@ -192,11 +162,6 @@ 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
@ -237,15 +202,7 @@ internal sealed class Atrac9DecodeState
break;
}
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)
if (output.Length - written < outputBytesPerSuperframe)
{
status |= ResultNotEnoughRoom;
break;
@ -274,19 +231,14 @@ internal sealed class Atrac9DecodeState
WriteInterleaved(
_planarPcm,
output.Slice(written, outputBytes),
skip,
samples,
output.Slice(written, outputBytesPerSuperframe),
config.SuperframeSamples,
channels,
encoding);
_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;
written += outputBytesPerSuperframe;
_compressedLength = 0;
_totalDecodedSamples += unchecked((uint)samples);
_totalDecodedSamples += unchecked((uint)config.SuperframeSamples);
frames += unchecked((uint)config.FramesPerSuperframe);
if (!multipleFrames)
@ -401,13 +353,12 @@ 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 = firstSample; sample < firstSample + samples; sample++)
for (var sample = 0; sample < samples; sample++)
{
for (var channel = 0; channel < channels; channel++)
{

View File

@ -60,14 +60,6 @@ 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
@ -205,9 +197,6 @@ 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;
@ -556,7 +545,6 @@ 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))
@ -567,41 +555,7 @@ public static class AudioOut2Exports
var attributeId = BinaryPrimitives.ReadUInt32LittleEndian(entry);
var valueAddress = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x08..]);
var valueSize = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x10..]);
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)
if (attributeId != PortAttributeIdPcm || valueAddress == 0 || valueSize < 8)
{
continue;
}
@ -1133,9 +1087,7 @@ 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) ||
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
out var leftGain, out var rightGain))
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
{
continue;
}
@ -1159,8 +1111,7 @@ public static class AudioOut2Exports
ch,
bps,
isFloat,
leftGain,
rightGain);
additive: mixedPorts > 0);
mixedPorts++;
}
@ -1211,69 +1162,20 @@ public static class AudioOut2Exports
}
}
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)
private static bool IsMainOrBgmPort(ushort portType)
{
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;
var kind = portType & 0xFF;
return kind is 0 or 1;
}
internal static void MixPortIntoStereo(
private static void MixPortIntoStereo(
ReadOnlySpan<byte> source,
Span<float> mix,
int frames,
int channels,
int bytesPerSample,
bool isFloat,
float leftGain,
float rightGain)
bool additive)
{
var frameSize = channels * bytesPerSample;
for (var frame = 0; frame < frames; frame++)
@ -1302,8 +1204,16 @@ public static class AudioOut2Exports
: ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
}
mix[frame * 2] += left * leftGain;
mix[(frame * 2) + 1] += right * rightGain;
if (additive)
{
mix[frame * 2] += left;
mix[(frame * 2) + 1] += right;
}
else
{
mix[frame * 2] = left;
mix[(frame * 2) + 1] = right;
}
}
}

View File

@ -557,21 +557,7 @@ 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;
@ -586,7 +572,10 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
}
else
{
var spans = mapped;
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;
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
{
foreach (var span in spans)

View File

@ -402,7 +402,8 @@ 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 = DeviceAddressSpans(memory);
var spans = new List<GuestSpan>();
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
var traceAddress = GuestGpuMemoryHook.TraceAddress;
if (traceAddress != 0)
{
@ -419,29 +420,7 @@ 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, _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;
_bufferCache.PrepareBda(spans);
}
public void BindResources(IPreparedBindings prepared)

View File

@ -473,19 +473,6 @@ 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;
@ -1067,19 +1054,7 @@ public static class ResourceMaterializer
numericClass = ImageNumericClass.Uint;
}
}
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)
else if (numericClass == ImageNumericClass.Unsupported || (baseImage.DepthCompare && numericClass != ImageNumericClass.Float))
{
return Fail($"sampled image descriptor {index} uses unsupported format {format}");
}

View File

@ -1,113 +0,0 @@
// 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);
}
}

View File

@ -1,158 +0,0 @@
// 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;
}
}

View File

@ -1,44 +0,0 @@
// 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 _));
}
}