Compare commits

...

21 Commits

Author SHA1 Message Date
ParantezTech 4fd68a8756 [Kernel] Isolate temp0 storage per game process 2026-09-30 00:20:12 +03:00
ParantezTech 27a1cf542d [Cpu] Reuse exited guest thread stack and TLS regions 2026-09-30 00:19:53 +03:00
ParantezTech 8b7f4a79fd [VideoOut] Add sceVideoOutVrrUnpegFromFixedRate stub 2026-09-29 18:49:43 +03:00
ParantezTech 8e6281f04e [SystemService] Added sceSystemServiceReenableMusicPlayer stub 2026-09-29 18:49:08 +03:00
ParantezTech 294d6c3548 [Runtime] Module loader correct order: libc > libSceNpCppWebApi > libScePfs > logiWheel 2026-09-29 18:38:56 +03:00
ParantezTech 71d1345e51 [Pad] For now, extra controller definitions should remain as stubs 2026-09-29 18:38:08 +03:00
ParantezTech 0d90e009c2 [Pad] Added scePadGetFeatureReport stub 2026-09-29 18:21:01 +03:00
ParantezTech 88b0e1565e [Fiber] updated current fiber address test 2026-09-29 15:24:14 +03:00
ParantezTech b2d1bc8254 [RazorCpu] added marker stubs 2026-09-29 15:01:51 +03:00
ParantezTech b36790cff9 [VideoOut] added VRR stubs 2026-09-29 15:01:51 +03:00
ParantezTech 85d6c7d6e4 [SystemService] added music player disable stub 2026-09-29 15:01:51 +03:00
ParantezTech 839f7cbd97 [TextToSpeech] added initialization stubs 2026-09-29 15:01:51 +03:00
ParantezTech f2bff69750 [SigninDialog] added dialog stubs 2026-09-29 15:01:51 +03:00
ParantezTech 6ab0edad99 [NpSessionSignaling] added signed-out request stubs 2026-09-29 15:00:41 +03:00
ParantezTech d9eafdbca6 [NpManager] added offline signup status 2026-09-29 15:00:41 +03:00
ParantezTech e302816af1 [Network] added socket receive and resolver support 2026-09-29 15:00:41 +03:00
ParantezTech d13e785618 [GPU] added unsigned scaled texture sampling 2026-09-29 15:00:41 +03:00
ParantezTech 7791a5cddc [Font] added bitmap glyph rendering 2026-09-29 15:00:41 +03:00
ParantezTech 1d28a6b169 [Fiber] fixed current fiber address query 2026-09-29 15:00:41 +03:00
ParantezTech a5d60a3720 [AvPlayer] enabled threaded video decoding 2026-09-29 15:00:41 +03:00
ParantezTech c4543a0ed1 [AvPlayer] improved video playback sync and stats 2026-09-29 15:00:41 +03:00
24 changed files with 784 additions and 151 deletions

View File

@ -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<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
private readonly Queue<(IVirtualMemory Memory, ulong StackBase, ulong StackSize, ulong TlsBase)> _reusableGuestThreadRegions = new();
private readonly Dictionary<ulong, ExternalGuestThreadState> _externalGuestThreads = new Dictionary<ulong, ExternalGuestThreadState>();
@ -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<byte> 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(

View File

@ -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.

View File

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

View File

@ -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(

View File

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

View File

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

View File

@ -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<ulong> FreeGlyphs = new();
private static ulong _librarySelectionAddress;
private static ulong _rendererSelectionAddress;
private static readonly ConcurrentDictionary<ulong, float> FontHeights = new();
private static readonly Lazy<byte[]?> 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<byte> cleared = stackalloc byte[RenderOutputSize];
cleared.Clear();
if (!ctx.Memory.TryWrite(resultAddress, cleared))
Span<byte> 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<byte> 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",

View File

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

View File

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

View File

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

View File

@ -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<byte> OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
@ -32,7 +38,8 @@ public static class NetExports
private static readonly ConcurrentDictionary<int, Socket> _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<byte> lengthBytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(addressLength, lengthBytes))
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
Span<byte> 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))

View File

@ -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<byte> 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",

View File

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

View File

@ -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<byte> 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",

View File

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

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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<uint> descriptor) => (descriptor[3] >> 28) & 0xF;

View File

@ -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]

View File

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

View File

@ -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<HeadlessVulkanFixt
{
private const ulong Base = 0x1_0000_0000;
[Fact]
public void EightBitUnsignedScaledTextureUsesUnormBackingWithShaderConversion()
{
Assert.Equal(ImageNumericClass.Float, GuestImageFormat.SampledNumericClass(GuestImageFormat.Format8Uscaled));
Assert.Equal(1u, GuestImageFormat.Remap(GuestImageFormat.Format8Uscaled));
var request = ImageRequestBuilders.Texture(
RegisterWords.Texture(Base, GuestPixelFormat.Bits8UScaled, 32, 32),
new ShaderImageShape(false, false, false, false, TextureNumericClass.Float));
Assert.Equal(Format.R8Unorm, request.Request.Description.PixelFormat);
Assert.True(request.ExactFormat);
var twoChannel = ImageRequestBuilders.Texture(
RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8UScaled, 32, 32),
new ShaderImageShape(false, false, false, false, TextureNumericClass.Float));
Assert.Equal(Format.R8G8Unorm, twoChannel.Request.Description.PixelFormat);
Assert.True(twoChannel.ExactFormat);
}
[Fact]
public void StorageWithoutMipOperandUsesTheAvailableMipRange()
{

View File

@ -14,6 +14,57 @@ public sealed class NetExportsTests
{
private readonly CpuContext _ctx = new(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
[Fact]
public void SocketDescriptorsFitGuestFdSet()
{
_ctx[CpuRegister.Rsi] = 2; // AF_INET
_ctx[CpuRegister.Rdx] = 2; // SOCK_DGRAM
_ctx[CpuRegister.Rcx] = 17; // UDP
Assert.Equal(0, NetExports.NetSocket(_ctx));
var first = _ctx[CpuRegister.Rax];
Assert.Equal(0, NetExports.NetSocket(_ctx));
var second = _ctx[CpuRegister.Rax];
try
{
Assert.InRange(first, 256UL, 1023UL);
Assert.InRange(second, 256UL, 1023UL);
Assert.NotEqual(first, second);
}
finally
{
_ctx[CpuRegister.Rdi] = first;
NetExports.NetSocketClose(_ctx);
_ctx[CpuRegister.Rdi] = second;
NetExports.NetSocketClose(_ctx);
}
}
[Fact]
public void NonblockingReceiveWithoutDataReturnsWouldBlock()
{
_ctx[CpuRegister.Rsi] = 2;
_ctx[CpuRegister.Rdx] = 2;
_ctx[CpuRegister.Rcx] = 17;
Assert.Equal(0, NetExports.NetSocket(_ctx));
var socket = _ctx[CpuRegister.Rax];
try
{
_ctx[CpuRegister.Rdi] = socket;
_ctx[CpuRegister.Rsi] = 0x1_0000_0000;
_ctx[CpuRegister.Rdx] = 1;
_ctx[CpuRegister.Rcx] = 0x80;
Assert.Equal(unchecked((int)0x80410123), NetExports.NetRecv(_ctx));
}
finally
{
_ctx[CpuRegister.Rdi] = socket;
NetExports.NetSocketClose(_ctx);
}
}
[Fact]
public void Htonl_SwapsAllFourBytes()
{

View File

@ -64,6 +64,24 @@ public sealed class PadExportsTests
Assert.Equal(cookie, BitConverter.ToUInt64(guard));
}
[Fact]
public void GetExtControllerInformation_DoesNotOverwriteCallerCookie()
{
const ulong informationAddress = Base + 0x100;
const ulong cookieAddress = informationAddress + 0x30;
const ulong cookie = 0xC0DEC0DECAFEBA00UL;
Assert.True(_memory.TryWrite(cookieAddress, BitConverter.GetBytes(cookie)));
_ctx[CpuRegister.Rdi] = 1;
_ctx[CpuRegister.Rsi] = informationAddress;
Assert.Equal(0, PadExports.PadGetExtControllerInformation(_ctx));
Span<byte> guard = stackalloc byte[8];
Assert.True(_memory.TryRead(cookieAddress, guard));
Assert.Equal(cookie, BitConverter.ToUInt64(guard));
}
[Theory]
[InlineData(2)]
[InlineData(-1)]