diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 3af1142..53a75e7 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -661,18 +661,30 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I finally { GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle); + _workAvailable.Dispose(); + } + } + + public void RequestStop() + { + lock (_gate) + { + if (_stopping) + { + return; + } + _stopping = true; + _workAvailable.Set(); } } public void Dispose() { - _stopping = true; - _workAvailable.Set(); + RequestStop(); if (!ReferenceEquals(Thread.CurrentThread, _thread)) { _thread.Join(500); } - _workAvailable.Dispose(); } } @@ -717,6 +729,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private int _readyGuestThreadCount; private readonly Dictionary _guestThreads = new Dictionary(); + private readonly Queue<(IVirtualMemory Memory, ulong StackBase, ulong StackSize, ulong TlsBase)> _reusableGuestThreadRegions = new(); private readonly Dictionary _externalGuestThreads = new Dictionary(); @@ -5342,6 +5355,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _readyGuestThreads.Clear(); Interlocked.Exchange(ref _readyGuestThreadCount, 0); _guestThreads.Clear(); + _reusableGuestThreadRegions.Clear(); _externalGuestThreads.Clear(); _pendingGuestExceptions.Clear(); Volatile.Write(ref _pendingGuestExceptionCount, 0); @@ -5380,13 +5394,50 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I stackSize = GuestThreadStackSize; } - if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, stackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error)) + ulong stackBase = 0; + ulong tlsBase = 0; + lock (_guestThreadGate) { - return false; + var count = _reusableGuestThreadRegions.Count; + for (var i = 0; i < count; i++) + { + var region = _reusableGuestThreadRegions.Dequeue(); + if (stackBase == 0 && ReferenceEquals(region.Memory, virtualMemory) && region.StackSize >= stackSize) + { + stackBase = region.StackBase; + stackSize = region.StackSize; + tlsBase = region.TlsBase; + } + else + { + _reusableGuestThreadRegions.Enqueue(region); + } + } } - if (!TryMapGuestThreadTlsRegion(virtualMemory, out var tlsBase, out error)) + if (stackBase == 0) { - return false; + if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, stackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out stackBase, out error)) + { + return false; + } + if (!TryMapGuestThreadTlsRegion(virtualMemory, out tlsBase, out error)) + { + return false; + } + } + else + { + Span zero = stackalloc byte[4096]; + zero.Clear(); + var start = tlsBase - GuestThreadTlsPrefixSize; + for (ulong offset = 0; offset < GuestThreadTlsPrefixSize + GuestThreadTlsSize; offset += (ulong)zero.Length) + { + if (!virtualMemory.TryWrite(start + offset, zero[..(int)Math.Min((ulong)zero.Length, GuestThreadTlsPrefixSize + GuestThreadTlsSize - offset)])) + { + error = "failed to reset reused guest TLS"; + return false; + } + } } var trackedMemory = new TrackedCpuMemory(virtualMemory); @@ -5851,6 +5902,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { pendingAfterExecutorRelease = pending; } + if (thread.State == GuestThreadRunState.Exited && + TryGetVirtualMemory(thread.Context, out var memory)) + { + _reusableGuestThreadRegions.Enqueue((memory, thread.StackBase, thread.StackSize, thread.Context.FsBase)); + thread.ExecutionRunner?.RequestStop(); + } } if (pendingAfterExecutorRelease is { } pendingException && !TryRaiseGuestException( diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index 51ba9a3..2cfc051 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -667,14 +667,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime var moduleDirectories = new[] { - // A linked PRX can be beside eboot.bin. Load only the app-root modules - // named by the main image at boot. Map the other app-root modules so - // sceKernelLoadStartModule can start them later. - (Path: ebootDirectory, StartAtBoot: true, LinkedOnly: true, SearchOption: SearchOption.TopDirectoryOnly), - (Path: ebootDirectory, StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly), (Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly), (Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly), (Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly), + // A linked PRX can be beside eboot.bin. Load only the app-root modules + // named by the main image at boot, after the normal module directories + // they may depend on. Map the other app-root modules so + // sceKernelLoadStartModule can start them later. + (Path: ebootDirectory, StartAtBoot: true, LinkedOnly: true, SearchOption: SearchOption.TopDirectoryOnly), + (Path: ebootDirectory, StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly), // Unity native plugins are loaded later through sceKernelLoadStartModule. Map // them up front so the HLE loader can return a real module handle and dlsym // can resolve their exports, but defer DT_INIT until the guest requests them. diff --git a/src/SharpEmu.Libs/AppContent/AppContentExports.cs b/src/SharpEmu.Libs/AppContent/AppContentExports.cs index 763147e..cde7f1e 100644 --- a/src/SharpEmu.Libs/AppContent/AppContentExports.cs +++ b/src/SharpEmu.Libs/AppContent/AppContentExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; using System.Buffers.Binary; using System.Text; using System.Text.Json; @@ -111,7 +112,7 @@ public static class AppContentExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - Directory.CreateDirectory(ResolveTemp0Root()); + Directory.CreateDirectory(KernelMemoryCompatExports.ResolveTemp0Root()); var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0"); if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes)) { @@ -163,7 +164,7 @@ public static class AppContentExports { try { - var storageRoot = ResolveTemp0Root(); + var storageRoot = KernelMemoryCompatExports.ResolveTemp0Root(); Directory.CreateDirectory(storageRoot); var volumeRoot = Path.GetPathRoot(Path.GetFullPath(storageRoot)); if (!string.IsNullOrWhiteSpace(volumeRoot)) @@ -240,28 +241,4 @@ public static class AppContentExports Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}"); } - private static string ResolveTemp0Root() - { - const string temp0VariableName = "SHARPEMU_TEMP0_DIR"; - var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName); - if (!string.IsNullOrWhiteSpace(configuredRoot)) - { - return Path.GetFullPath(configuredRoot); - } - - var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); - var appName = string.IsNullOrWhiteSpace(app0Root) - ? "default" - : Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root)); - if (string.IsNullOrWhiteSpace(appName)) - { - appName = "default"; - } - - var invalidChars = Path.GetInvalidFileNameChars(); - appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()); - var root = Path.Combine(AppContext.BaseDirectory, "user", "temp", appName, "temp0"); - Environment.SetEnvironmentVariable(temp0VariableName, root); - return root; - } } diff --git a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs index fdf686d..0acf6e3 100644 --- a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs +++ b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs @@ -351,6 +351,8 @@ public static class AvPlayerExports : VideoFrameReadResult.Pending; } + public int QueuedFrameCount => _frames.Reader.Count; + private void DecodeFrames() { try @@ -456,6 +458,12 @@ public static class AvPlayerExports public ulong LastVideoTimestamp { get; set; } public ulong EventDataBuffer { get; set; } public long NextFrameIndex { get; set; } + public long VideoCalls { get; set; } + public long VideoDelivered { get; set; } + public long VideoPending { get; set; } + public long VideoSyncWait { get; set; } + public long VideoWriteFailures { get; set; } + public long VideoStatsSecond { get; set; } = -1; public ulong AudioBufferBase { get; set; } public int NextAudioBuffer { get; set; } public long NextAudioFrameIndex { get; set; } @@ -498,6 +506,12 @@ public static class AvPlayerExports DisposePlaybackResources(); PlaybackClock.Reset(); NextFrameIndex = 0; + VideoCalls = 0; + VideoDelivered = 0; + VideoPending = 0; + VideoSyncWait = 0; + VideoWriteFailures = 0; + VideoStatsSecond = -1; LastGuestBuffer = 0; LastVideoTimestamp = 0; SeekVideoFramePending = false; @@ -793,6 +807,7 @@ public static class AvPlayerExports player.PlaybackClock.Stop(); player.FallbackPlayback?.Pause(); Console.Error.WriteLine($"[AVPLAYER][INFO] pause handle=0x{player.Handle:X16}"); + TraceVideoStats(player); } @@ -1619,6 +1634,14 @@ public static class AvPlayerExports return SetReturn(ctx, 0); } + player.VideoCalls++; + var statsSecond = player.PlaybackClock.ElapsedMilliseconds / 1000; + if (statsSecond > player.VideoStatsSecond) + { + player.VideoStatsSecond = statsSecond; + TraceVideoStats(player); + } + if (player.Paused) { if (player.SeekVideoFramePending && @@ -1658,47 +1681,26 @@ public static class AvPlayerExports } var fps = Math.Max(1.0, player.FramesPerSecond); + var expectedFrame = player.NextFrameIndex; if (player.AvSyncMode == AvSyncModeDefault) { - var expectedFrame = CalculateExpectedDefaultSyncVideoFrame( + expectedFrame = CalculateExpectedDefaultSyncVideoFrame( player.HasAudio, player.NextAudioFrameIndex, player.PlaybackClock.Elapsed.TotalSeconds, fps); if (player.NextFrameIndex > expectedFrame) { + player.VideoSyncWait++; return SetReturn(ctx, 0); } - var behind = expectedFrame - player.NextFrameIndex; - if (behind > MaxCatchUpFrames) - { - // Limit this call's work without changing the shared playback clock. - expectedFrame = player.NextFrameIndex + MaxCatchUpFrames; - TraceOnce( - "catch_up_capped", - $"catch_up capped behind={behind} max={MaxCatchUpFrames} " + - $"fps={fps:F3} {player.Width}x{player.Height}"); - } - - while (player.NextFrameIndex < expectedFrame) - { - var skippedFrame = ReadFrame(player); - if (skippedFrame == VideoFrameReadResult.Pending) - { - return SetReturn(ctx, 0); - } - if (skippedFrame == VideoFrameReadResult.End) - { - return FinishStream(ctx, player); - } - player.NextFrameIndex++; - } } var frameResult = ReadFrame(player); if (frameResult == VideoFrameReadResult.Pending) { + player.VideoPending++; return SetReturn(ctx, 0); } if (frameResult == VideoFrameReadResult.End) @@ -1707,9 +1709,23 @@ public static class AvPlayerExports } var frameIndex = player.NextFrameIndex; + // Only discard old frames when a replacement is already decoded. + for (var skipped = 0; + skipped < MaxCatchUpFrames && + player.NextFrameIndex < expectedFrame && + player.VideoDecoder!.QueuedFrameCount > 1; + skipped++) + { + player.NextFrameIndex++; + if (ReadFrame(player) != VideoFrameReadResult.Ready) + { + break; + } + frameIndex = player.NextFrameIndex; + } var timestamp = player.StartTimeMilliseconds + checked((ulong)Math.Round(frameIndex * 1000.0 / fps)); - player.NextFrameIndex++; + player.NextFrameIndex = frameIndex + 1; if (!WriteVideoFrame( ctx, player, @@ -1718,15 +1734,28 @@ public static class AvPlayerExports frameIndex, extended)) { + player.VideoWriteFailures++; return SetReturn(ctx, 0); } player.LastVideoTimestamp = timestamp; + player.VideoDelivered++; Trace($"video_frame handle=0x{player.Handle:X16} ex={extended} ts={timestamp} data=0x{player.LastGuestBuffer:X16}"); return SetReturn(ctx, 1); } } + private static void TraceVideoStats(PlayerState player) + { + Console.Error.WriteLine( + $"[AVPLAYER][INFO] video_stats handle=0x{player.Handle:X16} " + + $"clock_ms={player.PlaybackClock.ElapsedMilliseconds} audio_blocks={player.NextAudioFrameIndex} " + + $"calls={player.VideoCalls} delivered={player.VideoDelivered} " + + $"pending={player.VideoPending} sync_wait={player.VideoSyncWait} " + + $"write_fail={player.VideoWriteFailures} next_frame={player.NextFrameIndex} " + + $"queued={player.VideoDecoder?.QueuedFrameCount ?? 0} last_ts={player.LastVideoTimestamp}"); + } + private static int FinishStream(CpuContext ctx, PlayerState player) { TraceOnce( diff --git a/src/SharpEmu.Libs/Diagnostics/RazorCpuExports.cs b/src/SharpEmu.Libs/Diagnostics/RazorCpuExports.cs new file mode 100644 index 0000000..fd8e28e --- /dev/null +++ b/src/SharpEmu.Libs/Diagnostics/RazorCpuExports.cs @@ -0,0 +1,17 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Diagnostics; + +public static class RazorCpuExports +{ + [SysAbiExport(Nid = "zw+celG7zSI", ExportName = "sceRazorCpuPushMarker", + Target = Generation.Gen5, LibraryName = "libSceRazorCpu")] + public static int PushMarker(CpuContext ctx) => ctx.SetReturn(0); + + [SysAbiExport(Nid = "YpkGsMXP3ew", ExportName = "sceRazorCpuPopMarker", + Target = Generation.Gen5, LibraryName = "libSceRazorCpu")] + public static int PopMarker(CpuContext ctx) => ctx.SetReturn(0); +} diff --git a/src/SharpEmu.Libs/Fiber/FiberExports.cs b/src/SharpEmu.Libs/Fiber/FiberExports.cs index ea2f99e..93e5c9c 100644 --- a/src/SharpEmu.Libs/Fiber/FiberExports.cs +++ b/src/SharpEmu.Libs/Fiber/FiberExports.cs @@ -306,13 +306,7 @@ public static class FiberExports return SetReturn(ctx, FiberErrorNull); } - var fiberAddress = ResolveCurrentFiberAddress(ctx); - if (fiberAddress == 0) - { - return SetReturn(ctx, FiberErrorPermission); - } - - return TryWriteUInt64(ctx, outAddress, fiberAddress) + return TryWriteUInt64(ctx, outAddress, ResolveCurrentFiberAddress(ctx)) ? SetReturn(ctx, 0) : SetReturn(ctx, FiberErrorInvalid); } diff --git a/src/SharpEmu.Libs/Font/FontExports.cs b/src/SharpEmu.Libs/Font/FontExports.cs index 19409fb..972b255 100644 --- a/src/SharpEmu.Libs/Font/FontExports.cs +++ b/src/SharpEmu.Libs/Font/FontExports.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; using SharpEmu.HLE; namespace SharpEmu.Libs.Font; @@ -16,6 +18,8 @@ public static class FontExports private static readonly Stack FreeGlyphs = new(); private static ulong _librarySelectionAddress; private static ulong _rendererSelectionAddress; + private static readonly ConcurrentDictionary FontHeights = new(); + private static readonly Lazy BitmapFont = new(LoadBitmapFont); [SysAbiExport( Nid = "whrS4oksXc4", @@ -98,7 +102,7 @@ public static class FontExports ExportName = "sceFontSetScalePixel", Target = Generation.Gen5, LibraryName = "libSceFont")] - public static int SetScalePixel(CpuContext ctx) => SetSuccess(ctx); + public static int SetScalePixel(CpuContext ctx) => SetFontHeight(ctx); [SysAbiExport( Nid = "TMtqoFQjjbA", @@ -119,7 +123,7 @@ public static class FontExports ExportName = "sceFontSetupRenderScalePixel", Target = Generation.Gen5, LibraryName = "libSceFont")] - public static int SetupRenderScalePixel(CpuContext ctx) => SetSuccess(ctx); + public static int SetupRenderScalePixel(CpuContext ctx) => SetFontHeight(ctx); [SysAbiExport( Nid = "lz9y9UFO2UU", @@ -288,19 +292,9 @@ public static class FontExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f }; - for (var index = 0; index < values.Length; index++) - { - if (!TryWriteUInt32( - ctx, - metricsAddress + (ulong)(index * sizeof(float)), - BitConverter.SingleToUInt32Bits(values[index]))) - { - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); - } - } - - return SetSuccess(ctx); + return WriteGlyphMetrics(ctx, metricsAddress, GetFontHeight(ctx[CpuRegister.Rdi])) + ? SetSuccess(ctx) + : SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } [SysAbiExport( @@ -367,6 +361,26 @@ public static class FontExports return SetSuccess(ctx); } + [SysAbiExport( + Nid = "vRxf4d0ulPs", + ExportName = "sceFontRenderSurfaceSetScissor", + Target = Generation.Gen5, LibraryName = "libSceFont")] + public static int RenderSurfaceSetScissor(CpuContext ctx) + { + var surfaceAddress = ctx[CpuRegister.Rdi]; + if (surfaceAddress == 0) + { + return SetSuccess(ctx); + } + + return TryWriteUInt32(ctx, surfaceAddress + 0x18, (uint)ctx[CpuRegister.Rsi]) && + TryWriteUInt32(ctx, surfaceAddress + 0x1C, (uint)ctx[CpuRegister.Rdx]) && + TryWriteUInt32(ctx, surfaceAddress + 0x20, (uint)ctx[CpuRegister.Rcx]) && + TryWriteUInt32(ctx, surfaceAddress + 0x24, (uint)ctx[CpuRegister.R8]) + ? SetSuccess(ctx) + : SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + [SysAbiExport( Nid = "C-4Qw5Srlyw", ExportName = "sceFontGenerateCharGlyph", @@ -434,37 +448,162 @@ public static class FontExports LibraryName = "libSceFont")] public static int RenderCharGlyphImageHorizontal(CpuContext ctx) { + var surfaceAddress = ctx[CpuRegister.Rdx]; var metricsAddress = ctx[CpuRegister.Rcx]; var resultAddress = ctx[CpuRegister.R8]; - - if (metricsAddress != 0) + var height = GetFontHeight(ctx[CpuRegister.Rdi]); + var width = Math.Clamp((height + 1) / 2, 4, 128); + var rows = BitmapFont.Value; + if (rows is null) { - var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f }; - for (var index = 0; index < values.Length; index++) - { - if (!TryWriteUInt32( - ctx, - metricsAddress + (ulong)(index * sizeof(float)), - BitConverter.SingleToUInt32Bits(values[index]))) - { - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); - } - } + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } + if (metricsAddress != 0 && !WriteGlyphMetrics(ctx, metricsAddress, height)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + ctx.GetXmmRegister(0, out var xBits, out _); + ctx.GetXmmRegister(1, out var yBits, out _); + var x = BitConverter.UInt32BitsToSingle((uint)xBits); + var y = BitConverter.UInt32BitsToSingle((uint)yBits); + var left = float.IsFinite(x) ? Math.Max((int)x, 0) : 0; + var top = float.IsFinite(y) ? Math.Max((int)(y - height * 0.75f), 0) : 0; + + ulong buffer = 0; + uint pitch = 0; + uint pixelSize = 0; if (resultAddress != 0) { - Span cleared = stackalloc byte[RenderOutputSize]; - cleared.Clear(); - if (!ctx.Memory.TryWrite(resultAddress, cleared)) + Span result = stackalloc byte[RenderOutputSize]; + result.Clear(); + if (surfaceAddress != 0 && ctx.TryReadUInt64(surfaceAddress, out buffer) && + ctx.TryReadUInt32(surfaceAddress + 8, out pitch) && + ctx.TryReadUInt32(surfaceAddress + 12, out pixelSize)) + { + BinaryPrimitives.WriteUInt64LittleEndian(result[8..], buffer); + BinaryPrimitives.WriteUInt32LittleEndian(result[16..], pitch); + result[20] = (byte)pixelSize; + } + BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)left); + BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)top); + BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)width); + BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)height); + BinaryPrimitives.WriteSingleLittleEndian(result[44..], height * 0.75f); + BinaryPrimitives.WriteSingleLittleEndian(result[48..], width); + BinaryPrimitives.WriteSingleLittleEndian(result[52..], width); + BinaryPrimitives.WriteUInt32LittleEndian(result[56..], (uint)width); + BinaryPrimitives.WriteUInt32LittleEndian(result[60..], (uint)height); + if (!ctx.Memory.TryWrite(resultAddress, result)) { return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } } + if (surfaceAddress != 0 && !DrawGlyph(ctx, surfaceAddress, (uint)ctx[CpuRegister.Rsi], left, top, width, height, rows)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + return SetSuccess(ctx); } + private static int SetFontHeight(CpuContext ctx) + { + ctx.GetXmmRegister(1, out var heightBits, out _); + var height = BitConverter.UInt32BitsToSingle((uint)heightBits); + if (float.IsFinite(height) && height > 1) + { + FontHeights[ctx[CpuRegister.Rdi]] = Math.Clamp((int)(height + 0.5f), 8, 128); + } + return SetSuccess(ctx); + } + + private static int GetFontHeight(ulong font) => + FontHeights.TryGetValue(font, out var height) ? (int)height : 16; + + private static bool WriteGlyphMetrics(CpuContext ctx, ulong address, int height) + { + var width = Math.Clamp((height + 1) / 2, 4, 128); + Span metrics = stackalloc byte[32]; + float[] values = [width, height, 0, height * 0.75f, width, 0, 0, height]; + for (var index = 0; index < values.Length; index++) + { + BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]); + } + return ctx.Memory.TryWrite(address, metrics); + } + + internal static bool DrawGlyph(CpuContext ctx, ulong surface, uint code, int left, int top, + int glyphWidth, int glyphHeight, byte[] font) + { + if (!ctx.TryReadUInt64(surface, out var buffer) || + !ctx.TryReadUInt32(surface + 8, out var pitch) || + !ctx.TryReadUInt32(surface + 12, out var pixelSize) || + !ctx.TryReadUInt32(surface + 16, out var width) || + !ctx.TryReadUInt32(surface + 20, out var height) || + !ctx.TryReadUInt32(surface + 24, out var sx0) || + !ctx.TryReadUInt32(surface + 28, out var sy0) || + !ctx.TryReadUInt32(surface + 32, out var sx1) || + !ctx.TryReadUInt32(surface + 36, out var sy1)) + { + return false; + } + pixelSize &= 0xff; + if (buffer == 0 || pixelSize is < 1 or > 4 || pitch == 0 || width == 0 || height == 0) + { + return true; + } + + code = code < 256 ? code : (uint)'?'; + var x0 = Math.Max(left, (int)Math.Min(sx0, width)); + var y0 = Math.Max(top, (int)Math.Min(sy0, height)); + var x1 = Math.Min(left + glyphWidth, (int)Math.Min(sx1, width)); + var y1 = Math.Min(top + glyphHeight, (int)Math.Min(sy1, height)); + if (x0 >= x1 || y0 >= y1 || (ulong)x1 * pixelSize > pitch) + { + return true; + } + + var line = new byte[(x1 - x0) * pixelSize]; + for (var yy = y0; yy < y1; yy++) + { + var address = buffer + (ulong)yy * pitch + (ulong)x0 * pixelSize; + if (!ctx.Memory.TryRead(address, line)) + { + return false; + } + for (var xx = x0; xx < x1; xx++) + { + var sourceY = (yy - top) * 16 / glyphHeight; + var sourceX = (xx - left) * 8 / glyphWidth; + if ((font[code * 16 + sourceY] & (0x80 >> sourceX)) != 0) + { + line.AsSpan((xx - x0) * (int)pixelSize, (int)pixelSize).Fill(0xff); + } + } + if (!ctx.Memory.TryWrite(address, line)) + { + return false; + } + } + return true; + } + + private static byte[]? LoadBitmapFont() + { + var library = Path.Combine(AppContext.BaseDirectory, "plugins", "avutil-59.dll"); + if (!NativeLibrary.TryLoad(library, out var handle) || + !NativeLibrary.TryGetExport(handle, "avpriv_vga16_font", out var data)) + { + return null; + } + var font = new byte[4096]; + Marshal.Copy(data, font, 0, font.Length); + return font; + } + [SysAbiExport( Nid = "vzHs3C8lWJk", ExportName = "sceFontCloseFont", diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs b/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs index 50a0f04..b730757 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs @@ -306,6 +306,7 @@ public static class GuestPixelFormats private static readonly FormatFacts[] Facts = [ new(GuestPixelFormat.Bits8UNorm, 1, 0, 1, true, false), + new(GuestPixelFormat.Bits8UScaled, 1, 0, 1, true, false), new(GuestPixelFormat.Bits8SNorm, 0, 0, 1, false, false), new(GuestPixelFormat.Bits8UInt, 1, 0, 1, true, true), new(GuestPixelFormat.Bits16UNorm, 2, 0, 2, true, false), @@ -549,7 +550,13 @@ public static class GuestPixelFormats } public static GuestPixelFormat RemapTextureFormat(GuestPixelFormat format) => - format == GuestPixelFormat.Bits11_11_10UInt ? GuestPixelFormat.Bits32UInt : format; + format switch + { + GuestPixelFormat.Bits8UScaled => GuestPixelFormat.Bits8UNorm, + GuestPixelFormat.Bits8_8UScaled => GuestPixelFormat.Bits8_8UNorm, + GuestPixelFormat.Bits11_11_10UInt => GuestPixelFormat.Bits32UInt, + _ => format, + }; public static Format HostFormat(GuestPixelFormat format) => (uint)format < LookupSize ? HostLookup[(int)format] : Format.Undefined; diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 64404b3..4107de7 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -5177,7 +5177,7 @@ public static partial class KernelMemoryCompatExports return root; } - private static string ResolveTemp0Root() + internal static string ResolveTemp0Root() { const string temp0VariableName = "SHARPEMU_TEMP0_DIR"; var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName); @@ -5197,7 +5197,7 @@ public static partial class KernelMemoryCompatExports var invalidChars = Path.GetInvalidFileNameChars(); appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()); - var root = Path.Combine(AppContext.BaseDirectory, "user", "temp", appName, "temp0"); + var root = Path.Combine(Path.GetTempPath(), "SharpEmu", "temp0", appName, Path.GetRandomFileName()); Environment.SetEnvironmentVariable(temp0VariableName, root); return root; } diff --git a/src/SharpEmu.Libs/Media/FfmpegMediaStream.cs b/src/SharpEmu.Libs/Media/FfmpegMediaStream.cs index dc2bb7b..d2e7855 100644 --- a/src/SharpEmu.Libs/Media/FfmpegMediaStream.cs +++ b/src/SharpEmu.Libs/Media/FfmpegMediaStream.cs @@ -154,8 +154,7 @@ internal sealed unsafe class FfmpegMediaStream : Stream if (codecContext is null || ffmpeg.avcodec_parameters_to_context( codecContext, - formatContext->streams[streamIndex]->codecpar) < 0 || - ffmpeg.avcodec_open2(codecContext, decoder, null) < 0) + formatContext->streams[streamIndex]->codecpar) < 0) { if (codecContext is not null) { @@ -166,6 +165,18 @@ internal sealed unsafe class FfmpegMediaStream : Stream return false; } + if (mediaType == AVMediaType.AVMEDIA_TYPE_VIDEO) + { + codecContext->thread_count = 0; + codecContext->thread_type = ffmpeg.FF_THREAD_FRAME | ffmpeg.FF_THREAD_SLICE; + } + if (ffmpeg.avcodec_open2(codecContext, decoder, null) < 0) + { + ffmpeg.avcodec_free_context(&codecContext); + ffmpeg.avformat_close_input(&formatContext); + return false; + } + candidate._formatContext = formatContext; candidate._codecContext = codecContext; candidate._streamIndex = streamIndex; diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs index 2f01884..ed2c0dc 100644 --- a/src/SharpEmu.Libs/Network/NetExports.cs +++ b/src/SharpEmu.Libs/Network/NetExports.cs @@ -15,15 +15,21 @@ namespace SharpEmu.Libs.Network; public static class NetExports { private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109); + private const int NetErrorFault = unchecked((int)0x8041010E); private const int NetErrorInvalidArgument = unchecked((int)0x80410116); + private const int NetErrorTooManyFiles = unchecked((int)0x80410118); private const int NetErrorWouldBlock = unchecked((int)0x80410123); private const int NetErrorAddressInUse = unchecked((int)0x80410130); + private const int NetErrorResolverNoDns = unchecked((int)0x804101E1); private const int NetErrorNotInitialized = unchecked((int)0x804101C8); private const int NetErrnoBadFileDescriptor = 9; + private const int NetErrnoFault = 14; private const int NetErrnoInvalidArgument = 22; + private const int NetErrnoTooManyFiles = 24; private const int NetErrnoWouldBlock = 35; private const int NetErrnoAddressInUse = 48; private const int NetErrnoNotInitialized = 200; + private const int NetErrnoResolverNoDns = 225; private const int MaxNameLength = 256; private static ReadOnlySpan OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50]; @@ -32,7 +38,8 @@ public static class NetExports private static readonly ConcurrentDictionary _sockets = new(); private static int _nextPoolId; private static int _nextResolverId = 0x2000; - private static int _nextSocketId = 0x4000; + private static int _nextSocketId = 255; + private static readonly object _socketIdGate = new(); // The platform networking module is usable immediately after it is loaded. // Games and middleware (notably FMOD) can create internal sockets before an // explicit sceNetInit call reaches application code. @@ -164,8 +171,12 @@ public static class NetExports try { var socket = new Socket(addressFamily, socketType, protocolType); - var id = Interlocked.Increment(ref _nextSocketId); - _sockets[id] = socket; + var id = AddSocket(socket); + if (id < 0) + { + socket.Dispose(); + return SetNetError(ctx, NetErrorTooManyFiles, NetErrnoTooManyFiles); + } TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol)); ctx[CpuRegister.Rax] = unchecked((ulong)id); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -194,6 +205,110 @@ public static class NetExports return ctx.SetReturn(0); } + [SysAbiExport( + Nid = "9wO9XrMsNhc", + ExportName = "sceNetRecv", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetRecv(CpuContext ctx) => ReceiveSocket(ctx, withAddress: false); + + [SysAbiExport( + Nid = "304ooNZxWDY", + ExportName = "sceNetRecvfrom", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetRecvfrom(CpuContext ctx) => ReceiveSocket(ctx, withAddress: true); + + private static int ReceiveSocket(CpuContext ctx, bool withAddress) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + var bufferAddress = ctx[CpuRegister.Rsi]; + var requested = ctx[CpuRegister.Rdx]; + var flags = unchecked((int)ctx[CpuRegister.Rcx]); + var address = withAddress ? ctx[CpuRegister.R8] : 0; + var addressLength = withAddress ? ctx[CpuRegister.R9] : 0; + if (!_sockets.TryGetValue(id, out var socket)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + if (bufferAddress == 0 || (address != 0 && addressLength == 0) || + (flags & ~(0x2 | 0x4 | 0x80 | 0x20000)) != 0) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if (requested == 0) + { + return ctx.SetReturn(0); + } + + var count = (int)Math.Min(requested, 1UL << 20); + var socketFlags = (flags & 0x2) != 0 ? SocketFlags.Peek : SocketFlags.None; + + try + { + if ((flags & 0x80) != 0 && !socket.Poll(0, SelectMode.SelectRead)) + { + return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock); + } + + var payload = new byte[count]; + IPEndPoint? endpoint = null; + int received; + if (address == 0) + { + received = socket.Receive(payload, socketFlags); + } + else + { + EndPoint remote = new IPEndPoint(IPAddress.Any, 0); + received = socket.ReceiveFrom(payload, socketFlags, ref remote); + endpoint = remote as IPEndPoint; + } + if (!ctx.Memory.TryWrite(bufferAddress, payload.AsSpan(0, received))) + { + return SetNetError(ctx, NetErrorFault, NetErrnoFault); + } + + if (address != 0 && endpoint is not null) + { + Span lengthBytes = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(addressLength, lengthBytes)) + { + return SetNetError(ctx, NetErrorFault, NetErrnoFault); + } + + Span guestAddress = stackalloc byte[16]; + guestAddress[0] = 16; + guestAddress[1] = 2; + BinaryPrimitives.WriteUInt16BigEndian(guestAddress[2..], unchecked((ushort)endpoint.Port)); + endpoint.Address.MapToIPv4().GetAddressBytes().CopyTo(guestAddress[4..]); + var copyLength = (int)Math.Min(BinaryPrimitives.ReadUInt32LittleEndian(lengthBytes), 16u); + BinaryPrimitives.WriteUInt32LittleEndian(lengthBytes, 16); + if (!ctx.Memory.TryWrite(address, guestAddress[..copyLength]) || + !ctx.Memory.TryWrite(addressLength, lengthBytes)) + { + return SetNetError(ctx, NetErrorFault, NetErrnoFault); + } + } + + return ctx.SetReturn(received); + } + catch (SocketException exception) when (exception.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending or SocketError.TimedOut) + { + return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock); + } + catch (SocketException) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + catch (ObjectDisposedException) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + } + [SysAbiExport( Nid = "2mKX2Spso7I", ExportName = "sceNetSetsockopt", @@ -563,8 +678,12 @@ public static class NetExports try { var accepted = socket.Accept(); - var acceptedId = Interlocked.Increment(ref _nextSocketId); - _sockets[acceptedId] = accepted; + var acceptedId = AddSocket(accepted); + if (acceptedId < 0) + { + accepted.Dispose(); + return SetNetError(ctx, NetErrorTooManyFiles, NetErrnoTooManyFiles); + } TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0); ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -771,6 +890,58 @@ public static class NetExports return ctx.SetReturn(result); } + [SysAbiExport( + Nid = "Nd91WaWmG2w", + ExportName = "sceNetResolverStartNtoa", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetResolverStartNtoa(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + if (!_resolvers.TryGetValue(id, out var resolver)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + var destination = ctx[CpuRegister.Rdx]; + var flags = unchecked((int)ctx[CpuRegister.R9]); + if (destination == 0 || ctx[CpuRegister.Rsi] == 0 || + !TryReadUtf8Z(ctx, ctx[CpuRegister.Rsi], MaxNameLength, out var hostname) || + (flags & ~0x10000) != 0) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if ((flags & 0x10000) == 0 && IPAddress.TryParse(hostname, out var parsed) && + parsed.AddressFamily == AddressFamily.InterNetwork) + { + return ctx.Memory.TryWrite(destination, parsed.GetAddressBytes()) + ? ctx.SetReturn(0) + : SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + _resolvers.TryUpdate(id, resolver with { LastError = NetErrorResolverNoDns }, resolver); + return SetNetError(ctx, NetErrorResolverNoDns, NetErrnoResolverNoDns); + } + + private static int AddSocket(Socket socket) + { + lock (_socketIdGate) + { + // Guest fd_set has 1024 bits; IDs outside it corrupt the guest stack. + for (var attempt = 0; attempt < 768; attempt++) + { + _nextSocketId = _nextSocketId == 1023 ? 256 : _nextSocketId + 1; + if (_sockets.TryAdd(_nextSocketId, socket)) + { + return _nextSocketId; + } + } + } + + return -1; + } + private static bool TryTranslateSocketParameters( int family, int type, @@ -900,6 +1071,13 @@ public static class NetExports return 1; } + [SysAbiExport( + Nid = "9vA2aW+CHuA", + ExportName = "sceNetInetNtop", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetInetNtop(CpuContext ctx) => PosixInetNtop(ctx); + private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2) { if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal)) diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs index 4f69258..08c237f 100644 --- a/src/SharpEmu.Libs/Np/NpManagerExports.cs +++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs @@ -261,6 +261,26 @@ public static class NpManagerExports : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + [SysAbiExport( + Nid = "Oad3rvY-NJQ", + ExportName = "sceNpHasSignedUp", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpManager")] + public static int NpHasSignedUp(CpuContext ctx) + { + var resultAddress = ctx[CpuRegister.Rsi]; + if (resultAddress == 0 || unchecked((int)ctx[CpuRegister.Rdi]) == -1) + { + return SetReturn(ctx, NpErrorInvalidArgument); + } + + Span signedUp = stackalloc byte[1]; + signedUp[0] = 0; // No online account is configured in the offline profile. + return ctx.Memory.TryWrite(resultAddress, signedUp) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + [SysAbiExport( Nid = "Ec63y59l9tw", ExportName = "sceNpSetNpTitleId", diff --git a/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs b/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs index 49eb507..5e95606 100644 --- a/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs +++ b/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs @@ -7,6 +7,7 @@ namespace SharpEmu.Libs.Np; public static class NpSessionSignalingExports { + private const int NpErrorSignedOut = unchecked((int)0x80550006); private static int _nextContextId; [SysAbiExport( @@ -31,4 +32,18 @@ public static class NpSessionSignalingExports ctx[CpuRegister.Rax] = unchecked((ulong)contextId); return contextId; } + + [SysAbiExport( + Nid = "r8mVMwlafF8", + ExportName = "sceNpSessionSignalingRequestPrepare", + Target = Generation.Gen5, + LibraryName = "libSceNpSessionSignaling")] + public static int NpSessionSignalingRequestPrepare(CpuContext ctx) => ctx.SetReturn(NpErrorSignedOut); + + [SysAbiExport( + Nid = "Z9Q9LzQDXf0", + ExportName = "sceNpSessionSignalingDestroyContext", + Target = Generation.Gen5, + LibraryName = "libSceNpSessionSignaling")] + public static int NpSessionSignalingDestroyContext(CpuContext ctx) => ctx.SetReturn(0); } diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index b6225a5..dbe6621 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -217,46 +217,21 @@ public static class PadExports : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + [SysAbiExport( + Nid = "PZSoY8j0Pko", + ExportName = "scePadGetFeatureReport", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadGetFeatureReport(CpuContext ctx) => + ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); + [SysAbiExport( Nid = "hGbf2QTBmqc", ExportName = "scePadGetExtControllerInformation", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libScePad")] - public static int PadGetExtControllerInformation(CpuContext ctx) - { - var handle = unchecked((int)ctx[CpuRegister.Rdi]); - var informationAddress = ctx[CpuRegister.Rsi]; - if (!IsPrimaryPadHandle(handle)) - { - return ctx.SetReturn(OrbisPadErrorInvalidHandle); - } - - if (informationAddress == 0) - { - return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); - } - - // Base ScePadControllerInformation + device-class/connection fields: report a connected - // DualSense so the guest's open -> get-ext-info -> close probe loop resolves. - Span information = stackalloc byte[0x40]; - information.Clear(); - BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f); - BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920); - BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943); - information[0x08] = 30; - information[0x09] = 30; - information[0x0A] = StandardPortType; - information[0x0B] = 1; // connected count - information[0x0C] = 1; // connected - BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0); - information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense - information[0x1D] = 1; // connected (ext) - information[0x1E] = 0; // connectionType: local - - return ctx.Memory.TryWrite(informationAddress, information) - ? ctx.SetReturn(0) - : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); - } + public static int PadGetExtControllerInformation(CpuContext ctx) => + ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); [SysAbiExport( Nid = "AcslpN1jHR8", diff --git a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs index 197fa7b..7738964 100644 --- a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs +++ b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs @@ -76,10 +76,44 @@ public static class GameServiceStubs Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")] public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx); + [SysAbiExport(Nid = "JlpJVoRWv7U", ExportName = "sceSigninDialogOpen", + Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")] + public static int SigninDialogOpen(CpuContext ctx) => Ok(ctx); + + [SysAbiExport(Nid = "Bw31liTFT3A", ExportName = "sceSigninDialogUpdateStatus", + Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")] + public static int SigninDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(3); + + [SysAbiExport(Nid = "nqG7rqnYw1U", ExportName = "sceSigninDialogGetResult", + Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")] + public static int SigninDialogGetResult(CpuContext ctx) + { + var address = ctx[CpuRegister.Rdi]; + if (address == 0) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + Span result = stackalloc byte[16]; + System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(result, 1); // User canceled. + return ctx.Memory.TryWrite(address, result) + ? Ok(ctx) + : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + [SysAbiExport(Nid = "LXlmS6PvJdU", ExportName = "sceSigninDialogTerminate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")] public static int SigninDialogTerminate(CpuContext ctx) => Ok(ctx); + [SysAbiExport(Nid = "UOjiprYwVNw", ExportName = "sceTextToSpeech2Initialize", + Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")] + public static int TextToSpeech2Initialize(CpuContext ctx) => Ok(ctx); + + [SysAbiExport(Nid = "X0HZNbSiqyg", ExportName = "sceTextToSpeech2Open", + Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")] + public static int TextToSpeech2Open(CpuContext ctx) => + ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED); + [SysAbiExport(Nid = "kvYEw2lBndk", ExportName = "sceGameLiveStreamingInitialize", Target = Generation.Gen5, LibraryName = "libSceGameLiveStreaming")] public static int GameLiveStreamingInitialize(CpuContext ctx) => Ok(ctx); diff --git a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs index ba4bb05..c56e059 100644 --- a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs +++ b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs @@ -27,6 +27,20 @@ public static class SystemServiceExports Volatile.Write(ref _systemLanguage, systemLanguage); } + [SysAbiExport( + Nid = "x1UB9bwDSOw", + ExportName = "sceSystemServiceDisableMusicPlayer", + Target = Generation.Gen5, + LibraryName = "libSceSystemService")] + public static int SystemServiceDisableMusicPlayer(CpuContext ctx) => ctx.SetReturn(0); + + [SysAbiExport( + Nid = "9kPCz7Or+1Y", + ExportName = "sceSystemServiceReenableMusicPlayer", + Target = Generation.Gen5, + LibraryName = "libSceSystemService")] + public static int SystemServiceReenableMusicPlayer(CpuContext ctx) => ctx.SetReturn(0); + [SysAbiExport( Nid = "3RQ5aQfnstU", ExportName = "sceSystemServiceGetNoticeScreenSkipFlag", diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 0470bbb..cc43361 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -862,6 +862,28 @@ public static partial class VideoOutExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + #pragma warning disable SHEM006 + [SysAbiExport( + Nid = "kP2L8t3j-aM", + ExportName = "VideoOutVrrStatus_kP2L8t3j_aM", + Target = Generation.Gen5, + LibraryName = "libSceVideoOutVrrStatus")] + public static int VideoOutVrrStatus(CpuContext ctx) => ctx.SetReturn(0); + + [SysAbiExport( + Nid = "5tRaBjtdTzY", + ExportName = "sceVideoOutVrrPegToFixedRate", + Target = Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutVrrPegToFixedRate(CpuContext ctx) => ctx.SetReturn(0); + + [SysAbiExport( + Nid = "T4ucGB8CsnM", + ExportName = "sceVideoOutVrrUnpegFromFixedRate", + Target = Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutVrrUnpegFromFixedRate(CpuContext ctx) => ctx.SetReturn(0); + [SysAbiExport( Nid = "Xru92wHJRmg", ExportName = "sceVideoOutAddVblankEvent", diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 4c4a7b9..3aeff81 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -5114,6 +5114,25 @@ public static partial class Gen5SpirvTranslator SpirvImageResource resource, uint texel) { + if (resource.ConversionFormat is GuestImageFormat.Format8Uscaled or GuestImageFormat.Format8x2Uscaled) + { + var scaled = _module.AddInstruction(SpirvOp.FMul, resource.VectorType, texel, + _module.AddInstruction(SpirvOp.CompositeConstruct, resource.VectorType, + Float(255), Float(255), Float(1), Float(1))); + var channels = new uint[4]; + for (var component = 0; component < channels.Length; component++) + { + var selector = (resource.ShaderSwizzle >> (component * 3)) & 7u; + channels[component] = selector switch + { + 1u => Float(1), + >= 4u => _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, scaled, selector - 4u), + _ => Float(0), + }; + } + return _module.AddInstruction(SpirvOp.CompositeConstruct, resource.VectorType, channels); + } + if (!TryGetPackedImageConversion( resource, out var componentCount, diff --git a/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs b/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs index 4589323..56883f7 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs @@ -8,6 +8,8 @@ namespace SharpEmu.ShaderCompiler.Resources; public static class GuestImageFormat { public const uint Invalid = 0; + public const uint Format8Uscaled = 3; + public const uint Format8x2Uscaled = 16; public const uint Format16Unorm = 7; public const uint Format32Uint = 20; public const uint Format32Sint = 21; @@ -32,7 +34,7 @@ public static class GuestImageFormat // is unsupported for sampling. private static readonly (uint Format, ImageNumericClass Class)[] SampledFormats = [ - (1, ImageNumericClass.Float), (5, ImageNumericClass.Uint), (7, ImageNumericClass.Float), (8, ImageNumericClass.Float), + (1, ImageNumericClass.Float), (Format8Uscaled, ImageNumericClass.Float), (5, ImageNumericClass.Uint), (7, ImageNumericClass.Float), (8, ImageNumericClass.Float), (11, ImageNumericClass.Uint), (12, ImageNumericClass.Sint), (13, ImageNumericClass.Float), (14, ImageNumericClass.Float), (15, ImageNumericClass.Float), (16, ImageNumericClass.Float), (18, ImageNumericClass.Uint), (19, ImageNumericClass.Sint), (20, ImageNumericClass.Uint), (21, ImageNumericClass.Sint), (22, ImageNumericClass.Float), (23, ImageNumericClass.Float), (24, ImageNumericClass.Float), @@ -67,7 +69,13 @@ public static class GuestImageFormat } // The 11_11_10 integer format is read as one 32-bit integer. - public static uint Remap(uint format) => format == Format11x2x10Uint ? Format32Uint : format; + public static uint Remap(uint format) => format switch + { + Format8Uscaled => 1, + Format8x2Uscaled => 14, + Format11x2x10Uint => Format32Uint, + _ => format, + }; public static uint ImageTypeOf(ReadOnlySpan descriptor) => (descriptor[3] >> 28) & 0xF; diff --git a/tests/SharpEmu.Libs.Tests/Fiber/FiberExportsTests.cs b/tests/SharpEmu.Libs.Tests/Fiber/FiberExportsTests.cs index e750a5e..07ba5fd 100644 --- a/tests/SharpEmu.Libs.Tests/Fiber/FiberExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Fiber/FiberExportsTests.cs @@ -70,7 +70,7 @@ public sealed class FiberExportsTests } [Fact] - public void GetSelf_OutsideFiberContext_ReturnsPermissionError() + public void GetSelf_OutsideFiberContext_ReturnsNullFiberAddress() { var memory = new FakeCpuMemory(Base, RegionSize); var context = new CpuContext(memory, Generation.Gen5); @@ -79,7 +79,8 @@ public sealed class FiberExportsTests var result = FiberExports.FiberGetSelf(context); - Assert.Equal(ErrorPermission, result); + Assert.Equal(0, result); + Assert.Equal(0UL, ReadUInt64(memory, Base + 0x100)); } [Fact] diff --git a/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs b/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs index 849f7a6..ab5a2ee 100644 --- a/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Font/FontExportsTests.cs @@ -69,4 +69,30 @@ public sealed class FontExportsTests (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, FontExports.GetVerticalLayout(_ctx)); } + + [Fact] + public void DrawGlyph_WritesOnlySetPixelsInsideScissor() + { + const ulong surface = Base + 0x200; + const ulong pixels = Base + 0x300; + Span descriptor = stackalloc byte[0x28]; + BinaryPrimitives.WriteUInt64LittleEndian(descriptor, pixels); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[8..], 8); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[12..], 1); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[16..], 8); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[20..], 1); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[24..], 2); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[28..], 0); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[32..], 6); + BinaryPrimitives.WriteUInt32LittleEndian(descriptor[36..], 1); + Assert.True(_ctx.Memory.TryWrite(surface, descriptor)); + + var font = new byte[4096]; + font['A' * 16] = 0xff; + Assert.True(FontExports.DrawGlyph(_ctx, surface, 'A', 0, 0, 8, 1, font)); + + Span result = stackalloc byte[8]; + Assert.True(_ctx.Memory.TryRead(pixels, result)); + Assert.Equal(new byte[] { 0, 0, 255, 255, 255, 255, 0, 0 }, result.ToArray()); + } } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs index 31b1a2e..3541db0 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs @@ -3,6 +3,7 @@ using SharpEmu.Libs.Gpu.Images; using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.Libs.Tests.Gpu.Scheduling; using SharpEmu.Libs.Tests.Gpu.Vulkan; @@ -17,6 +18,25 @@ public sealed class ImageRequestBuildersTests : IClassFixture guard = stackalloc byte[8]; + Assert.True(_memory.TryRead(cookieAddress, guard)); + Assert.Equal(cookie, BitConverter.ToUInt64(guard)); + } + [Theory] [InlineData(2)] [InlineData(-1)]