Compare commits
20 Commits
1016_part3
...
unity_fixe
| Author | SHA1 | Date |
|---|---|---|
|
|
810ff6a606 | |
|
|
edc2d17cf6 | |
|
|
dbb2c1676c | |
|
|
c698196493 | |
|
|
017969fba3 | |
|
|
4cc0eab03a | |
|
|
e96e3f480b | |
|
|
fd56cef9b9 | |
|
|
448fb6be17 | |
|
|
df55be4110 | |
|
|
909ea50117 | |
|
|
41b90121f3 | |
|
|
dd9b48f9cc | |
|
|
32f077df44 | |
|
|
eba01a757d | |
|
|
15854585f6 | |
|
|
93022ba6d6 | |
|
|
6647257dc5 | |
|
|
da6d7f15d2 | |
|
|
e9e1680d02 |
|
|
@ -22,7 +22,6 @@ public static class AudioOut2Exports
|
||||||
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
|
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
|
||||||
private const int AudioOut2ContextParamSize = 0x40;
|
private const int AudioOut2ContextParamSize = 0x40;
|
||||||
private const int AudioOut2ContextMemorySize = 0x4000;
|
private const int AudioOut2ContextMemorySize = 0x4000;
|
||||||
private const int AudioOut2ContextMemoryAlignment = 0x100;
|
|
||||||
// Exact object body size. Do not page-align to 64K — the RAGE Main Thread
|
// Exact object body size. Do not page-align to 64K — the RAGE Main Thread
|
||||||
// stack-allocates this and a 64K VLA is what planted 0x10000 on the canary.
|
// stack-allocates this and a 64K VLA is what planted 0x10000 on the canary.
|
||||||
private const int SpeakerArrayHeaderSize = 0x40;
|
private const int SpeakerArrayHeaderSize = 0x40;
|
||||||
|
|
@ -36,6 +35,17 @@ public static class AudioOut2Exports
|
||||||
private const int SpeakerArrayDivisorFieldOffset = 0x34;
|
private const int SpeakerArrayDivisorFieldOffset = 0x34;
|
||||||
private const int SpeakerArrayResultFieldOffset = 0x3C;
|
private const int SpeakerArrayResultFieldOffset = 0x3C;
|
||||||
private const uint SpeakerArrayDefaultDivisor = 1;
|
private const uint SpeakerArrayDefaultDivisor = 1;
|
||||||
|
private const int SpeakerArrayCoefficientBytes = 0x400;
|
||||||
|
|
||||||
|
private static readonly string _stackOutBufferModes =
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_AUDIO_OUT2_STACK_WRITES") ?? "1";
|
||||||
|
|
||||||
|
private static bool AllowStackOut(string which) =>
|
||||||
|
string.Equals(_stackOutBufferModes, "1", StringComparison.Ordinal) ||
|
||||||
|
_stackOutBufferModes
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Contains(which, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// SceAudioOut2SpeakerArrayParam: Position* (0x00), uint uiNumSpeakers (0x08),
|
// SceAudioOut2SpeakerArrayParam: Position* (0x00), uint uiNumSpeakers (0x08),
|
||||||
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
|
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
|
||||||
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
|
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
|
||||||
|
|
@ -60,6 +70,7 @@ public static class AudioOut2Exports
|
||||||
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
||||||
private const int AttributeEntrySize = 0x18;
|
private const int AttributeEntrySize = 0x18;
|
||||||
private const uint PortAttributeIdPcm = 0;
|
private const uint PortAttributeIdPcm = 0;
|
||||||
|
private const ushort AudioOut2PortTypeVibration = 6;
|
||||||
private const uint PortAttributeIdGain = 1;
|
private const uint PortAttributeIdGain = 1;
|
||||||
private const uint PortAttributeIdPassthrough = 5;
|
private const uint PortAttributeIdPassthrough = 5;
|
||||||
private const uint PortAttributeIdAmbisonics = 8;
|
private const uint PortAttributeIdAmbisonics = 8;
|
||||||
|
|
@ -88,6 +99,11 @@ public static class AudioOut2Exports
|
||||||
private static long _pushTraceCount;
|
private static long _pushTraceCount;
|
||||||
private static long _submitTraceCount;
|
private static long _submitTraceCount;
|
||||||
private static long _submitSkipTraceCount;
|
private static long _submitSkipTraceCount;
|
||||||
|
private static long _bedWriteTraceCount;
|
||||||
|
private static long _portCreateTraceCount;
|
||||||
|
private static long _portDestroyTraceCount;
|
||||||
|
private static long _portStateTraceCount;
|
||||||
|
private static long _attributeCallTraceCount;
|
||||||
private static long _attributePcmTraceCount;
|
private static long _attributePcmTraceCount;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
|
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
|
||||||
|
|
@ -212,7 +228,7 @@ public static class AudioOut2Exports
|
||||||
public uint Ambisonics = AmbisonicsNone;
|
public uint Ambisonics = AmbisonicsNone;
|
||||||
public uint Passthrough;
|
public uint Passthrough;
|
||||||
|
|
||||||
public int PcmPending;
|
public byte[]? PcmData;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,6 +242,7 @@ public static class AudioOut2Exports
|
||||||
private static string SecondaryBackendName = "none";
|
private static string SecondaryBackendName = "none";
|
||||||
private static ulong PrimaryContextHandle;
|
private static ulong PrimaryContextHandle;
|
||||||
private static readonly object HostSubmitGate = new();
|
private static readonly object HostSubmitGate = new();
|
||||||
|
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "g2tViFIohHE",
|
Nid = "g2tViFIohHE",
|
||||||
|
|
@ -252,6 +269,16 @@ public static class AudioOut2Exports
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "v8iOE+j8a5o",
|
||||||
|
ExportName = "sceAudioOut2MasteringSetParam",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2MasteringSetParam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// 3D-audio object latency hint; the host mixer has no object pipeline to
|
// 3D-audio object latency hint; the host mixer has no object pipeline to
|
||||||
// tune, but failure here makes Yotei tear down its whole ACM context and
|
// tune, but failure here makes Yotei tear down its whole ACM context and
|
||||||
// abort audio arena bring-up.
|
// abort audio arena bring-up.
|
||||||
|
|
@ -305,8 +332,8 @@ public static class AudioOut2Exports
|
||||||
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
|
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
|
||||||
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
|
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
|
||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
var memorySizeAddress = ctx[CpuRegister.Rsi];
|
||||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
if (paramAddress == 0 || memorySizeAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||||
}
|
}
|
||||||
|
|
@ -324,13 +351,12 @@ public static class AudioOut2Exports
|
||||||
contextMemorySize = checked(0x10000UL + (queueDepth * 0x590UL));
|
contextMemorySize = checked(0x10000UL + (queueDepth * 0x590UL));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The output is a single size_t wherever it lives. Titles keep other locals right after it
|
|
||||||
// (Octopath Traveler II divides by the dword at +12), so nothing else may be written.
|
|
||||||
Span<byte> memorySize = stackalloc byte[sizeof(ulong)];
|
Span<byte> memorySize = stackalloc byte[sizeof(ulong)];
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
|
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
|
$"[LOADER][TRACE] audio_out2.context-query-memory " +
|
||||||
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
|
$"out=0x{memorySizeAddress:X} size=0x{contextMemorySize:X}");
|
||||||
|
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||||
}
|
}
|
||||||
|
|
@ -391,8 +417,20 @@ public static class AudioOut2Exports
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// Shared backend lifetime is process-wide; just drop the context entry.
|
var contextHandle = ctx[CpuRegister.Rdi];
|
||||||
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
Contexts.TryRemove(contextHandle, out _);
|
||||||
|
foreach (var (portHandle, port) in Ports)
|
||||||
|
{
|
||||||
|
if (port.ContextHandle != contextHandle ||
|
||||||
|
!Ports.TryRemove(portHandle, out var removedPort))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref removedPort.PcmData, null);
|
||||||
|
removedPort.PcmAddress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,7 +439,38 @@ public static class AudioOut2Exports
|
||||||
ExportName = "sceAudioOut2ContextBedWrite",
|
ExportName = "sceAudioOut2ContextBedWrite",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
public static int AudioOut2ContextBedWrite(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var n = Interlocked.Increment(ref _bedWriteTraceCount);
|
||||||
|
if (n <= 8 || n % 500 == 0)
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"context-bed-write#{n} context=0x{ctx[CpuRegister.Rdi]:X} " +
|
||||||
|
$"rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} " +
|
||||||
|
$"rcx=0x{ctx[CpuRegister.Rcx]:X} r8=0x{ctx[CpuRegister.R8]:X} " +
|
||||||
|
$"r9=0x{ctx[CpuRegister.R9]:X}");
|
||||||
|
|
||||||
|
Span<byte> probe = stackalloc byte[0x40];
|
||||||
|
foreach (var (name, address) in new[]
|
||||||
|
{
|
||||||
|
("rsi", ctx[CpuRegister.Rsi]),
|
||||||
|
("rdx", ctx[CpuRegister.Rdx]),
|
||||||
|
("rcx", ctx[CpuRegister.Rcx]),
|
||||||
|
("r8", ctx[CpuRegister.R8]),
|
||||||
|
("r9", ctx[CpuRegister.R9]),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (address != 0 && ctx.Memory.TryRead(address, probe))
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"context-bed-write-probe#{n} {name}=0x{address:X} " +
|
||||||
|
$"bytes={Convert.ToHexString(probe)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "aII9h5nli9U",
|
Nid = "aII9h5nli9U",
|
||||||
|
|
@ -494,11 +563,6 @@ public static class AudioOut2Exports
|
||||||
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
|
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
|
||||||
var outLevelAddress = ctx[CpuRegister.Rsi];
|
var outLevelAddress = ctx[CpuRegister.Rsi];
|
||||||
var outAvailableAddress = ctx[CpuRegister.Rdx];
|
var outAvailableAddress = ctx[CpuRegister.Rdx];
|
||||||
if (outLevelAddress == 0)
|
|
||||||
{
|
|
||||||
outLevelAddress = outAvailableAddress;
|
|
||||||
outAvailableAddress = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Titles size their rendering by these: Wwise renders exactly puiAvailableQueues grains per
|
// Titles size their rendering by these: Wwise renders exactly puiAvailableQueues grains per
|
||||||
// update and pushes them non-blocking, so a queue always reported empty made it render a
|
// update and pushes them non-blocking, so a queue always reported empty made it render a
|
||||||
|
|
@ -550,6 +614,41 @@ public static class AudioOut2Exports
|
||||||
var portHandle = ctx[CpuRegister.Rdi];
|
var portHandle = ctx[CpuRegister.Rdi];
|
||||||
var attributesAddress = ctx[CpuRegister.Rsi];
|
var attributesAddress = ctx[CpuRegister.Rsi];
|
||||||
var attributeCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
var attributeCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var attributeStride = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||||
|
var traceCall = Interlocked.Increment(ref _attributeCallTraceCount);
|
||||||
|
if (traceCall <= 8 && attributesAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> descriptorProbe = stackalloc byte[0x40];
|
||||||
|
if (ctx.Memory.TryRead(attributesAddress, descriptorProbe))
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-set-attributes-raw#{traceCall} port=0x{portHandle:X} " +
|
||||||
|
$"count={attributeCount} stride={attributeStride} address=0x{attributesAddress:X} " +
|
||||||
|
$"bytes={Convert.ToHexString(descriptorProbe)}");
|
||||||
|
|
||||||
|
Span<byte> targetProbe = stackalloc byte[0x20];
|
||||||
|
for (var offset = 0; offset <= descriptorProbe.Length - sizeof(ulong); offset += sizeof(ulong))
|
||||||
|
{
|
||||||
|
var candidate = BinaryPrimitives.ReadUInt64LittleEndian(descriptorProbe[offset..]);
|
||||||
|
if (!IsWritableOutBuffer(candidate) ||
|
||||||
|
!ctx.Memory.TryRead(candidate, targetProbe))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-set-attributes-target#{traceCall} offset=0x{offset:X} " +
|
||||||
|
$"address=0x{candidate:X} bytes={Convert.ToHexString(targetProbe)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-set-attributes-raw#{traceCall} read-failed address=0x{attributesAddress:X} " +
|
||||||
|
$"count={attributeCount} stride={attributeStride}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Ports.TryGetValue(portHandle, out var port))
|
if (!Ports.TryGetValue(portHandle, out var port))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
|
|
@ -622,14 +721,38 @@ public static class AudioOut2Exports
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||||
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
|
byte[]? pcmData = null;
|
||||||
|
if (pcmAddress != 0 &&
|
||||||
|
TryDecodeDataFormat(port.DataFormat, out var channels, out var bytesPerSample, out _) &&
|
||||||
|
IsSnapshotPcmPort(port.PortType, channels))
|
||||||
|
{
|
||||||
|
int byteLength;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byteLength = checked((int)port.GrainSamples * channels * bytesPerSample);
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
pcmData = new byte[byteLength];
|
||||||
|
if (!ctx.Memory.TryRead(pcmAddress, pcmData))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
port.PcmAddress = pcmAddress;
|
||||||
|
Volatile.Write(ref port.PcmData, pcmData);
|
||||||
var n = Interlocked.Increment(ref _attributePcmTraceCount);
|
var n = Interlocked.Increment(ref _attributePcmTraceCount);
|
||||||
if (n <= 8 || n % 500 == 0)
|
if (n <= 8 || n % 500 == 0)
|
||||||
{
|
{
|
||||||
TraceAudioOut2(
|
TraceAudioOut2(
|
||||||
$"port-set-pcm#{n} port=0x{portHandle:X} pcm=0x{port.PcmAddress:X} " +
|
$"port-set-pcm#{n} port=0x{portHandle:X} pcm=0x{port.PcmAddress:X} " +
|
||||||
$"format=0x{port.DataFormat:X} grains={port.GrainSamples}");
|
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
|
||||||
|
$"grains={port.GrainSamples}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -645,10 +768,10 @@ public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
// ABI: sceAudioOut2PortCreate(SceAudioOut2ContextHandle hCtx,
|
// ABI: sceAudioOut2PortCreate(SceAudioOut2ContextHandle hCtx,
|
||||||
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
|
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
|
||||||
// rdx is the only out pointer.
|
|
||||||
var contextHandle = ctx[CpuRegister.Rdi];
|
var contextHandle = ctx[CpuRegister.Rdi];
|
||||||
var paramAddress = ctx[CpuRegister.Rsi];
|
var paramAddress = ctx[CpuRegister.Rsi];
|
||||||
var outPortAddress = ctx[CpuRegister.Rdx];
|
var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||||
|
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
|
||||||
if (outPortAddress == 0)
|
if (outPortAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||||
|
|
@ -669,6 +792,14 @@ public static class AudioOut2Exports
|
||||||
Span<byte> param = stackalloc byte[PortParamSize];
|
Span<byte> param = stackalloc byte[PortParamSize];
|
||||||
if (ctx.Memory.TryRead(paramAddress, param))
|
if (ctx.Memory.TryRead(paramAddress, param))
|
||||||
{
|
{
|
||||||
|
if (traceCreate <= 8 || traceCreate % 500 == 0)
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-create-param#{traceCreate} context=0x{contextHandle:X} " +
|
||||||
|
$"address=0x{paramAddress:X} out=0x{outPortAddress:X} " +
|
||||||
|
$"bytes={Convert.ToHexString(param)}");
|
||||||
|
}
|
||||||
|
|
||||||
portType = BinaryPrimitives.ReadUInt16LittleEndian(param);
|
portType = BinaryPrimitives.ReadUInt16LittleEndian(param);
|
||||||
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
||||||
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
||||||
|
|
@ -719,13 +850,16 @@ public static class AudioOut2Exports
|
||||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsGuestStackAddress(stateAddress) &&
|
||||||
|
!(AllowStackOut("portstate") && Ports.ContainsKey(portHandle)))
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-get-state skip-stack handle=0x{portHandle:X} state=0x{stateAddress:X}");
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
Span<byte> state = stackalloc byte[PortStateSize];
|
Span<byte> state = stackalloc byte[PortStateSize];
|
||||||
state.Clear();
|
state.Clear();
|
||||||
// +0x00 u16 output = OUTPUT_PRIMARY (1)
|
|
||||||
// +0x02 u8 numChannels = from port format when known, else 2
|
|
||||||
// +0x04 s16 volume = -1 (not applicable outside PADSPK)
|
|
||||||
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
|
|
||||||
// +0x08 u32 flags = 0 (no 3D, not mono-forced)
|
|
||||||
byte channels = 2;
|
byte channels = 2;
|
||||||
if (Ports.TryGetValue(portHandle, out var port) &&
|
if (Ports.TryGetValue(portHandle, out var port) &&
|
||||||
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
|
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
|
||||||
|
|
@ -735,15 +869,21 @@ public static class AudioOut2Exports
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
||||||
state[0x02] = channels;
|
state[0x02] = channels;
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
if (!ctx.Memory.TryWrite(stateAddress, state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAudioOut2(
|
var traceState = Interlocked.Increment(ref _portStateTraceCount);
|
||||||
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
|
if (traceState <= 8 || traceState % 500 == 0)
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-get-state#{traceState} handle=0x{portHandle:X} state=0x{stateAddress:X} " +
|
||||||
|
$"bytes={Convert.ToHexString(state)}");
|
||||||
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -972,7 +1112,17 @@ public static class AudioOut2Exports
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2PortDestroy(CpuContext ctx)
|
public static int AudioOut2PortDestroy(CpuContext ctx)
|
||||||
{
|
{
|
||||||
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
|
var portHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var removed = Ports.TryRemove(portHandle, out var port);
|
||||||
|
var n = Interlocked.Increment(ref _portDestroyTraceCount);
|
||||||
|
if (n <= 8 || n % 500 == 0)
|
||||||
|
{
|
||||||
|
TraceAudioOut2(
|
||||||
|
$"port-destroy#{n} handle=0x{portHandle:X} removed={removed} " +
|
||||||
|
$"context=0x{port?.ContextHandle ?? 0:X} pcm=0x{port?.PcmAddress ?? 0:X} " +
|
||||||
|
$"remaining={Ports.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1064,13 +1214,20 @@ public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var audio = HostPlatform.Current.Audio;
|
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||||
// Deeper host queue than classic AudioOut: FMOD's bursty
|
if (streamFactory is not null)
|
||||||
// AudioOut2 Push pattern underran a 32 KiB (~171 ms) bed.
|
{
|
||||||
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
PrimaryBackend = streamFactory(context.Frequency);
|
||||||
context.Frequency,
|
PrimaryBackendName = PrimaryBackend is null ? "silent" : "test-primary";
|
||||||
maxQueuedPcmBytes: 128 * 1024);
|
}
|
||||||
PrimaryBackendName = audio.BackendName + "-primary";
|
else
|
||||||
|
{
|
||||||
|
var audio = HostPlatform.Current.Audio;
|
||||||
|
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
||||||
|
context.Frequency,
|
||||||
|
maxQueuedPcmBytes: 128 * 1024);
|
||||||
|
PrimaryBackendName = audio.BackendName + "-primary";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|
@ -1088,11 +1245,20 @@ public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var audio = HostPlatform.Current.Audio;
|
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
if (streamFactory is not null)
|
||||||
context.Frequency,
|
{
|
||||||
maxQueuedPcmBytes: 128 * 1024);
|
SecondaryBackend = streamFactory(context.Frequency);
|
||||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
SecondaryBackendName = SecondaryBackend is null ? "silent" : "test-secondary";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var audio = HostPlatform.Current.Audio;
|
||||||
|
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||||
|
context.Frequency,
|
||||||
|
maxQueuedPcmBytes: 128 * 1024);
|
||||||
|
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|
@ -1155,7 +1321,6 @@ public static class AudioOut2Exports
|
||||||
lock (HostSubmitGate)
|
lock (HostSubmitGate)
|
||||||
{
|
{
|
||||||
var mix = ArrayPool<float>.Shared.Rent(frames * 2);
|
var mix = ArrayPool<float>.Shared.Rent(frames * 2);
|
||||||
var source = ArrayPool<byte>.Shared.Rent(frames * 16 * sizeof(float));
|
|
||||||
var output = ArrayPool<byte>.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize);
|
var output = ArrayPool<byte>.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -1164,29 +1329,42 @@ public static class AudioOut2Exports
|
||||||
foreach (var port in Ports.Values)
|
foreach (var port in Ports.Values)
|
||||||
{
|
{
|
||||||
if (port.ContextHandle != context.Handle ||
|
if (port.ContextHandle != context.Handle ||
|
||||||
port.PcmAddress == 0 ||
|
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
|
||||||
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
|
{
|
||||||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat) ||
|
continue;
|
||||||
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
|
}
|
||||||
out var leftGain, out var rightGain))
|
|
||||||
|
var pcmData = TakePortPcmSnapshot(port);
|
||||||
|
if (pcmData is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetStereoGains(
|
||||||
|
port.PortType,
|
||||||
|
ch,
|
||||||
|
port.Gain,
|
||||||
|
port.Ambisonics,
|
||||||
|
port.Passthrough,
|
||||||
|
out var leftGain,
|
||||||
|
out var rightGain))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var byteLength = checked(frames * ch * bps);
|
var byteLength = checked(frames * ch * bps);
|
||||||
if (byteLength <= 0 || byteLength > source.Length)
|
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceSpan = source.AsSpan(0, byteLength);
|
if (pcmData.Length < byteLength)
|
||||||
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MixPortIntoStereo(
|
MixPortIntoStereo(
|
||||||
sourceSpan,
|
pcmData.AsSpan(0, byteLength),
|
||||||
mix.AsSpan(0, frames * 2),
|
mix.AsSpan(0, frames * 2),
|
||||||
frames,
|
frames,
|
||||||
ch,
|
ch,
|
||||||
|
|
@ -1238,12 +1416,70 @@ public static class AudioOut2Exports
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ArrayPool<float>.Shared.Return(mix);
|
ArrayPool<float>.Shared.Return(mix);
|
||||||
ArrayPool<byte>.Shared.Return(source);
|
|
||||||
ArrayPool<byte>.Shared.Return(output);
|
ArrayPool<byte>.Shared.Return(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static byte[]? GetPortPcmSnapshotForTests(ulong portHandle)
|
||||||
|
{
|
||||||
|
if (!Ports.TryGetValue(portHandle, out var port))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = Volatile.Read(ref port.PcmData);
|
||||||
|
return snapshot?.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasPortForTests(ulong portHandle) => Ports.ContainsKey(portHandle);
|
||||||
|
|
||||||
|
internal static byte[]? TakePortPcmSnapshotForTests(ulong portHandle)
|
||||||
|
{
|
||||||
|
return Ports.TryGetValue(portHandle, out var port)
|
||||||
|
? TakePortPcmSnapshot(port)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[]? TakePortPcmSnapshot(PortState port) =>
|
||||||
|
Interlocked.Exchange(ref port.PcmData, null);
|
||||||
|
|
||||||
|
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
|
||||||
|
Volatile.Write(ref _streamFactoryForTests, streamFactory);
|
||||||
|
|
||||||
|
internal static void ResetForTests()
|
||||||
|
{
|
||||||
|
Contexts.Clear();
|
||||||
|
Ports.Clear();
|
||||||
|
SpeakerArrays.Clear();
|
||||||
|
Interlocked.Exchange(ref _nextContextHandle, 1);
|
||||||
|
Interlocked.Exchange(ref _nextUserHandle, 1);
|
||||||
|
Interlocked.Exchange(ref _nextPortId, 0);
|
||||||
|
Interlocked.Exchange(ref _pushTraceCount, 0);
|
||||||
|
Interlocked.Exchange(ref _submitTraceCount, 0);
|
||||||
|
Interlocked.Exchange(ref _submitSkipTraceCount, 0);
|
||||||
|
Interlocked.Exchange(ref _attributePcmTraceCount, 0);
|
||||||
|
|
||||||
|
lock (HostBackendGate)
|
||||||
|
{
|
||||||
|
PrimaryBackend?.Dispose();
|
||||||
|
SecondaryBackend?.Dispose();
|
||||||
|
PrimaryBackend = null;
|
||||||
|
SecondaryBackend = null;
|
||||||
|
PrimaryBackendName = "none";
|
||||||
|
SecondaryBackendName = "none";
|
||||||
|
PrimaryContextHandle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Volatile.Write(ref _streamFactoryForTests, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMainOrBgmPort(ushort portType)
|
||||||
|
{
|
||||||
|
var kind = portType & 0xFF;
|
||||||
|
return kind is 0 or 1;
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool RoutesToSpeakers(ushort portType) => (portType & 0xFF) is 0 or 1 or 2;
|
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) =>
|
internal static (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) =>
|
||||||
|
|
@ -1298,6 +1534,11 @@ public static class AudioOut2Exports
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsSnapshotPcmPort(ushort portType, int channels) =>
|
||||||
|
RoutesToSpeakers(portType) &&
|
||||||
|
(portType & 0xFF) != AudioOut2PortTypeVibration &&
|
||||||
|
(!IsObjectPort(portType) || channels == 1);
|
||||||
|
|
||||||
internal static void MixPortIntoStereo(
|
internal static void MixPortIntoStereo(
|
||||||
ReadOnlySpan<byte> source,
|
ReadOnlySpan<byte> source,
|
||||||
Span<float> mix,
|
Span<float> mix,
|
||||||
|
|
@ -1453,13 +1694,19 @@ public static class AudioOut2Exports
|
||||||
private static bool IsDirectMemoryWindowAddress(ulong value) =>
|
private static bool IsDirectMemoryWindowAddress(ulong value) =>
|
||||||
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
|
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
|
||||||
|
|
||||||
// Only sceAudioOut2ContextQueryMemory and sceAudioOut2ContextGetQueueLevel still
|
|
||||||
// pick an out buffer by host address range; both are left untouched here so they do
|
|
||||||
// not collide with the changes already in flight for them, and these two predicates
|
|
||||||
// go away with the last of their callers.
|
|
||||||
private static bool IsGuestStackAddress(ulong value) =>
|
private static bool IsGuestStackAddress(ulong value) =>
|
||||||
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
|
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
|
||||||
|
|
||||||
|
private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary)
|
||||||
|
{
|
||||||
|
if (IsWritableOutBuffer(primary))
|
||||||
|
{
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsWritableOutBuffer(secondary) ? secondary : 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsWritableOutBuffer(ulong value) =>
|
private static bool IsWritableOutBuffer(ulong value) =>
|
||||||
value != 0 &&
|
value != 0 &&
|
||||||
value != 0x10000UL &&
|
value != 0x10000UL &&
|
||||||
|
|
|
||||||
|
|
@ -881,6 +881,32 @@ public static class AvPlayerExports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "N6Oy-EjduiY",
|
||||||
|
ExportName = "sceAvPlayerSetAvailableBandwidth",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAvPlayer")]
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+7xJ+lFxmkQ",
|
||||||
|
ExportName = "sceAvPlayerSetAvailableBW",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAvPlayer")]
|
||||||
|
public static int AvPlayerSetAvailableBandwidth(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var availableBandwidth = ctx[CpuRegister.Rsi];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Players.ContainsKey(handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, InvalidParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace($"set_available_bandwidth handle=0x{handle:X16} bandwidth={availableBandwidth}");
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ODJK2sn9w4A",
|
Nid = "ODJK2sn9w4A",
|
||||||
ExportName = "sceAvPlayerEnableStream",
|
ExportName = "sceAvPlayerEnableStream",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Headless lifecycle for the system web-browser dialog. SharpEmu does not yet
|
||||||
|
/// provide a host browser, so an opened dialog completes on its first status
|
||||||
|
/// poll instead of leaving games in an infinite RUNNING loop.
|
||||||
|
/// </summary>
|
||||||
|
public static class WebBrowserDialogExports
|
||||||
|
{
|
||||||
|
internal const int StatusNone = 0;
|
||||||
|
internal const int StatusInitialized = 1;
|
||||||
|
internal const int StatusRunning = 2;
|
||||||
|
internal const int StatusFinished = 3;
|
||||||
|
|
||||||
|
private const int ErrorOk = 0;
|
||||||
|
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||||
|
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||||
|
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||||
|
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||||
|
|
||||||
|
private static int _status;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jqb7HntFQFc",
|
||||||
|
ExportName = "sceWebBrowserDialogInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "FraP7debcdg",
|
||||||
|
ExportName = "sceWebBrowserDialogOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogOpen(CpuContext ctx) => Open(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "O7dIZQrwVFY",
|
||||||
|
ExportName = "sceWebBrowserDialogOpenForPredeterminedContent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogOpenForPredeterminedContent(CpuContext ctx) => Open(ctx);
|
||||||
|
|
||||||
|
private static int Open(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorArgNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Volatile.Read(ref _status) == StatusNone)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _status, StatusRunning);
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CFTG6a8TjOU",
|
||||||
|
ExportName = "sceWebBrowserDialogGetStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "h1dR-t5ISgg",
|
||||||
|
ExportName = "sceWebBrowserDialogUpdateStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||||
|
|
||||||
|
private static int PollStatus()
|
||||||
|
{
|
||||||
|
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||||
|
return Volatile.Read(ref _status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PSK+Eik919Q",
|
||||||
|
ExportName = "sceWebBrowserDialogClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "vCaW0fgVQmc",
|
||||||
|
ExportName = "sceWebBrowserDialogGetResult",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogGetResult(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorArgNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Volatile.Read(ref _status) != StatusFinished)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||||
|
result.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(ctx[CpuRegister.Rdi], result))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Wit4LjeoeX4",
|
||||||
|
ExportName = "sceWebBrowserDialogGetEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogGetEvent(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uYELOMVnmNQ",
|
||||||
|
ExportName = "sceWebBrowserDialogNavigate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogNavigate(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RLhKBOoNyXY",
|
||||||
|
ExportName = "sceWebBrowserDialogSetZoom",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogSetZoom(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ocHtyBwHfys",
|
||||||
|
ExportName = "sceWebBrowserDialogTerminate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceWebBrowserDialog")]
|
||||||
|
public static int WebBrowserDialogTerminate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int StatusForTests => Volatile.Read(ref _status);
|
||||||
|
|
||||||
|
internal static void ResetForTests() => Interlocked.Exchange(ref _status, StatusNone);
|
||||||
|
}
|
||||||
|
|
@ -168,6 +168,14 @@ public static class JsonExports
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "GvGvswb0v34",
|
||||||
|
ExportName = "_ZN3sce4Json14InitParameter2C2Ev",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int InitParameter2BaseConstructor(CpuContext ctx) =>
|
||||||
|
InitParameter2Constructor(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "I2QC8PYhJWY",
|
Nid = "I2QC8PYhJWY",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||||
|
|
@ -193,6 +201,14 @@ public static class JsonExports
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "W72B9ylU2JA",
|
||||||
|
ExportName = "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int InitParameterRtti2SetAllocatorRtti(CpuContext ctx) =>
|
||||||
|
InitParameter2SetAllocator(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Eu95jmqn5Rw",
|
Nid = "Eu95jmqn5Rw",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||||
|
|
|
||||||
|
|
@ -12,29 +12,82 @@ public static class Http2Exports
|
||||||
private const int Http2ErrorInvalidVersion = unchecked((int)0x817B106A);
|
private const int Http2ErrorInvalidVersion = unchecked((int)0x817B106A);
|
||||||
private const int Http2ErrorInvalidId = unchecked((int)0x817B1100);
|
private const int Http2ErrorInvalidId = unchecked((int)0x817B1100);
|
||||||
private const int Http2ErrorInvalidValue = unchecked((int)0x817B11FE);
|
private const int Http2ErrorInvalidValue = unchecked((int)0x817B11FE);
|
||||||
|
private const int Http2ErrorNullPointer = unchecked((int)0x817B1225);
|
||||||
private const int Http2ErrorProhibited = unchecked((int)0x817B5224);
|
private const int Http2ErrorProhibited = unchecked((int)0x817B5224);
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<int, Http2Context> Contexts = new();
|
private static readonly ConcurrentDictionary<int, Http2Context> Contexts = new();
|
||||||
private static readonly ConcurrentDictionary<int, Http2Template> Templates = new();
|
private static readonly ConcurrentDictionary<int, Http2Template> Templates = new();
|
||||||
private static readonly ConcurrentDictionary<int, Http2Request> Requests = new();
|
private static readonly ConcurrentDictionary<int, Http2Request> Requests = new();
|
||||||
|
private static readonly ConcurrentDictionary<int, Http2CookieBox> CookieBoxes = new();
|
||||||
private static int _nextContextId;
|
private static int _nextContextId;
|
||||||
private static int _nextTemplateId = 0x1000;
|
private static int _nextTemplateId = 0x1000;
|
||||||
private static int _nextRequestId = 0x2000;
|
private static int _nextRequestId = 0x2000;
|
||||||
|
private static int _nextCookieBoxId = 0x3000;
|
||||||
|
|
||||||
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
|
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
|
||||||
|
|
||||||
private sealed record Http2Template(int ContextId, string UserAgent, int HttpVersion, bool AutoProxyConfig);
|
private sealed record Http2CookieBox;
|
||||||
|
|
||||||
private sealed record Http2Request(int TemplateId, string Method, string Url, ulong ContentLength);
|
private sealed record Http2Options(
|
||||||
|
uint MinimumSslVersion,
|
||||||
|
uint SslOptions,
|
||||||
|
ulong RedirectCallback,
|
||||||
|
ulong RedirectUserArgument,
|
||||||
|
bool AutoRedirect,
|
||||||
|
int CookieBoxId,
|
||||||
|
uint ConnectTimeoutMicroseconds,
|
||||||
|
uint ReceiveTimeoutMicroseconds,
|
||||||
|
uint SendTimeoutMicroseconds,
|
||||||
|
bool AuthEnabled,
|
||||||
|
ulong SslCallback,
|
||||||
|
ulong SslUserArgument)
|
||||||
|
{
|
||||||
|
public static readonly Http2Options Default = new(0, 0, 0, 0, true, 0, 0, 0, 0, false, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record Http2Header(string Name, string Value, uint Mode);
|
||||||
|
|
||||||
|
private sealed record Http2Template(
|
||||||
|
int ContextId,
|
||||||
|
string UserAgent,
|
||||||
|
int HttpVersion,
|
||||||
|
bool AutoProxyConfig,
|
||||||
|
Http2Options Options);
|
||||||
|
|
||||||
|
private sealed record Http2Request(
|
||||||
|
int TemplateId,
|
||||||
|
string Method,
|
||||||
|
string Url,
|
||||||
|
ulong ContentLength,
|
||||||
|
Http2Options Options,
|
||||||
|
Http2Header[] Headers);
|
||||||
|
|
||||||
|
internal readonly record struct Http2OptionsSnapshot(
|
||||||
|
uint MinimumSslVersion,
|
||||||
|
uint SslOptions,
|
||||||
|
ulong RedirectCallback,
|
||||||
|
ulong RedirectUserArgument,
|
||||||
|
bool AutoRedirect,
|
||||||
|
int CookieBoxId,
|
||||||
|
uint ConnectTimeoutMicroseconds,
|
||||||
|
uint ReceiveTimeoutMicroseconds,
|
||||||
|
uint SendTimeoutMicroseconds,
|
||||||
|
bool AuthEnabled,
|
||||||
|
ulong SslCallback,
|
||||||
|
ulong SslUserArgument);
|
||||||
|
|
||||||
|
internal readonly record struct Http2HeaderSnapshot(string Name, string Value, uint Mode);
|
||||||
|
|
||||||
public static void ResetRuntimeState()
|
public static void ResetRuntimeState()
|
||||||
{
|
{
|
||||||
Contexts.Clear();
|
Contexts.Clear();
|
||||||
Templates.Clear();
|
Templates.Clear();
|
||||||
Requests.Clear();
|
Requests.Clear();
|
||||||
|
CookieBoxes.Clear();
|
||||||
_nextContextId = 0;
|
_nextContextId = 0;
|
||||||
_nextTemplateId = 0x1000;
|
_nextTemplateId = 0x1000;
|
||||||
_nextRequestId = 0x2000;
|
_nextRequestId = 0x2000;
|
||||||
|
_nextCookieBoxId = 0x3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|
@ -119,7 +172,12 @@ public static class Http2Exports
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = Interlocked.Increment(ref _nextTemplateId);
|
var id = Interlocked.Increment(ref _nextTemplateId);
|
||||||
Templates[id] = new Http2Template(contextId, userAgent, httpVersion, autoProxyConfig != 0);
|
Templates[id] = new Http2Template(
|
||||||
|
contextId,
|
||||||
|
userAgent,
|
||||||
|
httpVersion,
|
||||||
|
autoProxyConfig != 0,
|
||||||
|
Http2Options.Default);
|
||||||
TraceHttp2("create_template", id, unchecked((ulong)contextId), ctx[CpuRegister.Rsi], unchecked((ulong)httpVersion), unchecked((ulong)autoProxyConfig));
|
TraceHttp2("create_template", id, unchecked((ulong)contextId), ctx[CpuRegister.Rsi], unchecked((ulong)httpVersion), unchecked((ulong)autoProxyConfig));
|
||||||
return ctx.SetReturn(id);
|
return ctx.SetReturn(id);
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +198,175 @@ public static class Http2Exports
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "09tk+kIA1Ns",
|
||||||
|
ExportName = "sceHttp2SetMinSslVersion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetMinSslVersion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var minimumSslVersion = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
return TryUpdateOptions(id, options => options with { MinimumSslVersion = minimumSslVersion })
|
||||||
|
? TraceAndReturn(ctx, "set_min_ssl_version", id, minimumSslVersion, 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "B37SruheQ5Y",
|
||||||
|
ExportName = "sceHttp2SslDisableOption",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SslDisableOption(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions & ~flags })
|
||||||
|
? TraceAndReturn(ctx, "ssl_disable_option", id, flags, 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "EWcwMpbr5F8",
|
||||||
|
ExportName = "sceHttp2SslEnableOption",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SslEnableOption(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions | flags })
|
||||||
|
? TraceAndReturn(ctx, "ssl_enable_option", id, flags, 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BJgi0CH7al4",
|
||||||
|
ExportName = "sceHttp2SetRedirectCallback",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetRedirectCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var callback = ctx[CpuRegister.Rsi];
|
||||||
|
var userArgument = ctx[CpuRegister.Rdx];
|
||||||
|
return TryUpdateOptions(
|
||||||
|
id,
|
||||||
|
options => options with
|
||||||
|
{
|
||||||
|
RedirectCallback = callback,
|
||||||
|
RedirectUserArgument = userArgument,
|
||||||
|
})
|
||||||
|
? TraceAndReturn(ctx, "set_redirect_callback", id, callback, userArgument, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "b9AvoIaOuHI",
|
||||||
|
ExportName = "sceHttp2SetAutoRedirect",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetAutoRedirect(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
|
||||||
|
return TryUpdateOptions(id, options => options with { AutoRedirect = enabled })
|
||||||
|
? TraceAndReturn(ctx, "set_auto_redirect", id, enabled ? 1UL : 0UL, 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "N4UfjvWJsMw",
|
||||||
|
ExportName = "sceHttp2CreateCookieBox",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2CreateCookieBox(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = Interlocked.Increment(ref _nextCookieBoxId);
|
||||||
|
CookieBoxes[id] = new Http2CookieBox();
|
||||||
|
TraceHttp2("create_cookie_box", id, 0, 0, 0, 0);
|
||||||
|
return ctx.SetReturn(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jrVHsKCXA0g",
|
||||||
|
ExportName = "sceHttp2SetCookieBox",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetCookieBox(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var cookieBoxId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (cookieBoxId != 0 && !CookieBoxes.ContainsKey(cookieBoxId))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryUpdateOptions(id, options => options with { CookieBoxId = cookieBoxId })
|
||||||
|
? TraceAndReturn(ctx, "set_cookie_box", id, unchecked((ulong)cookieBoxId), 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-HIO4VT87v8",
|
||||||
|
ExportName = "sceHttp2SetConnectTimeOut",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetConnectTimeOut(CpuContext ctx) =>
|
||||||
|
SetTimeout(ctx, static (options, value) => options with { ConnectTimeoutMicroseconds = value }, "connect");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "izvHhqgDt44",
|
||||||
|
ExportName = "sceHttp2SetRecvTimeOut",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetRecvTimeOut(CpuContext ctx) =>
|
||||||
|
SetTimeout(ctx, static (options, value) => options with { ReceiveTimeoutMicroseconds = value }, "recv");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XPtW45xiLHk",
|
||||||
|
ExportName = "sceHttp2SetSendTimeOut",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetSendTimeOut(CpuContext ctx) =>
|
||||||
|
SetTimeout(ctx, static (options, value) => options with { SendTimeoutMicroseconds = value }, "send");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jjFahkBPCYs",
|
||||||
|
ExportName = "sceHttp2SetAuthEnabled",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetAuthEnabled(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
|
||||||
|
return TryUpdateOptions(id, options => options with { AuthEnabled = enabled })
|
||||||
|
? TraceAndReturn(ctx, "set_auth_enabled", id, enabled ? 1UL : 0UL, 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "YrWX+DhPHQY",
|
||||||
|
ExportName = "sceHttp2SetSslCallback",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetSslCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var callback = ctx[CpuRegister.Rsi];
|
||||||
|
var userArgument = ctx[CpuRegister.Rdx];
|
||||||
|
return TryUpdateOptions(
|
||||||
|
id,
|
||||||
|
options => options with
|
||||||
|
{
|
||||||
|
SslCallback = callback,
|
||||||
|
SslUserArgument = userArgument,
|
||||||
|
})
|
||||||
|
? TraceAndReturn(ctx, "set_ssl_callback", id, callback, userArgument, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
ExportName = "sceHttp2CreateRequestWithURL",
|
ExportName = "sceHttp2CreateRequestWithURL",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
|
|
@ -147,7 +374,7 @@ public static class Http2Exports
|
||||||
public static int Http2CreateRequestWithUrl(CpuContext ctx)
|
public static int Http2CreateRequestWithUrl(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!Templates.ContainsKey(templateId))
|
if (!Templates.TryGetValue(templateId, out var template))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
}
|
}
|
||||||
|
|
@ -159,11 +386,73 @@ public static class Http2Exports
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = Interlocked.Increment(ref _nextRequestId);
|
var id = Interlocked.Increment(ref _nextRequestId);
|
||||||
Requests[id] = new Http2Request(templateId, method, url, ctx[CpuRegister.Rcx]);
|
Requests[id] = new Http2Request(
|
||||||
|
templateId,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
ctx[CpuRegister.Rcx],
|
||||||
|
template.Options,
|
||||||
|
[]);
|
||||||
TraceHttp2("create_request", id, unchecked((ulong)templateId), ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
TraceHttp2("create_request", id, unchecked((ulong)templateId), ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||||
return ctx.SetReturn(id);
|
return ctx.SetReturn(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "nrPfOE8TQu0",
|
||||||
|
ExportName = "sceHttp2AddRequestHeader",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2AddRequestHeader(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var mode = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||||
|
if (nameAddress == 0 || valueAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(Http2ErrorNullPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadUtf8Z(ctx, nameAddress, 4096, out var name) ||
|
||||||
|
!TryReadUtf8Z(ctx, valueAddress, 16384, out var value))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(Http2ErrorInvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (Requests.TryGetValue(requestId, out var request))
|
||||||
|
{
|
||||||
|
var headers = new Http2Header[request.Headers.Length + 1];
|
||||||
|
Array.Copy(request.Headers, headers, request.Headers.Length);
|
||||||
|
headers[^1] = new Http2Header(name, value, mode);
|
||||||
|
if (Requests.TryUpdate(requestId, request with { Headers = headers }, request))
|
||||||
|
{
|
||||||
|
return TraceAndReturn(ctx, "add_request_header", requestId, nameAddress, valueAddress, mode, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "FSAFOzi0FpM",
|
||||||
|
ExportName = "sceHttp2SetRequestContentLength",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2SetRequestContentLength(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var contentLength = ctx[CpuRegister.Rsi];
|
||||||
|
while (Requests.TryGetValue(requestId, out var request))
|
||||||
|
{
|
||||||
|
if (Requests.TryUpdate(requestId, request with { ContentLength = contentLength }, request))
|
||||||
|
{
|
||||||
|
return TraceAndReturn(ctx, "set_request_content_length", requestId, contentLength, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
ExportName = "sceHttp2DeleteRequest",
|
ExportName = "sceHttp2DeleteRequest",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
|
|
@ -202,6 +491,100 @@ public static class Http2Exports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SetTimeout(
|
||||||
|
CpuContext ctx,
|
||||||
|
Func<Http2Options, uint, Http2Options> update,
|
||||||
|
string timeoutName)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var microseconds = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
return TryUpdateOptions(id, options => update(options, microseconds))
|
||||||
|
? TraceAndReturn(ctx, $"set_{timeoutName}_timeout", id, microseconds, 0, 0, 0)
|
||||||
|
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryUpdateOptions(int id, Func<Http2Options, Http2Options> update)
|
||||||
|
{
|
||||||
|
while (Templates.TryGetValue(id, out var template))
|
||||||
|
{
|
||||||
|
if (Templates.TryUpdate(id, template with { Options = update(template.Options) }, template))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (Requests.TryGetValue(id, out var request))
|
||||||
|
{
|
||||||
|
if (Requests.TryUpdate(id, request with { Options = update(request.Options) }, request))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryGetOptionsForTests(int id, out Http2OptionsSnapshot snapshot)
|
||||||
|
{
|
||||||
|
var options = Templates.TryGetValue(id, out var template)
|
||||||
|
? template.Options
|
||||||
|
: Requests.TryGetValue(id, out var request)
|
||||||
|
? request.Options
|
||||||
|
: null;
|
||||||
|
if (options is null)
|
||||||
|
{
|
||||||
|
snapshot = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot = new Http2OptionsSnapshot(
|
||||||
|
options.MinimumSslVersion,
|
||||||
|
options.SslOptions,
|
||||||
|
options.RedirectCallback,
|
||||||
|
options.RedirectUserArgument,
|
||||||
|
options.AutoRedirect,
|
||||||
|
options.CookieBoxId,
|
||||||
|
options.ConnectTimeoutMicroseconds,
|
||||||
|
options.ReceiveTimeoutMicroseconds,
|
||||||
|
options.SendTimeoutMicroseconds,
|
||||||
|
options.AuthEnabled,
|
||||||
|
options.SslCallback,
|
||||||
|
options.SslUserArgument);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Http2HeaderSnapshot[] GetRequestHeadersForTests(int requestId) =>
|
||||||
|
Requests.TryGetValue(requestId, out var request)
|
||||||
|
? request.Headers
|
||||||
|
.Select(static header => new Http2HeaderSnapshot(header.Name, header.Value, header.Mode))
|
||||||
|
.ToArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
internal static bool TryGetRequestContentLengthForTests(int requestId, out ulong contentLength)
|
||||||
|
{
|
||||||
|
if (Requests.TryGetValue(requestId, out var request))
|
||||||
|
{
|
||||||
|
contentLength = request.ContentLength;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentLength = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TraceAndReturn(
|
||||||
|
CpuContext ctx,
|
||||||
|
string operation,
|
||||||
|
int id,
|
||||||
|
ulong arg0,
|
||||||
|
ulong arg1,
|
||||||
|
ulong arg2,
|
||||||
|
ulong arg3)
|
||||||
|
{
|
||||||
|
TraceHttp2(operation, id, arg0, arg1, arg2, arg3);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
{
|
{
|
||||||
value = string.Empty;
|
value = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,10 @@ public static class SaveDataExports
|
||||||
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
|
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
|
||||||
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
|
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
|
||||||
// SceSaveDataEventType
|
// SceSaveDataEventType
|
||||||
|
private const uint EventTypeBackupEnd = 2;
|
||||||
private const uint EventTypeSaveDataMemorySyncEnd = 3;
|
private const uint EventTypeSaveDataMemorySyncEnd = 3;
|
||||||
private const int SaveDataEventSize = 0x60;
|
private const int SaveDataTitleIdStructSize = 0x10;
|
||||||
|
private const int SaveDataEventSize = 0x68;
|
||||||
private const int MountInfoSize = 0x40;
|
private const int MountInfoSize = 0x40;
|
||||||
private const uint SaveDataBlockSize = 65536;
|
private const uint SaveDataBlockSize = 65536;
|
||||||
private const ulong SaveDataBlocksMax = 16384;
|
private const ulong SaveDataBlocksMax = 16384;
|
||||||
|
|
@ -86,16 +88,79 @@ public static class SaveDataExports
|
||||||
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
|
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
|
||||||
private static readonly Dictionary<string, MountEntry> _mounts = new(StringComparer.Ordinal);
|
private static readonly Dictionary<string, MountEntry> _mounts = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
private readonly record struct SaveDataEvent(uint Type, int ErrorCode, int UserId, string DirName);
|
private readonly record struct SaveDataEvent(
|
||||||
|
uint Type,
|
||||||
|
int ErrorCode,
|
||||||
|
int UserId,
|
||||||
|
string TitleId,
|
||||||
|
string DirName);
|
||||||
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
|
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
|
||||||
|
|
||||||
private static void EnqueueEvent(uint type, int userId, string dirName, int errorCode = 0)
|
private static void EnqueueEvent(
|
||||||
|
uint type,
|
||||||
|
int userId,
|
||||||
|
string titleId,
|
||||||
|
string dirName,
|
||||||
|
int errorCode = 0)
|
||||||
{
|
{
|
||||||
lock (_eventGate)
|
lock (_eventGate)
|
||||||
{
|
{
|
||||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, dirName));
|
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, titleId, dirName));
|
||||||
}
|
}
|
||||||
TraceSaveData($"event.enqueue type={type} user={userId} dir='{dirName}' err=0x{errorCode:X}");
|
TraceSaveData($"event.enqueue type={type} user={userId} title='{titleId}' dir='{dirName}' err=0x{errorCode:X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "z1JA8-iJt3k",
|
||||||
|
ExportName = "sceSaveDataBackup",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataBackup(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var backupAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (backupAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadInt32(ctx, backupAddress, out var userId) ||
|
||||||
|
!ctx.TryReadUInt64(backupAddress + 0x08, out var titleIdAddress) ||
|
||||||
|
!ctx.TryReadUInt64(backupAddress + 0x10, out var dirNameAddress))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId < 0 || dirNameAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(dirName))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
string titleId;
|
||||||
|
if (titleIdAddress == 0)
|
||||||
|
{
|
||||||
|
titleId = ResolveConfiguredTitleId();
|
||||||
|
}
|
||||||
|
else if (!TryReadFixedAscii(ctx, titleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(titleId))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnqueueEvent(EventTypeBackupEnd, userId, titleId, dirName);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|
@ -131,7 +196,14 @@ public static class SaveDataExports
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
|
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
|
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
|
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
|
||||||
WriteAscii(ev.Slice(0x10, SaveDataDirNameSize), pending.DirName);
|
WriteFixedAscii(
|
||||||
|
ev.Slice(0x10, SaveDataTitleIdStructSize),
|
||||||
|
pending.TitleId,
|
||||||
|
SaveDataTitleIdSize);
|
||||||
|
WriteFixedAscii(
|
||||||
|
ev.Slice(0x20, SaveDataDirNameSize),
|
||||||
|
pending.DirName,
|
||||||
|
SaveDataDirNameSize);
|
||||||
if (!ctx.Memory.TryWrite(eventAddress, ev))
|
if (!ctx.Memory.TryWrite(eventAddress, ev))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
|
@ -617,6 +689,11 @@ public static class SaveDataExports
|
||||||
ExportName = "sceSaveDataDirNameSearchPs4",
|
ExportName = "sceSaveDataDirNameSearchPs4",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PHnuI4LhuRk",
|
||||||
|
ExportName = "sceSaveDataDirNameSearch2",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataDirNameSearch(CpuContext ctx)
|
public static int SaveDataDirNameSearch(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var condAddress = ctx[CpuRegister.Rdi];
|
var condAddress = ctx[CpuRegister.Rdi];
|
||||||
|
|
@ -1241,6 +1318,19 @@ public static class SaveDataExports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WriteFixedAscii(
|
||||||
|
Span<byte> destination,
|
||||||
|
string value,
|
||||||
|
int maxContentLength)
|
||||||
|
{
|
||||||
|
var count = Math.Min(value.Length, Math.Min(destination.Length, maxContentLength));
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var ch = value[i];
|
||||||
|
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
|
@ -1486,7 +1576,11 @@ public static class SaveDataExports
|
||||||
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
|
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
|
||||||
// until the SAVE_DATA_MEMORY_SYNC_END event arrives. Post it so that
|
// until the SAVE_DATA_MEMORY_SYNC_END event arrives. Post it so that
|
||||||
// poll completes (this is what wedged Dead Cells at FLIP 0 in-level).
|
// poll completes (this is what wedged Dead Cells at FLIP 0 in-level).
|
||||||
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
|
EnqueueEvent(
|
||||||
|
EventTypeSaveDataMemorySyncEnd,
|
||||||
|
userId,
|
||||||
|
ResolveConfiguredTitleId(),
|
||||||
|
"sce_sdmemory");
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,6 @@ public static class GameServiceStubs
|
||||||
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
|
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
|
||||||
public static int NpGameIntentTerminate(CpuContext ctx) => Ok(ctx);
|
public static int NpGameIntentTerminate(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
[SysAbiExport(Nid = "jqb7HntFQFc", ExportName = "sceWebBrowserDialogInitialize",
|
|
||||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
|
||||||
public static int WebBrowserDialogInitialize(CpuContext ctx) => Ok(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "ocHtyBwHfys", ExportName = "sceWebBrowserDialogTerminate",
|
|
||||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
|
||||||
public static int WebBrowserDialogTerminate(CpuContext ctx) => Ok(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(Nid = "mlYGfmqE3fQ", ExportName = "sceSigninDialogInitialize",
|
[SysAbiExport(Nid = "mlYGfmqE3fQ", ExportName = "sceSigninDialogInitialize",
|
||||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")]
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")]
|
||||||
public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx);
|
public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
// 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 AudioOut2ContextGetQueueLevelExportsTests
|
||||||
|
{
|
||||||
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
|
private const ulong ParamAddress = MemoryBase + 0x100;
|
||||||
|
private const ulong ContextMemoryAddress = MemoryBase + 0x200;
|
||||||
|
private const ulong OutContextAddress = MemoryBase + 0x300;
|
||||||
|
private const ulong LevelAddress = MemoryBase + 0x400;
|
||||||
|
private const ulong AvailableAddress = MemoryBase + 0x404;
|
||||||
|
private const uint QueueDepth = 7;
|
||||||
|
|
||||||
|
private static (CpuContext Context, FakeCpuMemory Memory, ulong Handle) CreateContext()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(MemoryBase, 0x2000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
Span<byte> param = stackalloc byte[0x40];
|
||||||
|
param.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], QueueDepth);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], 512);
|
||||||
|
Assert.True(memory.TryWrite(ParamAddress, param));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = ParamAddress;
|
||||||
|
ctx[CpuRegister.Rsi] = ContextMemoryAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = 0x1000;
|
||||||
|
ctx[CpuRegister.Rcx] = OutContextAddress;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextCreate(ctx));
|
||||||
|
|
||||||
|
Span<byte> handleBytes = stackalloc byte[8];
|
||||||
|
Assert.True(memory.TryRead(OutContextAddress, handleBytes));
|
||||||
|
return (ctx, memory, BinaryPrimitives.ReadUInt64LittleEndian(handleBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ReadU32(FakeCpuMemory memory, ulong address)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[4];
|
||||||
|
Assert.True(memory.TryRead(address, value));
|
||||||
|
return BinaryPrimitives.ReadUInt32LittleEndian(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextGetQueueLevel_AvailableOnlyWritesQueueCapacity()
|
||||||
|
{
|
||||||
|
var (ctx, memory, handle) = CreateContext();
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 0;
|
||||||
|
ctx[CpuRegister.Rdx] = AvailableAddress;
|
||||||
|
|
||||||
|
var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx);
|
||||||
|
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
Assert.Equal(QueueDepth, ReadU32(memory, AvailableAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextGetQueueLevel_LevelOnlyWritesZero()
|
||||||
|
{
|
||||||
|
var (ctx, memory, handle) = CreateContext();
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = LevelAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = 0;
|
||||||
|
|
||||||
|
var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx);
|
||||||
|
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
Assert.Equal(0u, ReadU32(memory, LevelAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextGetQueueLevel_BothOutputsRemainDistinct()
|
||||||
|
{
|
||||||
|
var (ctx, memory, handle) = CreateContext();
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = LevelAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = AvailableAddress;
|
||||||
|
|
||||||
|
var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx);
|
||||||
|
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
Assert.Equal(0u, ReadU32(memory, LevelAddress));
|
||||||
|
Assert.Equal(QueueDepth, ReadU32(memory, AvailableAddress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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 AudioOut2ContextQueryMemoryExportsTests
|
||||||
|
{
|
||||||
|
private const int InvalidPointer = unchecked((int)0x8026_800C);
|
||||||
|
private const ulong MemoryBase = 0x10_0000_0000;
|
||||||
|
private const ulong ParamAddress = MemoryBase + 0x100;
|
||||||
|
private const ulong StackPointer = MemoryBase + 0x400;
|
||||||
|
private const ulong MemorySizeAddress = StackPointer + 0x10;
|
||||||
|
private const ulong StaleThirdArgument = MemoryBase + 0x800;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextQueryMemory_WritesOnlyOneSizeTAndIgnoresRdx()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
Span<byte> param = stackalloc byte[0x40];
|
||||||
|
param.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 4);
|
||||||
|
Assert.True(memory.TryWrite(ParamAddress, param));
|
||||||
|
|
||||||
|
Span<byte> guard = stackalloc byte[0x10];
|
||||||
|
guard.Fill(0xA5);
|
||||||
|
Assert.True(memory.TryWrite(MemorySizeAddress, guard));
|
||||||
|
Assert.True(memory.TryWrite(StaleThirdArgument, guard));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = ParamAddress;
|
||||||
|
ctx[CpuRegister.Rsi] = MemorySizeAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = StaleThirdArgument;
|
||||||
|
ctx[CpuRegister.Rsp] = StackPointer;
|
||||||
|
ctx[CpuRegister.Rbp] = StackPointer + 0x48;
|
||||||
|
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextQueryMemory(ctx));
|
||||||
|
|
||||||
|
Span<byte> output = stackalloc byte[0x10];
|
||||||
|
Assert.True(memory.TryRead(MemorySizeAddress, output));
|
||||||
|
Assert.Equal(0x11640UL, BinaryPrimitives.ReadUInt64LittleEndian(output));
|
||||||
|
Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5 }, output[8..].ToArray());
|
||||||
|
|
||||||
|
Span<byte> stale = stackalloc byte[0x10];
|
||||||
|
Assert.True(memory.TryRead(StaleThirdArgument, stale));
|
||||||
|
Assert.Equal(guard.ToArray(), stale.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextQueryMemory_RejectsMissingSecondArgument()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = ParamAddress;
|
||||||
|
ctx[CpuRegister.Rsi] = 0;
|
||||||
|
ctx[CpuRegister.Rdx] = StaleThirdArgument;
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
InvalidPointer,
|
||||||
|
AudioOut2Exports.AudioOut2ContextQueryMemory(ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
public sealed class AudioOut2MasteringExportsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void MasteringSetParam_AcceptsOpaqueParameters()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = 0x1_0000_0100;
|
||||||
|
ctx[CpuRegister.Rsi] = 2;
|
||||||
|
ctx[CpuRegister.Rdx] = 1;
|
||||||
|
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
|
|
||||||
|
var result = AudioOut2Exports.AudioOut2MasteringSetParam(ctx);
|
||||||
|
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MasteringSetParam_RegistersForGen5()
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterExports(
|
||||||
|
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||||
|
|
||||||
|
Assert.True(manager.TryGetExport("v8iOE+j8a5o", out var export));
|
||||||
|
Assert.Equal("sceAudioOut2MasteringSetParam", export.Name);
|
||||||
|
Assert.Equal("libSceAudioOut2", export.LibraryName);
|
||||||
|
Assert.Equal(Generation.Gen5, export.Target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
[Collection(AudioOutStateCollection.Name)]
|
||||||
|
public sealed class AudioOut2PcmSnapshotTests : IDisposable
|
||||||
|
{
|
||||||
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
|
private const ulong ContextParamAddress = MemoryBase + 0x100;
|
||||||
|
private const ulong ContextOutAddress = MemoryBase + 0x200;
|
||||||
|
private const ulong PortParamAddress = MemoryBase + 0x300;
|
||||||
|
private const ulong FirstPortOutAddress = MemoryBase + 0x400;
|
||||||
|
private const ulong SecondPortOutAddress = MemoryBase + 0x408;
|
||||||
|
private const ulong AttributeAddress = MemoryBase + 0x500;
|
||||||
|
private const ulong PcmAttributeValueAddress = MemoryBase + 0x600;
|
||||||
|
private const ulong ContextMemoryAddress = MemoryBase + 0x1000;
|
||||||
|
private const ulong SharedPcmAddress = MemoryBase + 0x4000;
|
||||||
|
|
||||||
|
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x10000);
|
||||||
|
private readonly CpuContext _ctx;
|
||||||
|
|
||||||
|
public AudioOut2PcmSnapshotTests()
|
||||||
|
{
|
||||||
|
AudioOut2Exports.ResetForTests();
|
||||||
|
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PortSetAttributes_SnapshotsPcmWhenPortsReuseGuestBuffer()
|
||||||
|
{
|
||||||
|
var context = CreateContext(grainSamples: 64);
|
||||||
|
var firstPort = CreateSigned16StereoPort(context, FirstPortOutAddress);
|
||||||
|
var secondPort = CreateSigned16StereoPort(context, SecondPortOutAddress);
|
||||||
|
WritePcmAttribute();
|
||||||
|
|
||||||
|
var firstPcm = Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||||
|
.Select(static value => unchecked((byte)(value + 1)))
|
||||||
|
.ToArray();
|
||||||
|
var secondPcm = Enumerable.Range(0, firstPcm.Length)
|
||||||
|
.Select(static value => unchecked((byte)(255 - value)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.True(_memory.TryWrite(SharedPcmAddress, firstPcm));
|
||||||
|
SetPcmAttribute(firstPort);
|
||||||
|
|
||||||
|
Assert.True(_memory.TryWrite(SharedPcmAddress, secondPcm));
|
||||||
|
SetPcmAttribute(secondPort);
|
||||||
|
|
||||||
|
Assert.True(_memory.TryWrite(SharedPcmAddress, new byte[firstPcm.Length]));
|
||||||
|
|
||||||
|
Assert.Equal(firstPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort));
|
||||||
|
Assert.Equal(secondPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(secondPort));
|
||||||
|
|
||||||
|
Assert.Equal(firstPcm, AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort));
|
||||||
|
Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort));
|
||||||
|
Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort));
|
||||||
|
|
||||||
|
Assert.Equal(secondPcm, AudioOut2Exports.TakePortPcmSnapshotForTests(secondPort));
|
||||||
|
Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(secondPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData((ushort)0x0100)]
|
||||||
|
[InlineData((ushort)6)]
|
||||||
|
public void ContextPush_DoesNotSubmitObjectOrVibrationPorts(ushort portType)
|
||||||
|
{
|
||||||
|
var stream = new RecordingAudioStream();
|
||||||
|
var openCalls = 0;
|
||||||
|
AudioOut2Exports.SetStreamFactoryForTests(_ =>
|
||||||
|
{
|
||||||
|
openCalls++;
|
||||||
|
return stream;
|
||||||
|
});
|
||||||
|
|
||||||
|
var context = CreateContext(grainSamples: 64);
|
||||||
|
var port = CreateSigned16StereoPort(context, FirstPortOutAddress, portType);
|
||||||
|
WritePcmAttribute();
|
||||||
|
var pcm = Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||||
|
.Select(static value => unchecked((byte)(value + 1)))
|
||||||
|
.ToArray();
|
||||||
|
Assert.True(_memory.TryWrite(SharedPcmAddress, pcm));
|
||||||
|
SetPcmAttribute(port);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = context;
|
||||||
|
_ctx[CpuRegister.Rsi] = 1;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx));
|
||||||
|
|
||||||
|
Assert.Equal(0, openCalls);
|
||||||
|
Assert.Empty(stream.Submissions);
|
||||||
|
Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(port));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextPush_SubmitsDevicePortSnapshotOnlyOnce()
|
||||||
|
{
|
||||||
|
var stream = new RecordingAudioStream();
|
||||||
|
AudioOut2Exports.SetStreamFactoryForTests(_ => stream);
|
||||||
|
|
||||||
|
var context = CreateContext(grainSamples: 64);
|
||||||
|
var port = CreateSigned16StereoPort(context, FirstPortOutAddress);
|
||||||
|
WritePcmAttribute();
|
||||||
|
Assert.True(_memory.TryWrite(
|
||||||
|
SharedPcmAddress,
|
||||||
|
Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||||
|
.Select(static value => unchecked((byte)(value + 1)))
|
||||||
|
.ToArray()));
|
||||||
|
SetPcmAttribute(port);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = context;
|
||||||
|
_ctx[CpuRegister.Rsi] = 1;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx));
|
||||||
|
Assert.Single(stream.Submissions);
|
||||||
|
Assert.Equal(64 * 2 * sizeof(short), stream.Submissions[0].Length);
|
||||||
|
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx));
|
||||||
|
Assert.Single(stream.Submissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextAdvance_SubmitsDevicePortSnapshotOnlyOnce()
|
||||||
|
{
|
||||||
|
var stream = new RecordingAudioStream();
|
||||||
|
AudioOut2Exports.SetStreamFactoryForTests(_ => stream);
|
||||||
|
|
||||||
|
var context = CreateContext(grainSamples: 64);
|
||||||
|
var port = CreateSigned16StereoPort(context, FirstPortOutAddress);
|
||||||
|
WritePcmAttribute();
|
||||||
|
Assert.True(_memory.TryWrite(
|
||||||
|
SharedPcmAddress,
|
||||||
|
Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||||
|
.Select(static value => unchecked((byte)(value + 1)))
|
||||||
|
.ToArray()));
|
||||||
|
SetPcmAttribute(port);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = context;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextAdvance(_ctx));
|
||||||
|
Assert.Single(stream.Submissions);
|
||||||
|
Assert.Equal(64 * 2 * sizeof(short), stream.Submissions[0].Length);
|
||||||
|
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextAdvance(_ctx));
|
||||||
|
Assert.Single(stream.Submissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextDestroy_RemovesOwnedPortsAndPendingSnapshots()
|
||||||
|
{
|
||||||
|
var firstContext = CreateContext(grainSamples: 64);
|
||||||
|
var secondContext = CreateContext(grainSamples: 64);
|
||||||
|
var firstPort = CreateSigned16StereoPort(firstContext, FirstPortOutAddress);
|
||||||
|
var secondPort = CreateSigned16StereoPort(secondContext, SecondPortOutAddress);
|
||||||
|
WritePcmAttribute();
|
||||||
|
|
||||||
|
byte[] firstPcm = new byte[64 * 2 * sizeof(short)];
|
||||||
|
byte[] secondPcm = Enumerable.Repeat((byte)0x5A, firstPcm.Length).ToArray();
|
||||||
|
Assert.True(_memory.TryWrite(SharedPcmAddress, firstPcm));
|
||||||
|
SetPcmAttribute(firstPort);
|
||||||
|
Assert.True(_memory.TryWrite(SharedPcmAddress, secondPcm));
|
||||||
|
SetPcmAttribute(secondPort);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = firstContext;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextDestroy(_ctx));
|
||||||
|
|
||||||
|
Assert.False(AudioOut2Exports.HasPortForTests(firstPort));
|
||||||
|
Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort));
|
||||||
|
Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort));
|
||||||
|
Assert.True(AudioOut2Exports.HasPortForTests(secondPort));
|
||||||
|
Assert.Equal(secondPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(secondPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => AudioOut2Exports.ResetForTests();
|
||||||
|
|
||||||
|
private ulong CreateContext(uint grainSamples)
|
||||||
|
{
|
||||||
|
Span<byte> param = stackalloc byte[0x40];
|
||||||
|
param.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 2);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], grainSamples);
|
||||||
|
Assert.True(_memory.TryWrite(ContextParamAddress, param));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ContextParamAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = ContextMemoryAddress;
|
||||||
|
_ctx[CpuRegister.Rdx] = 0x2000;
|
||||||
|
_ctx[CpuRegister.Rcx] = ContextOutAddress;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextCreate(_ctx));
|
||||||
|
return ReadUInt64(ContextOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong CreateSigned16StereoPort(ulong context, ulong outAddress, ushort portType = 0)
|
||||||
|
{
|
||||||
|
Span<byte> param = stackalloc byte[0x40];
|
||||||
|
param.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(param, portType);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 0x201);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
|
||||||
|
Assert.True(_memory.TryWrite(PortParamAddress, param));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = context;
|
||||||
|
_ctx[CpuRegister.Rsi] = PortParamAddress;
|
||||||
|
_ctx[CpuRegister.Rdx] = outAddress;
|
||||||
|
_ctx[CpuRegister.Rcx] = 0;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2PortCreate(_ctx));
|
||||||
|
return ReadUInt64(outAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WritePcmAttribute()
|
||||||
|
{
|
||||||
|
Span<byte> pcm = stackalloc byte[8];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pcm, SharedPcmAddress);
|
||||||
|
Assert.True(_memory.TryWrite(PcmAttributeValueAddress, pcm));
|
||||||
|
|
||||||
|
Span<byte> attribute = stackalloc byte[0x18];
|
||||||
|
attribute.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(attribute, 0);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x08..], PcmAttributeValueAddress);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x10..], 8);
|
||||||
|
Assert.True(_memory.TryWrite(AttributeAddress, attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPcmAttribute(ulong port)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = port;
|
||||||
|
_ctx[CpuRegister.Rsi] = AttributeAddress;
|
||||||
|
_ctx[CpuRegister.Rdx] = 1;
|
||||||
|
Assert.Equal(0, AudioOut2Exports.AudioOut2PortSetAttributes(_ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong ReadUInt64(ulong address)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[8];
|
||||||
|
Assert.True(_memory.TryRead(address, value));
|
||||||
|
return BinaryPrimitives.ReadUInt64LittleEndian(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingAudioStream : IHostAudioStream
|
||||||
|
{
|
||||||
|
public List<byte[]> Submissions { get; } = [];
|
||||||
|
|
||||||
|
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||||
|
{
|
||||||
|
Submissions.Add(stereoPcm16.ToArray());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,9 +42,12 @@ public sealed class AudioOut2PortGetStateExportsTests
|
||||||
// SceAudioOut2PortState: output@0, numChannels@2, volume@4.
|
// SceAudioOut2PortState: output@0, numChannels@2, volume@4.
|
||||||
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
|
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
|
||||||
Assert.Equal(2, state[2]);
|
Assert.Equal(2, state[2]);
|
||||||
Assert.Equal(-1, BinaryPrimitives.ReadInt16LittleEndian(state[4..]));
|
Assert.Equal(0, state[3]);
|
||||||
|
Assert.Equal(127, BinaryPrimitives.ReadInt16LittleEndian(state[4..]));
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(state[6..]));
|
||||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[8..]));
|
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[8..]));
|
||||||
// sizeof(SceAudioOut2PortState) is 0x40; nothing past it may be touched.
|
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[0x0C..]));
|
||||||
|
Assert.All(state[0x10..0x40].ToArray(), value => Assert.Equal(0, value));
|
||||||
Assert.Equal(0xAB, state[0x40]);
|
Assert.Equal(0xAB, state[0x40]);
|
||||||
Assert.Equal(0xAB, state[0x7F]);
|
Assert.Equal(0xAB, state[0x7F]);
|
||||||
}
|
}
|
||||||
|
|
@ -102,15 +105,19 @@ public sealed class AudioOut2PortGetStateExportsTests
|
||||||
Assert.Equal(0, result);
|
Assert.Equal(0, result);
|
||||||
Span<byte> info = stackalloc byte[0x80];
|
Span<byte> info = stackalloc byte[0x80];
|
||||||
Assert.True(memory.TryRead(StateAddress, info));
|
Assert.True(memory.TryRead(StateAddress, info));
|
||||||
// type@0 = SPEAKER_TYPE_TV, availableBits@4 = FRONT_LEFT | FRONT_RIGHT.
|
|
||||||
Assert.Equal(0, info[0]);
|
Assert.Equal(0, info[0]);
|
||||||
Assert.Equal(3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
|
Assert.Equal(0, info[1]);
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[2..]));
|
||||||
|
Assert.Equal(0x3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
|
||||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[8..]));
|
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[8..]));
|
||||||
// aSpeakerAngle[0..1] = { -30, 0 }, { +30, 0 } degrees.
|
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[0x0C..]));
|
||||||
Assert.Equal(-30, BinaryPrimitives.ReadInt16LittleEndian(info[0x10..]));
|
Assert.Equal(-30, BinaryPrimitives.ReadInt16LittleEndian(info[0x10..]));
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x12..]));
|
||||||
Assert.Equal(30, BinaryPrimitives.ReadInt16LittleEndian(info[0x14..]));
|
Assert.Equal(30, BinaryPrimitives.ReadInt16LittleEndian(info[0x14..]));
|
||||||
// sizeof(SceAudioOut2SpeakerInfo) is 0x50.
|
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x16..]));
|
||||||
|
Assert.All(info[0x18..0x50].ToArray(), value => Assert.Equal(0, value));
|
||||||
Assert.Equal(0xCD, info[0x50]);
|
Assert.Equal(0xCD, info[0x50]);
|
||||||
|
Assert.Equal(0xCD, info[0x7F]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.AvPlayer;
|
||||||
|
using SharpEmu.Libs.Tests.Kernel;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.AvPlayer;
|
||||||
|
|
||||||
|
[Collection(KernelMemoryCompatStateCollection.Name)]
|
||||||
|
public sealed class AvPlayerControlTests : IDisposable
|
||||||
|
{
|
||||||
|
private const int InvalidParameters = unchecked((int)0x806A0001);
|
||||||
|
private const ulong Handle = 0xA0_0000_2400;
|
||||||
|
private readonly CpuContext _context = new(
|
||||||
|
new FakeCpuMemory(0x1_0000_0000, 0x1000),
|
||||||
|
Generation.Gen5);
|
||||||
|
|
||||||
|
public AvPlayerControlTests()
|
||||||
|
{
|
||||||
|
AvPlayerExports.RegisterPlayerForTest(
|
||||||
|
Handle,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
durationMilliseconds: 1000);
|
||||||
|
_context[CpuRegister.Rdi] = Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AvailableBandwidthAcceptsAValidPlayerWithoutAStreamingSource()
|
||||||
|
{
|
||||||
|
_context[CpuRegister.Rsi] = 0;
|
||||||
|
|
||||||
|
Assert.Equal(0, AvPlayerExports.AvPlayerSetAvailableBandwidth(_context));
|
||||||
|
Assert.Equal(0UL, _context[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AvailableBandwidthRejectsAnUnknownPlayer()
|
||||||
|
{
|
||||||
|
_context[CpuRegister.Rdi] = Handle + 1;
|
||||||
|
_context[CpuRegister.Rsi] = 25_000_000;
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
InvalidParameters,
|
||||||
|
AvPlayerExports.AvPlayerSetAvailableBandwidth(_context));
|
||||||
|
Assert.Equal(
|
||||||
|
unchecked((ulong)InvalidParameters),
|
||||||
|
_context[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => AvPlayerExports.RemovePlayerForTest(Handle);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.CommonDialog;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.CommonDialog;
|
||||||
|
|
||||||
|
[CollectionDefinition("WebBrowserDialog", DisableParallelization = true)]
|
||||||
|
public sealed class WebBrowserDialogCollection;
|
||||||
|
|
||||||
|
[Collection("WebBrowserDialog")]
|
||||||
|
public sealed class WebBrowserDialogExportsTests : IDisposable
|
||||||
|
{
|
||||||
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
|
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000);
|
||||||
|
private readonly CpuContext _context;
|
||||||
|
|
||||||
|
public WebBrowserDialogExportsTests()
|
||||||
|
{
|
||||||
|
WebBrowserDialogExports.ResetForTests();
|
||||||
|
_context = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => WebBrowserDialogExports.ResetForTests();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HeadlessLifecycle_CompletesOnFirstStatusPoll()
|
||||||
|
{
|
||||||
|
Assert.Equal(WebBrowserDialogExports.StatusNone,
|
||||||
|
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogInitialize(_context));
|
||||||
|
Assert.Equal(WebBrowserDialogExports.StatusInitialized,
|
||||||
|
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||||
|
|
||||||
|
_context[CpuRegister.Rdi] = MemoryBase + 0x100;
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogOpen(_context));
|
||||||
|
Assert.Equal(WebBrowserDialogExports.StatusRunning, WebBrowserDialogExports.StatusForTests);
|
||||||
|
Assert.Equal(WebBrowserDialogExports.StatusFinished,
|
||||||
|
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||||
|
|
||||||
|
var resultAddress = MemoryBase + 0x200;
|
||||||
|
_context[CpuRegister.Rdi] = resultAddress;
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogGetResult(_context));
|
||||||
|
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||||
|
Assert.True(_memory.TryRead(resultAddress, result));
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(result));
|
||||||
|
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogTerminate(_context));
|
||||||
|
Assert.Equal(WebBrowserDialogExports.StatusNone,
|
||||||
|
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Close_FinishesRunningDialogWithoutPolling()
|
||||||
|
{
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogInitialize(_context));
|
||||||
|
_context[CpuRegister.Rdi] = MemoryBase + 0x100;
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogOpenForPredeterminedContent(_context));
|
||||||
|
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogClose(_context));
|
||||||
|
Assert.Equal(WebBrowserDialogExports.StatusFinished, WebBrowserDialogExports.StatusForTests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,8 @@ public sealed class JsonExportRegistrationTests
|
||||||
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
|
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
|
||||||
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
|
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
|
||||||
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
|
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
|
||||||
|
("GvGvswb0v34", "_ZN3sce4Json14InitParameter2C2Ev"),
|
||||||
|
("W72B9ylU2JA", "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv"),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static ModuleManager CreateRegisteredManager()
|
private static ModuleManager CreateRegisteredManager()
|
||||||
|
|
@ -131,4 +133,36 @@ public sealed class JsonExportRegistrationTests
|
||||||
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
|
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
|
||||||
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
|
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InitParameter2Aliases_InitializeAndWriteTheirDocumentedFields()
|
||||||
|
{
|
||||||
|
const ulong memoryBase = 0x1_0000_0000;
|
||||||
|
const ulong parameterAddress = memoryBase + 0x100;
|
||||||
|
const ulong allocatorAddress = 0x8_0012_3000;
|
||||||
|
const ulong userDataAddress = memoryBase + 0x500;
|
||||||
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||||
|
var manager = CreateRegisteredManager();
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
Assert.True(memory.TryWrite(parameterAddress, Enumerable.Repeat((byte)0xA5, 0x28).ToArray()));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = parameterAddress;
|
||||||
|
Assert.True(manager.TryDispatch("GvGvswb0v34", ctx, out var constructorResult));
|
||||||
|
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, constructorResult);
|
||||||
|
|
||||||
|
var parameter = new byte[0x28];
|
||||||
|
Assert.True(memory.TryRead(parameterAddress, parameter));
|
||||||
|
Assert.All(parameter, value => Assert.Equal(0, value));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = parameterAddress;
|
||||||
|
ctx[CpuRegister.Rsi] = allocatorAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = userDataAddress;
|
||||||
|
Assert.True(manager.TryDispatch("W72B9ylU2JA", ctx, out var allocatorResult));
|
||||||
|
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, allocatorResult);
|
||||||
|
|
||||||
|
Assert.True(memory.TryRead(parameterAddress, parameter));
|
||||||
|
Assert.Equal(allocatorAddress, BitConverter.ToUInt64(parameter, 0));
|
||||||
|
Assert.Equal(userDataAddress, BitConverter.ToUInt64(parameter, 8));
|
||||||
|
Assert.All(parameter[0x10..], value => Assert.Equal(0, value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,276 @@ public sealed class Http2ExportsTests
|
||||||
|
|
||||||
Assert.Equal(unchecked((int)0x817B106A), Http2Exports.Http2CreateTemplate(ctx));
|
Assert.Equal(unchecked((int)0x817B106A), Http2Exports.Http2CreateTemplate(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetMinSslVersion_AcceptsLiveIdsAndRejectsDeletedIds()
|
||||||
|
{
|
||||||
|
Http2Exports.ResetRuntimeState();
|
||||||
|
var memory = new FakeCpuMemory(MemoryBase, 0x2000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = 1;
|
||||||
|
ctx[CpuRegister.Rsi] = 2;
|
||||||
|
ctx[CpuRegister.Rdx] = 0x1000;
|
||||||
|
ctx[CpuRegister.Rcx] = 2;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2Init(ctx));
|
||||||
|
var contextId = ctx[CpuRegister.Rax];
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = contextId;
|
||||||
|
ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x100, "test");
|
||||||
|
ctx[CpuRegister.Rdx] = 3;
|
||||||
|
ctx[CpuRegister.Rcx] = 0;
|
||||||
|
Assert.True(Http2Exports.Http2CreateTemplate(ctx) > 0);
|
||||||
|
var templateId = ctx[CpuRegister.Rax];
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = templateId;
|
||||||
|
ctx[CpuRegister.Rsi] = 4;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetMinSslVersion(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = templateId;
|
||||||
|
ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x200, "GET");
|
||||||
|
ctx[CpuRegister.Rdx] = memory.WriteCString(MemoryBase + 0x300, "https://example.invalid/");
|
||||||
|
ctx[CpuRegister.Rcx] = 0;
|
||||||
|
Assert.True(Http2Exports.Http2CreateRequestWithUrl(ctx) > 0);
|
||||||
|
var requestId = ctx[CpuRegister.Rax];
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = requestId;
|
||||||
|
ctx[CpuRegister.Rsi] = 4;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetMinSslVersion(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = templateId;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2DeleteTemplate(ctx));
|
||||||
|
Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetMinSslVersion(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = requestId;
|
||||||
|
Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetMinSslVersion(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetMinSslVersion_RegistersPs5Nid()
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||||
|
|
||||||
|
Assert.True(manager.TryGetExport("09tk+kIA1Ns", out var export));
|
||||||
|
Assert.Equal("sceHttp2SetMinSslVersion", export.Name);
|
||||||
|
Assert.Equal("libSceHttp2", export.LibraryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Options_AreStoredInheritedAndIndependentlyUpdated()
|
||||||
|
{
|
||||||
|
var (memory, ctx, templateId) = CreateTemplate();
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)templateId);
|
||||||
|
ctx[CpuRegister.Rsi] = 0x0F;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SslEnableOption(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 0x05;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SslDisableOption(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 0x1234_5678;
|
||||||
|
ctx[CpuRegister.Rdx] = 0x8765_4321;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetRedirectCallback(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 0;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetAutoRedirect(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 101;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetConnectTimeOut(ctx));
|
||||||
|
ctx[CpuRegister.Rsi] = 202;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetRecvTimeOut(ctx));
|
||||||
|
ctx[CpuRegister.Rsi] = 303;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetSendTimeOut(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 7;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetAuthEnabled(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 0x1111_2222;
|
||||||
|
ctx[CpuRegister.Rdx] = 0x3333_4444;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetSslCallback(ctx));
|
||||||
|
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(templateId, out var templateOptions));
|
||||||
|
Assert.Equal(0x0Au, templateOptions.SslOptions);
|
||||||
|
Assert.Equal(0x1234_5678UL, templateOptions.RedirectCallback);
|
||||||
|
Assert.Equal(0x8765_4321UL, templateOptions.RedirectUserArgument);
|
||||||
|
Assert.False(templateOptions.AutoRedirect);
|
||||||
|
Assert.Equal(101u, templateOptions.ConnectTimeoutMicroseconds);
|
||||||
|
Assert.Equal(202u, templateOptions.ReceiveTimeoutMicroseconds);
|
||||||
|
Assert.Equal(303u, templateOptions.SendTimeoutMicroseconds);
|
||||||
|
Assert.True(templateOptions.AuthEnabled);
|
||||||
|
Assert.Equal(0x1111_2222UL, templateOptions.SslCallback);
|
||||||
|
Assert.Equal(0x3333_4444UL, templateOptions.SslUserArgument);
|
||||||
|
|
||||||
|
var requestId = CreateRequest(memory, ctx, templateId);
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var inheritedOptions));
|
||||||
|
Assert.Equal(templateOptions, inheritedOptions);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)requestId);
|
||||||
|
ctx[CpuRegister.Rsi] = 404;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetRecvTimeOut(ctx));
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var requestOptions));
|
||||||
|
Assert.Equal(404u, requestOptions.ReceiveTimeoutMicroseconds);
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(templateId, out var unchangedTemplateOptions));
|
||||||
|
Assert.Equal(202u, unchangedTemplateOptions.ReceiveTimeoutMicroseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OptionSetters_RejectUnknownHandles()
|
||||||
|
{
|
||||||
|
Http2Exports.ResetRuntimeState();
|
||||||
|
var ctx = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||||
|
ctx[CpuRegister.Rdi] = 0x7FFF_FFFF;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
ctx[CpuRegister.Rdx] = 2;
|
||||||
|
|
||||||
|
Func<CpuContext, int>[] setters =
|
||||||
|
[
|
||||||
|
Http2Exports.Http2SslDisableOption,
|
||||||
|
Http2Exports.Http2SslEnableOption,
|
||||||
|
Http2Exports.Http2SetRedirectCallback,
|
||||||
|
Http2Exports.Http2SetAutoRedirect,
|
||||||
|
Http2Exports.Http2SetCookieBox,
|
||||||
|
Http2Exports.Http2SetConnectTimeOut,
|
||||||
|
Http2Exports.Http2SetRecvTimeOut,
|
||||||
|
Http2Exports.Http2SetSendTimeOut,
|
||||||
|
Http2Exports.Http2SetAuthEnabled,
|
||||||
|
Http2Exports.Http2SetSslCallback,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (var setter in setters)
|
||||||
|
{
|
||||||
|
Assert.Equal(unchecked((int)0x817B1100), setter(ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRequestHeader_ValidatesPointersAndTracksGuestStrings()
|
||||||
|
{
|
||||||
|
var (memory, ctx, templateId) = CreateTemplate();
|
||||||
|
var requestId = CreateRequest(memory, ctx, templateId);
|
||||||
|
var nameAddress = memory.WriteCString(MemoryBase + 0x500, "X-SharpEmu-Test");
|
||||||
|
var valueAddress = memory.WriteCString(MemoryBase + 0x600, "enabled");
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)requestId);
|
||||||
|
ctx[CpuRegister.Rsi] = nameAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = valueAddress;
|
||||||
|
ctx[CpuRegister.Rcx] = 1;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2AddRequestHeader(ctx));
|
||||||
|
|
||||||
|
var header = Assert.Single(Http2Exports.GetRequestHeadersForTests(requestId));
|
||||||
|
Assert.Equal("X-SharpEmu-Test", header.Name);
|
||||||
|
Assert.Equal("enabled", header.Value);
|
||||||
|
Assert.Equal(1u, header.Mode);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 0;
|
||||||
|
Assert.Equal(unchecked((int)0x817B1225), Http2Exports.Http2AddRequestHeader(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = MemoryBase + 0x3000;
|
||||||
|
Assert.Equal(unchecked((int)0x817B11FE), Http2Exports.Http2AddRequestHeader(ctx));
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = 0x7FFF_FFFF;
|
||||||
|
ctx[CpuRegister.Rsi] = nameAddress;
|
||||||
|
Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2AddRequestHeader(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CookieBox_IsCreatedBoundInheritedAndCanBeDetached()
|
||||||
|
{
|
||||||
|
var (memory, ctx, templateId) = CreateTemplate();
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = 0x80000; // Deliberately stale sceHttp2Init register state.
|
||||||
|
Assert.Equal(0x3001, Http2Exports.Http2CreateCookieBox(ctx));
|
||||||
|
var cookieBoxId = unchecked((int)ctx[CpuRegister.Rax]);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)templateId);
|
||||||
|
ctx[CpuRegister.Rsi] = unchecked((uint)cookieBoxId);
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetCookieBox(ctx));
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(templateId, out var templateOptions));
|
||||||
|
Assert.Equal(cookieBoxId, templateOptions.CookieBoxId);
|
||||||
|
|
||||||
|
var requestId = CreateRequest(memory, ctx, templateId);
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var inheritedOptions));
|
||||||
|
Assert.Equal(cookieBoxId, inheritedOptions.CookieBoxId);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)requestId);
|
||||||
|
ctx[CpuRegister.Rsi] = 0;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetCookieBox(ctx));
|
||||||
|
Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var detachedOptions));
|
||||||
|
Assert.Equal(0, detachedOptions.CookieBoxId);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rsi] = 0x7FFF_FFFF;
|
||||||
|
Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetCookieBox(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetRequestContentLength_UpdatesOnlyLiveRequests()
|
||||||
|
{
|
||||||
|
var (memory, ctx, templateId) = CreateTemplate();
|
||||||
|
var requestId = CreateRequest(memory, ctx, templateId);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)requestId);
|
||||||
|
ctx[CpuRegister.Rsi] = 0x1_0000_0042;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2SetRequestContentLength(ctx));
|
||||||
|
Assert.True(Http2Exports.TryGetRequestContentLengthForTests(requestId, out var contentLength));
|
||||||
|
Assert.Equal(0x1_0000_0042UL, contentLength);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = 0x7FFF_FFFF;
|
||||||
|
Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetRequestContentLength(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("B37SruheQ5Y", "sceHttp2SslDisableOption")]
|
||||||
|
[InlineData("EWcwMpbr5F8", "sceHttp2SslEnableOption")]
|
||||||
|
[InlineData("BJgi0CH7al4", "sceHttp2SetRedirectCallback")]
|
||||||
|
[InlineData("b9AvoIaOuHI", "sceHttp2SetAutoRedirect")]
|
||||||
|
[InlineData("N4UfjvWJsMw", "sceHttp2CreateCookieBox")]
|
||||||
|
[InlineData("jrVHsKCXA0g", "sceHttp2SetCookieBox")]
|
||||||
|
[InlineData("FSAFOzi0FpM", "sceHttp2SetRequestContentLength")]
|
||||||
|
[InlineData("-HIO4VT87v8", "sceHttp2SetConnectTimeOut")]
|
||||||
|
[InlineData("izvHhqgDt44", "sceHttp2SetRecvTimeOut")]
|
||||||
|
[InlineData("XPtW45xiLHk", "sceHttp2SetSendTimeOut")]
|
||||||
|
[InlineData("jjFahkBPCYs", "sceHttp2SetAuthEnabled")]
|
||||||
|
[InlineData("nrPfOE8TQu0", "sceHttp2AddRequestHeader")]
|
||||||
|
[InlineData("YrWX+DhPHQY", "sceHttp2SetSslCallback")]
|
||||||
|
public void CompatibilityExports_RegisterPs5Nids(string nid, string name)
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||||
|
|
||||||
|
Assert.True(manager.TryGetExport(nid, out var export));
|
||||||
|
Assert.Equal(name, export.Name);
|
||||||
|
Assert.Equal("libSceHttp2", export.LibraryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (FakeCpuMemory Memory, CpuContext Context, int TemplateId) CreateTemplate()
|
||||||
|
{
|
||||||
|
Http2Exports.ResetRuntimeState();
|
||||||
|
var memory = new FakeCpuMemory(MemoryBase, 0x2000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = 1;
|
||||||
|
ctx[CpuRegister.Rsi] = 2;
|
||||||
|
ctx[CpuRegister.Rdx] = 0x1000;
|
||||||
|
ctx[CpuRegister.Rcx] = 2;
|
||||||
|
Assert.Equal(0, Http2Exports.Http2Init(ctx));
|
||||||
|
var contextId = unchecked((int)ctx[CpuRegister.Rax]);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)contextId);
|
||||||
|
ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x100, "test");
|
||||||
|
ctx[CpuRegister.Rdx] = 3;
|
||||||
|
ctx[CpuRegister.Rcx] = 0;
|
||||||
|
Assert.True(Http2Exports.Http2CreateTemplate(ctx) > 0);
|
||||||
|
return (memory, ctx, unchecked((int)ctx[CpuRegister.Rax]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CreateRequest(FakeCpuMemory memory, CpuContext ctx, int templateId)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rdi] = unchecked((uint)templateId);
|
||||||
|
ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x200, "GET");
|
||||||
|
ctx[CpuRegister.Rdx] = memory.WriteCString(MemoryBase + 0x300, "https://example.invalid/");
|
||||||
|
ctx[CpuRegister.Rcx] = 0;
|
||||||
|
Assert.True(Http2Exports.Http2CreateRequestWithUrl(ctx) > 0);
|
||||||
|
return unchecked((int)ctx[CpuRegister.Rax]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,18 @@ public sealed class SaveDataExportsTests : IDisposable
|
||||||
private const ulong SearchResult = Base + 0x1140;
|
private const ulong SearchResult = Base + 0x1140;
|
||||||
private const ulong SearchDirNames = Base + 0x1180;
|
private const ulong SearchDirNames = Base + 0x1180;
|
||||||
private const ulong SearchInfos = Base + 0x1200;
|
private const ulong SearchInfos = Base + 0x1200;
|
||||||
|
private const ulong BackupParam = Base + 0x1300;
|
||||||
|
private const ulong BackupTitleId = Base + 0x1380;
|
||||||
|
private const ulong BackupDirName = Base + 0x13C0;
|
||||||
|
private const ulong Unmapped = Base + 0x2_0000;
|
||||||
|
|
||||||
private const int NoEvent = unchecked((int)0x809F0008);
|
private const int NoEvent = unchecked((int)0x809F0008);
|
||||||
private const int ParameterError = unchecked((int)0x809F0000);
|
private const int ParameterError = unchecked((int)0x809F0000);
|
||||||
|
private const int MemoryFault = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
private const int ResourceBusy = unchecked((int)0x809F001B);
|
private const int ResourceBusy = unchecked((int)0x809F001B);
|
||||||
private const uint MountModeCreate = 1u << 2;
|
private const uint MountModeCreate = 1u << 2;
|
||||||
|
private const string BackupTitle = "PPSA10067";
|
||||||
|
private const string BackupDirectory = "SLOT_WITH_32_CHARACTERS_12345678";
|
||||||
|
|
||||||
private readonly FakeCpuMemory _memory = new(Base, 0x10000);
|
private readonly FakeCpuMemory _memory = new(Base, 0x10000);
|
||||||
private readonly CpuContext _ctx;
|
private readonly CpuContext _ctx;
|
||||||
|
|
@ -121,6 +128,65 @@ public sealed class SaveDataExportsTests : IDisposable
|
||||||
Assert.Equal(ParameterError, SaveDataExports.SaveDataGetEventResult(Reg(rsi: 0)));
|
Assert.Equal(ParameterError, SaveDataExports.SaveDataGetEventResult(Reg(rsi: 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Backup_QueuesCompleteBackupEndEvent_ThenDrains()
|
||||||
|
{
|
||||||
|
WriteAscii(BackupTitleId, BackupTitle);
|
||||||
|
WriteAscii(BackupDirName, BackupDirectory);
|
||||||
|
WriteBackupParam(BackupTitleId, BackupDirName);
|
||||||
|
|
||||||
|
Assert.Equal(0, SaveDataExports.SaveDataBackup(Reg(rdi: BackupParam)));
|
||||||
|
|
||||||
|
var initialized = new byte[0x70];
|
||||||
|
Array.Fill(initialized, (byte)0xCC);
|
||||||
|
Assert.True(_memory.TryWrite(EventOut, initialized));
|
||||||
|
Assert.Equal(0, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
||||||
|
|
||||||
|
var ev = new byte[0x70];
|
||||||
|
Assert.True(_memory.TryRead(EventOut, ev));
|
||||||
|
Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(ev));
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(ev.AsSpan(0x04)));
|
||||||
|
Assert.Equal(UserId, BinaryPrimitives.ReadInt32LittleEndian(ev.AsSpan(0x08)));
|
||||||
|
Assert.Equal(BackupTitle, Encoding.ASCII.GetString(ev, 0x10, 0x10).TrimEnd('\0'));
|
||||||
|
Assert.Equal(BackupDirectory, Encoding.ASCII.GetString(ev, 0x20, 0x20).TrimEnd('\0'));
|
||||||
|
Assert.All(ev[0x1A..0x20], value => Assert.Equal((byte)0, value));
|
||||||
|
Assert.All(ev[0x40..0x68], value => Assert.Equal((byte)0, value));
|
||||||
|
Assert.All(ev[0x68..0x70], value => Assert.Equal((byte)0xCC, value));
|
||||||
|
Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Backup_NullParameterOrDirectory_ReturnsParameterWithoutEvent()
|
||||||
|
{
|
||||||
|
Assert.Equal(ParameterError, SaveDataExports.SaveDataBackup(Reg(rdi: 0)));
|
||||||
|
|
||||||
|
WriteBackupParam(titleIdAddress: 0, dirNameAddress: 0);
|
||||||
|
Assert.Equal(ParameterError, SaveDataExports.SaveDataBackup(Reg(rdi: BackupParam)));
|
||||||
|
Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void Backup_InvalidNestedPointer_ReturnsMemoryFaultWithoutEvent(bool invalidTitle)
|
||||||
|
{
|
||||||
|
WriteAscii(BackupTitleId, BackupTitle);
|
||||||
|
WriteAscii(BackupDirName, BackupDirectory);
|
||||||
|
WriteBackupParam(
|
||||||
|
invalidTitle ? Unmapped : BackupTitleId,
|
||||||
|
invalidTitle ? BackupDirName : Unmapped);
|
||||||
|
|
||||||
|
Assert.Equal(MemoryFault, SaveDataExports.SaveDataBackup(Reg(rdi: BackupParam)));
|
||||||
|
Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Backup_InvalidStructurePointer_ReturnsMemoryFaultWithoutEvent()
|
||||||
|
{
|
||||||
|
Assert.Equal(MemoryFault, SaveDataExports.SaveDataBackup(Reg(rdi: Unmapped)));
|
||||||
|
Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SyncSaveDataMemory_PostsSyncEndEvent_DrainedOnce()
|
public void SyncSaveDataMemory_PostsSyncEndEvent_DrainedOnce()
|
||||||
{
|
{
|
||||||
|
|
@ -145,6 +211,11 @@ public sealed class SaveDataExportsTests : IDisposable
|
||||||
Assert.True(_ctx.TryReadInt32(EventOut + 0x04, out var errorCode));
|
Assert.True(_ctx.TryReadInt32(EventOut + 0x04, out var errorCode));
|
||||||
Assert.Equal(0, errorCode);
|
Assert.Equal(0, errorCode);
|
||||||
|
|
||||||
|
var ev = new byte[0x68];
|
||||||
|
Assert.True(_memory.TryRead(EventOut, ev));
|
||||||
|
Assert.Equal(TitleId[..10], Encoding.ASCII.GetString(ev, 0x10, 0x10).TrimEnd('\0'));
|
||||||
|
Assert.Equal("sce_sdmemory", Encoding.ASCII.GetString(ev, 0x20, 0x20).TrimEnd('\0'));
|
||||||
|
|
||||||
Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,4 +468,14 @@ public sealed class SaveDataExportsTests : IDisposable
|
||||||
0,
|
0,
|
||||||
SaveDataExports.SaveDataPrepare(Reg(rdi: MountPointStr, rsi: PrepareParam)));
|
SaveDataExports.SaveDataPrepare(Reg(rdi: MountPointStr, rsi: PrepareParam)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WriteBackupParam(ulong titleIdAddress, ulong dirNameAddress)
|
||||||
|
{
|
||||||
|
Span<byte> backup = stackalloc byte[0x40];
|
||||||
|
backup.Clear();
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(backup, UserId);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(backup[0x08..], titleIdAddress);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(backup[0x10..], dirNameAddress);
|
||||||
|
Assert.True(_memory.TryWrite(BackupParam, backup));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue