Compare commits

...

20 Commits

Author SHA1 Message Date
ParantezTech 810ff6a606 [Tests] Cover the AudioOut2, save data, Http2 and dialog exports
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:11 +03:00
ParantezTech edc2d17cf6 [AvPlayer] Accept available bandwidth hints
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech dbb2c1676c [Json] Add InitParameterRtti2::setAllocatorRtti
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech c698196493 [Json] Add the InitParameter2 base constructor
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech 017969fba3 [CommonDialog] Add the web browser dialog lifecycle
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech 4cc0eab03a [Network] Add Http2 option, cookie and request exports
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech e96e3f480b [Network] Track Http2 template and request options
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech fd56cef9b9 [SaveData] Add sceSaveDataDirNameSearch2
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech 448fb6be17 [SaveData] Add sceSaveDataBackup
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech df55be4110 [SaveData] Lay out save data events with their title ID
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:10 +03:00
ParantezTech 909ea50117 [Audio] Add the AudioOut2 vibration port type
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech 41b90121f3 [Audio] Let tests replace the AudioOut2 host streams
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech dd9b48f9cc [Audio] Trace AudioOut2 port, attribute and bed calls
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech 32f077df44 [Audio] Report full volume in the AudioOut2 port state
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech eba01a757d [Audio] Keep the AudioOut2 queue level and availability pointers apart
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech 15854585f6 [Audio] Report the AudioOut2 context memory size through its out pointer
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech 93022ba6d6 [Audio] Resolve AudioOut2 out-buffers from either ABI slot
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech 6647257dc5 [Audio] Accept sceAudioOut2MasteringSetParam
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:09 +03:00
ParantezTech da6d7f15d2 [Audio] Drop the ports of a destroyed AudioOut2 context
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:08 +03:00
ParantezTech e9e1680d02 [Audio] Snapshot AudioOut2 PCM when the guest submits it
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 02:43:08 +03:00
17 changed files with 1980 additions and 83 deletions

View File

@ -22,7 +22,6 @@ public static class AudioOut2Exports
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
private const int AudioOut2ContextParamSize = 0x40;
private const int AudioOut2ContextMemorySize = 0x4000;
private const int AudioOut2ContextMemoryAlignment = 0x100;
// 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.
private const int SpeakerArrayHeaderSize = 0x40;
@ -36,6 +35,17 @@ public static class AudioOut2Exports
private const int SpeakerArrayDivisorFieldOffset = 0x34;
private const int SpeakerArrayResultFieldOffset = 0x3C;
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),
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
@ -60,6 +70,7 @@ public static class AudioOut2Exports
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
private const int AttributeEntrySize = 0x18;
private const uint PortAttributeIdPcm = 0;
private const ushort AudioOut2PortTypeVibration = 6;
private const uint PortAttributeIdGain = 1;
private const uint PortAttributeIdPassthrough = 5;
private const uint PortAttributeIdAmbisonics = 8;
@ -88,6 +99,11 @@ public static class AudioOut2Exports
private static long _pushTraceCount;
private static long _submitTraceCount;
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 readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
@ -212,7 +228,7 @@ public static class AudioOut2Exports
public uint Ambisonics = AmbisonicsNone;
public uint Passthrough;
public int PcmPending;
public byte[]? PcmData;
}
@ -226,6 +242,7 @@ public static class AudioOut2Exports
private static string SecondaryBackendName = "none";
private static ulong PrimaryContextHandle;
private static readonly object HostSubmitGate = new();
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
[SysAbiExport(
Nid = "g2tViFIohHE",
@ -252,6 +269,16 @@ public static class AudioOut2Exports
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
// tune, but failure here makes Yotei tear down its whole ACM context and
// abort audio arena bring-up.
@ -305,8 +332,8 @@ public static class AudioOut2Exports
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
var memorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memorySizeAddress == 0)
{
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
@ -324,13 +351,12 @@ public static class AudioOut2Exports
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)];
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
Console.Error.WriteLine(
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
$"[LOADER][TRACE] audio_out2.context-query-memory " +
$"out=0x{memorySizeAddress:X} size=0x{contextMemorySize:X}");
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
? SetReturn(ctx, 0)
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
@ -391,8 +417,20 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx)
{
// Shared backend lifetime is process-wide; just drop the context entry.
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
var contextHandle = ctx[CpuRegister.Rdi];
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);
}
@ -401,7 +439,38 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2ContextBedWrite",
Target = Generation.Gen5,
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(
Nid = "aII9h5nli9U",
@ -494,11 +563,6 @@ public static class AudioOut2Exports
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
var outLevelAddress = ctx[CpuRegister.Rsi];
var outAvailableAddress = ctx[CpuRegister.Rdx];
if (outLevelAddress == 0)
{
outLevelAddress = outAvailableAddress;
outAvailableAddress = 0;
}
// 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
@ -550,6 +614,41 @@ public static class AudioOut2Exports
var portHandle = ctx[CpuRegister.Rdi];
var attributesAddress = ctx[CpuRegister.Rsi];
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))
{
return SetReturn(ctx, 0);
@ -622,14 +721,38 @@ public static class AudioOut2Exports
continue;
}
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
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);
if (n <= 8 || n % 500 == 0)
{
TraceAudioOut2(
$"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,
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
// rdx is the only out pointer.
var contextHandle = ctx[CpuRegister.Rdi];
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)
{
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
@ -669,6 +792,14 @@ public static class AudioOut2Exports
Span<byte> param = stackalloc byte[PortParamSize];
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);
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
@ -719,13 +850,16 @@ public static class AudioOut2Exports
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];
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;
if (Ports.TryGetValue(portHandle, out var port) &&
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
@ -735,15 +869,21 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
state[0x02] = channels;
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
if (!ctx.Memory.TryWrite(stateAddress, state))
{
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
TraceAudioOut2(
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
var traceState = Interlocked.Increment(ref _portStateTraceCount);
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);
}
@ -972,7 +1112,17 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")]
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);
}
@ -1064,13 +1214,20 @@ public static class AudioOut2Exports
{
try
{
var audio = HostPlatform.Current.Audio;
// Deeper host queue than classic AudioOut: FMOD's bursty
// AudioOut2 Push pattern underran a 32 KiB (~171 ms) bed.
PrimaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
PrimaryBackendName = audio.BackendName + "-primary";
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
if (streamFactory is not null)
{
PrimaryBackend = streamFactory(context.Frequency);
PrimaryBackendName = PrimaryBackend is null ? "silent" : "test-primary";
}
else
{
var audio = HostPlatform.Current.Audio;
PrimaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
PrimaryBackendName = audio.BackendName + "-primary";
}
}
catch (Exception exception)
{
@ -1088,11 +1245,20 @@ public static class AudioOut2Exports
{
try
{
var audio = HostPlatform.Current.Audio;
SecondaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
SecondaryBackendName = audio.BackendName + "-secondary";
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
if (streamFactory is not null)
{
SecondaryBackend = streamFactory(context.Frequency);
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)
{
@ -1155,7 +1321,6 @@ public static class AudioOut2Exports
lock (HostSubmitGate)
{
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);
try
{
@ -1164,29 +1329,42 @@ public static class AudioOut2Exports
foreach (var port in Ports.Values)
{
if (port.ContextHandle != context.Handle ||
port.PcmAddress == 0 ||
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat) ||
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
out var leftGain, out var rightGain))
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
{
continue;
}
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;
}
var byteLength = checked(frames * ch * bps);
if (byteLength <= 0 || byteLength > source.Length)
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
{
continue;
}
var sourceSpan = source.AsSpan(0, byteLength);
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
if (pcmData.Length < byteLength)
{
continue;
}
MixPortIntoStereo(
sourceSpan,
pcmData.AsSpan(0, byteLength),
mix.AsSpan(0, frames * 2),
frames,
ch,
@ -1238,12 +1416,70 @@ public static class AudioOut2Exports
finally
{
ArrayPool<float>.Shared.Return(mix);
ArrayPool<byte>.Shared.Return(source);
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 (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) =>
@ -1298,6 +1534,11 @@ public static class AudioOut2Exports
return false;
}
private static bool IsSnapshotPcmPort(ushort portType, int channels) =>
RoutesToSpeakers(portType) &&
(portType & 0xFF) != AudioOut2PortTypeVibration &&
(!IsObjectPort(portType) || channels == 1);
internal static void MixPortIntoStereo(
ReadOnlySpan<byte> source,
Span<float> mix,
@ -1453,13 +1694,19 @@ public static class AudioOut2Exports
private static bool IsDirectMemoryWindowAddress(ulong value) =>
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) =>
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) =>
value != 0 &&
value != 0x10000UL &&

View File

@ -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(
Nid = "ODJK2sn9w4A",
ExportName = "sceAvPlayerEnableStream",

View File

@ -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);
}

View File

@ -168,6 +168,14 @@ public static class JsonExports
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(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
@ -193,6 +201,14 @@ public static class JsonExports
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(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",

View File

@ -12,29 +12,82 @@ public static class Http2Exports
private const int Http2ErrorInvalidVersion = unchecked((int)0x817B106A);
private const int Http2ErrorInvalidId = unchecked((int)0x817B1100);
private const int Http2ErrorInvalidValue = unchecked((int)0x817B11FE);
private const int Http2ErrorNullPointer = unchecked((int)0x817B1225);
private const int Http2ErrorProhibited = unchecked((int)0x817B5224);
private static readonly ConcurrentDictionary<int, Http2Context> Contexts = new();
private static readonly ConcurrentDictionary<int, Http2Template> Templates = new();
private static readonly ConcurrentDictionary<int, Http2Request> Requests = new();
private static readonly ConcurrentDictionary<int, Http2CookieBox> CookieBoxes = new();
private static int _nextContextId;
private static int _nextTemplateId = 0x1000;
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 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()
{
Contexts.Clear();
Templates.Clear();
Requests.Clear();
CookieBoxes.Clear();
_nextContextId = 0;
_nextTemplateId = 0x1000;
_nextRequestId = 0x2000;
_nextCookieBoxId = 0x3000;
}
[SysAbiExport(
@ -119,7 +172,12 @@ public static class Http2Exports
}
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));
return ctx.SetReturn(id);
}
@ -140,6 +198,175 @@ public static class Http2Exports
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(
ExportName = "sceHttp2CreateRequestWithURL",
Target = Generation.Gen5,
@ -147,7 +374,7 @@ public static class Http2Exports
public static int Http2CreateRequestWithUrl(CpuContext ctx)
{
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Templates.ContainsKey(templateId))
if (!Templates.TryGetValue(templateId, out var template))
{
return ctx.SetReturn(Http2ErrorInvalidId);
}
@ -159,11 +386,73 @@ public static class Http2Exports
}
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]);
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(
ExportName = "sceHttp2DeleteRequest",
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)
{
value = string.Empty;

View File

@ -74,8 +74,10 @@ public static class SaveDataExports
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
// SceSaveDataEventType
private const uint EventTypeBackupEnd = 2;
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 uint SaveDataBlockSize = 65536;
private const ulong SaveDataBlocksMax = 16384;
@ -86,16 +88,79 @@ public static class SaveDataExports
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
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 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)
{
_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(
@ -131,7 +196,14 @@ public static class SaveDataExports
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
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))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -617,6 +689,11 @@ public static class SaveDataExports
ExportName = "sceSaveDataDirNameSearchPs4",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
[SysAbiExport(
Nid = "PHnuI4LhuRk",
ExportName = "sceSaveDataDirNameSearch2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataDirNameSearch(CpuContext ctx)
{
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)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
@ -1486,7 +1576,11 @@ public static class SaveDataExports
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
// 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).
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
EnqueueEvent(
EventTypeSaveDataMemorySyncEnd,
userId,
ResolveConfiguredTitleId(),
"sce_sdmemory");
return ctx.SetReturn(0);
}

View File

@ -64,14 +64,6 @@ public static class GameServiceStubs
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
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",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")]
public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx);

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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()
{
}
}
}

View File

@ -42,9 +42,12 @@ public sealed class AudioOut2PortGetStateExportsTests
// SceAudioOut2PortState: output@0, numChannels@2, volume@4.
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
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..]));
// 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[0x7F]);
}
@ -102,15 +105,19 @@ public sealed class AudioOut2PortGetStateExportsTests
Assert.Equal(0, result);
Span<byte> info = stackalloc byte[0x80];
Assert.True(memory.TryRead(StateAddress, info));
// type@0 = SPEAKER_TYPE_TV, availableBits@4 = FRONT_LEFT | FRONT_RIGHT.
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..]));
// aSpeakerAngle[0..1] = { -30, 0 }, { +30, 0 } degrees.
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[0x0C..]));
Assert.Equal(-30, BinaryPrimitives.ReadInt16LittleEndian(info[0x10..]));
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x12..]));
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[0x7F]);
}
[Fact]

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -26,6 +26,8 @@ public sealed class JsonExportRegistrationTests
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
("GvGvswb0v34", "_ZN3sce4Json14InitParameter2C2Ev"),
("W72B9ylU2JA", "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv"),
};
private static ModuleManager CreateRegisteredManager()
@ -131,4 +133,36 @@ public sealed class JsonExportRegistrationTests
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
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));
}
}

View File

@ -72,4 +72,276 @@ public sealed class Http2ExportsTests
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]);
}
}

View File

@ -44,11 +44,18 @@ public sealed class SaveDataExportsTests : IDisposable
private const ulong SearchResult = Base + 0x1140;
private const ulong SearchDirNames = Base + 0x1180;
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 ParameterError = unchecked((int)0x809F0000);
private const int MemoryFault = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
private const int ResourceBusy = unchecked((int)0x809F001B);
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 CpuContext _ctx;
@ -121,6 +128,65 @@ public sealed class SaveDataExportsTests : IDisposable
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]
public void SyncSaveDataMemory_PostsSyncEndEvent_DrainedOnce()
{
@ -145,6 +211,11 @@ public sealed class SaveDataExportsTests : IDisposable
Assert.True(_ctx.TryReadInt32(EventOut + 0x04, out var 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)));
}
@ -397,4 +468,14 @@ public sealed class SaveDataExportsTests : IDisposable
0,
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));
}
}