From b02c06181a296f4dc7d9ee2a44780a6ecd959592 Mon Sep 17 00:00:00 2001 From: Berk <12572227+par274@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:47:42 +0300 Subject: [PATCH] Unity fixes 1016p2 (#1062) * [Audio] Snapshot AudioOut2 PCM when the guest submits it Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Drop the ports of a destroyed AudioOut2 context Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Accept sceAudioOut2MasteringSetParam Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Resolve AudioOut2 out-buffers from either ABI slot Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Report the AudioOut2 context memory size through its out pointer Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Keep the AudioOut2 queue level and availability pointers apart Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Report full volume in the AudioOut2 port state Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Trace AudioOut2 port, attribute and bed calls Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Let tests replace the AudioOut2 host streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Audio] Add the AudioOut2 vibration port type Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [SaveData] Lay out save data events with their title ID Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [SaveData] Add sceSaveDataBackup Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [SaveData] Add sceSaveDataDirNameSearch2 Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Network] Track Http2 template and request options Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Network] Add Http2 option, cookie and request exports Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [CommonDialog] Add the web browser dialog lifecycle Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Json] Add the InitParameter2 base constructor Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Json] Add InitParameterRtti2::setAllocatorRtti Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [AvPlayer] Accept available bandwidth hints Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Tests] Cover the AudioOut2, save data, Http2 and dialog exports Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> --------- Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> --- src/SharpEmu.Libs/Audio/AudioOut2Exports.cs | 361 +++++++++++++--- src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs | 26 ++ .../CommonDialog/WebBrowserDialogExports.cs | 171 ++++++++ src/SharpEmu.Libs/Json/JsonExports.cs | 16 + src/SharpEmu.Libs/Network/Http2Exports.cs | 393 +++++++++++++++++- src/SharpEmu.Libs/SaveData/SaveDataExports.cs | 108 ++++- src/SharpEmu.Libs/Stubs/GameServiceStubs.cs | 8 - ...dioOut2ContextGetQueueLevelExportsTests.cs | 92 ++++ ...AudioOut2ContextQueryMemoryExportsTests.cs | 68 +++ .../Audio/AudioOut2MasteringExportsTests.cs | 40 ++ .../Audio/AudioOut2PcmSnapshotTests.cs | 255 ++++++++++++ .../AudioOut2PortGetStateExportsTests.cs | 19 +- .../AvPlayer/AvPlayerControlTests.cs | 54 +++ .../WebBrowserDialogExportsTests.cs | 65 +++ .../Json/JsonExportRegistrationTests.cs | 34 ++ .../Network/Http2ExportsTests.cs | 272 ++++++++++++ .../SaveData/SaveDataExportsTests.cs | 81 ++++ 17 files changed, 1980 insertions(+), 83 deletions(-) create mode 100644 src/SharpEmu.Libs/CommonDialog/WebBrowserDialogExports.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index de77a4b..4d3d2d5 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -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 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? _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 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 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 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 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 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 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.Shared.Rent(frames * 2); - var source = ArrayPool.Shared.Rent(frames * 16 * sizeof(float)); var output = ArrayPool.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.Shared.Return(mix); - ArrayPool.Shared.Return(source); ArrayPool.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? 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 source, Span 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 && diff --git a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs index 199a41d..dfb5d3d 100644 --- a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs +++ b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs @@ -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", diff --git a/src/SharpEmu.Libs/CommonDialog/WebBrowserDialogExports.cs b/src/SharpEmu.Libs/CommonDialog/WebBrowserDialogExports.cs new file mode 100644 index 0000000..78340d0 --- /dev/null +++ b/src/SharpEmu.Libs/CommonDialog/WebBrowserDialogExports.cs @@ -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; + +/// +/// 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. +/// +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 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); +} diff --git a/src/SharpEmu.Libs/Json/JsonExports.cs b/src/SharpEmu.Libs/Json/JsonExports.cs index e83e22e..ba3fc3d 100644 --- a/src/SharpEmu.Libs/Json/JsonExports.cs +++ b/src/SharpEmu.Libs/Json/JsonExports.cs @@ -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", diff --git a/src/SharpEmu.Libs/Network/Http2Exports.cs b/src/SharpEmu.Libs/Network/Http2Exports.cs index 4502745..c3dd043 100644 --- a/src/SharpEmu.Libs/Network/Http2Exports.cs +++ b/src/SharpEmu.Libs/Network/Http2Exports.cs @@ -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 Contexts = new(); private static readonly ConcurrentDictionary Templates = new(); private static readonly ConcurrentDictionary Requests = new(); + private static readonly ConcurrentDictionary 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 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 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; diff --git a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs index c0c2260..84038bf 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs @@ -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 _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 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 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); } diff --git a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs index feae3ac..0d57f65 100644 --- a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs +++ b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs @@ -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); diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs new file mode 100644 index 0000000..2e7a5b1 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs @@ -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 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 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 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)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs new file mode 100644 index 0000000..1ebfe22 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs @@ -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 param = stackalloc byte[0x40]; + param.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 4); + Assert.True(memory.TryWrite(ParamAddress, param)); + + Span 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 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 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)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs new file mode 100644 index 0000000..457fc7c --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs @@ -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); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs new file mode 100644 index 0000000..8a79ae7 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs @@ -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 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 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 pcm = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(pcm, SharedPcmAddress); + Assert.True(_memory.TryWrite(PcmAttributeValueAddress, pcm)); + + Span 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 value = stackalloc byte[8]; + Assert.True(_memory.TryRead(address, value)); + return BinaryPrimitives.ReadUInt64LittleEndian(value); + } + + private sealed class RecordingAudioStream : IHostAudioStream + { + public List Submissions { get; } = []; + + public bool Submit(ReadOnlySpan stereoPcm16) + { + Submissions.Add(stereoPcm16.ToArray()); + return true; + } + + public void Dispose() + { + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs index 0119875..d3ab7b5 100644 --- a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs @@ -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 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] diff --git a/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs new file mode 100644 index 0000000..9701ee8 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs @@ -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); +} diff --git a/tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs b/tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs new file mode 100644 index 0000000..f5bc4a6 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs @@ -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 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); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs index f51f7ea..ce5227d 100644 --- a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs +++ b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs @@ -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)); + } } diff --git a/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs b/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs index 5da305c..0c6acd1 100644 --- a/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs @@ -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[] 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]); + } } diff --git a/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs b/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs index 970d39c..4ae3e75 100644 --- a/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs @@ -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 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)); + } }