[kernel/videoOut] extend memory managements and videoOut (this is not a swapchain)

This commit is contained in:
ParantezTech
2026-06-07 15:43:26 +03:00
parent a1dea499bc
commit 1ea1396979
8 changed files with 2632 additions and 29 deletions
File diff suppressed because it is too large Load Diff
@@ -9,10 +9,21 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelEventQueueCompatExports
{
private const int KernelEventSize = 0x20;
private static readonly object _eventQueueGate = new();
private static readonly HashSet<ulong> _eventQueues = new();
private static readonly Dictionary<ulong, Queue<KernelQueuedEvent>> _pendingEvents = new();
private static long _nextEventQueueHandle = 1;
public readonly record struct KernelQueuedEvent(
ulong Ident,
short Filter,
ushort Flags,
uint Fflags,
ulong Data,
ulong UserData);
[SysAbiExport(
Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue",
@@ -30,6 +41,7 @@ public static class KernelEventQueueCompatExports
lock (_eventQueueGate)
{
_eventQueues.Add(handle);
_pendingEvents[handle] = new Queue<KernelQueuedEvent>();
}
if (!ctx.TryWriteUInt64(outAddress, handle))
@@ -52,6 +64,7 @@ public static class KernelEventQueueCompatExports
lock (_eventQueueGate)
{
_eventQueues.Remove(handle);
_pendingEvents.Remove(handle);
}
TraceEventQueue(ctx, "delete", handle);
@@ -165,23 +178,108 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelWaitEqueue(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var eventsAddress = ctx[CpuRegister.Rsi];
var eventCapacity = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
var outCountAddress = ctx[CpuRegister.Rcx];
var timeoutAddress = ctx[CpuRegister.R8];
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0))
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
if (deliveredCount > 0)
{
TraceEventQueue(ctx, "wait-block", ctx[CpuRegister.Rdi]);
TraceEventQueue(ctx, "wait-deliver", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]);
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
{
TraceEventQueue(ctx, "wait-block", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TraceEventQueue(ctx, "wait", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static bool IsValidEqueue(ulong handle)
{
lock (_eventQueueGate)
{
return _eventQueues.Contains(handle);
}
}
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
{
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
{
return false;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new Queue<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
queue.Enqueue(queuedEvent);
return true;
}
}
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
{
if (eventsAddress == 0 || eventCapacity <= 0)
{
return 0;
}
KernelQueuedEvent[] events;
lock (_eventQueueGate)
{
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
{
return 0;
}
var count = Math.Min(eventCapacity, queue.Count);
events = new KernelQueuedEvent[count];
for (var i = 0; i < count; i++)
{
events[i] = queue.Dequeue();
}
}
for (var i = 0; i < events.Length; i++)
{
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
{
return i;
}
}
return events.Length;
}
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
{
Span<byte> eventBytes = stackalloc byte[KernelEventSize];
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x00..], queuedEvent.Ident);
BinaryPrimitives.WriteInt16LittleEndian(eventBytes[0x08..], queuedEvent.Filter);
BinaryPrimitives.WriteUInt16LittleEndian(eventBytes[0x0A..], queuedEvent.Flags);
BinaryPrimitives.WriteUInt32LittleEndian(eventBytes[0x0C..], queuedEvent.Fflags);
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x10..], queuedEvent.Data);
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x18..], queuedEvent.UserData);
return ctx.Memory.TryWrite(address, eventBytes);
}
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
@@ -1444,6 +1444,51 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Oy6IpwgtYOk",
ExportName = "lseek",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixLseek(CpuContext ctx)
{
var result = KernelLseekCore(
unchecked((int)ctx[CpuRegister.Rdi]),
unchecked((long)ctx[CpuRegister.Rsi]),
unchecked((int)ctx[CpuRegister.Rdx]),
out var position);
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
ctx[CpuRegister.Rax] = ulong.MaxValue;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
ctx[CpuRegister.Rax] = unchecked((ulong)position);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "oib76F-12fk",
ExportName = "sceKernelLseek",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelLseek(CpuContext ctx)
{
var result = KernelLseekCore(
unchecked((int)ctx[CpuRegister.Rdi]),
unchecked((long)ctx[CpuRegister.Rsi]),
unchecked((int)ctx[CpuRegister.Rdx]),
out var position);
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
return (int)result;
}
ctx[CpuRegister.Rax] = unchecked((ulong)position);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "taRWhTJFTgE",
ExportName = "sceKernelGetdirentries",
@@ -1474,6 +1519,58 @@ public static class KernelMemoryCompatExports
0);
}
private static OrbisGen2Result KernelLseekCore(int fd, long offset, int whence, out long position)
{
position = -1;
FileStream? stream;
lock (_fdGate)
{
_openFiles.TryGetValue(fd, out stream);
}
if (stream is null)
{
LogIoTrace("lseek", $"fd:{fd}", $"offset={offset} whence={whence} result=badfd");
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
SeekOrigin origin;
switch (whence)
{
case SeekSet:
origin = SeekOrigin.Begin;
break;
case SeekCur:
origin = SeekOrigin.Current;
break;
case SeekEnd:
origin = SeekOrigin.End;
break;
default:
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid_whence");
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
position = stream.Seek(offset, origin);
}
catch (IOException ex)
{
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=io_error ex={ex.Message}");
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
catch (ArgumentException ex)
{
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid ex={ex.Message}");
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} pos={position}");
return OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "FxVZqBAA7ks",
ExportName = "_write",
@@ -3567,13 +3664,13 @@ public static class KernelMemoryCompatExports
var devlogAppRoot = ResolveDevlogAppRoot();
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
{
var relative = guestPath["/devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar);
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
return Path.Combine(devlogAppRoot, relative);
}
if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase))
{
var relative = guestPath["devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar);
var relative = NormalizeMountRelativePath(guestPath["devlog/app/".Length..]);
return Path.Combine(devlogAppRoot, relative);
}
@@ -3586,7 +3683,7 @@ public static class KernelMemoryCompatExports
var temp0Root = ResolveTemp0Root();
if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase))
{
var relative = guestPath["/temp0/".Length..].Replace('/', Path.DirectorySeparatorChar);
var relative = NormalizeMountRelativePath(guestPath["/temp0/".Length..]);
return Path.Combine(temp0Root, relative);
}
@@ -3606,13 +3703,13 @@ public static class KernelMemoryCompatExports
if (guestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase))
{
var relative = guestPath["/app0/".Length..].Replace('/', Path.DirectorySeparatorChar);
var relative = NormalizeMountRelativePath(guestPath["/app0/".Length..]);
return Path.Combine(app0Root, relative);
}
if (guestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase))
{
var relative = guestPath["app0/".Length..].Replace('/', Path.DirectorySeparatorChar);
var relative = NormalizeMountRelativePath(guestPath["app0/".Length..]);
return Path.Combine(app0Root, relative);
}
@@ -3628,6 +3725,14 @@ public static class KernelMemoryCompatExports
return guestPath;
}
private static string NormalizeMountRelativePath(string relativePath)
{
return relativePath
.TrimStart('/', '\\')
.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar);
}
private static string ResolveDevlogAppRoot()
{
var configuredRoot = Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR");
@@ -0,0 +1,318 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
public static class KernelSemaphoreCompatExports
{
private const int MaxSemaphoreNameLength = 128;
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
public required int InitialCount { get; init; }
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
public object Gate { get; } = new();
}
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCreateSema(CpuContext ctx)
{
var semaphoreAddress = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
var attr = unchecked((uint)ctx[CpuRegister.Rdx]);
var initialCount = unchecked((int)ctx[CpuRegister.Rcx]);
var maxCount = unchecked((int)ctx[CpuRegister.R8]);
var optionAddress = ctx[CpuRegister.R9];
if (semaphoreAddress == 0 ||
nameAddress == 0 ||
attr > 2 ||
initialCount < 0 ||
maxCount <= 0 ||
initialCount > maxCount ||
optionAddress != 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
if (handle == 0)
{
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
};
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "Zxa0VhQVTsk",
ExportName = "sceKernelWaitSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelWaitSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
var timeoutAddress = ctx[CpuRegister.Rdx];
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
if (timeoutAddress != 0)
{
if (!TryReadUInt32(ctx, timeoutAddress, out _))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema"))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "12wOHk8ywb0",
ExportName = "sceKernelPollSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelPollSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count < needCount)
{
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
semaphore.Count -= needCount;
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "4czppHBiriw",
ExportName = "sceKernelSignalSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelSignalSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (signalCount <= 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count > semaphore.MaxCount - signalCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "4DM06U2BNEY",
ExportName = "sceKernelCancelSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCancelSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (setCount > semaphore.MaxCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0;
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "R1Jvn8bSCW8",
ExportName = "sceKernelDeleteSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
if (!_semaphores.TryRemove(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
{
return false;
}
var bytes = new byte[Math.Min(maxLength, 4096)];
for (var i = 0; i < bytes.Length; i++)
{
Span<byte> current = stackalloc byte[1];
if (!ctx.Memory.TryRead(address + (ulong)i, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, i);
return true;
}
bytes[i] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
}
}
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Diagnostics;
namespace SharpEmu.Libs.Pad;
public static class PadExports
{
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
private const int PrimaryUserId = 1;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
private static bool _initialized;
[SysAbiExport(
Nid = "hv1luiJrqQM",
ExportName = "scePadInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadInit(CpuContext ctx)
{
_initialized = true;
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "xk0AcarP3V4",
ExportName = "scePadOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized)
{
return SetReturn(ctx, OrbisPadErrorNotInitialized);
}
if (userId == -1)
{
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
}
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
{
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
}
return SetReturn(ctx, PrimaryPadHandle);
}
[SysAbiExport(
Nid = "clVvL4ZDntw",
ExportName = "scePadSetMotionSensorState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle
? SetReturn(ctx, 0)
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
[SysAbiExport(
Nid = "gjP9-KQzoUk",
ExportName = "scePadGetControllerInformation",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetControllerInformation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> information = stackalloc byte[ControllerInformationSize];
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;
information[0x0C] = 1;
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
return ctx.Memory.TryWrite(informationAddress, information)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "YndgXqQVV7c",
ExportName = "scePadReadState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadReadState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> data = stackalloc byte[PadDataSize];
data.Clear();
data[0x04] = 128;
data[0x05] = 128;
data[0x06] = 128;
data[0x07] = 128;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
var timestampMicroseconds =
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
BinaryPrimitives.WriteUInt64LittleEndian(
data[0x50..],
timestampMicroseconds);
data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -0,0 +1,42 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.SystemService;
public static class SystemServiceExports
{
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
private const int SystemServiceStatusSize = 0x0C;
[SysAbiExport(
Nid = "rPo6tV8D9bM",
ExportName = "sceSystemServiceGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceGetStatus(CpuContext ctx)
{
var statusAddress = ctx[CpuRegister.Rdi];
if (statusAddress == 0)
{
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
}
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
status.Clear();
BinaryPrimitives.WriteInt32LittleEndian(status, 0);
status[0x06] = 1;
return ctx.Memory.TryWrite(statusAddress, status)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -0,0 +1,79 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.UserService;
public static class UserServiceExports
{
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
private const int PrimaryUserId = 1;
private const int InvalidUserId = -1;
[SysAbiExport(
Nid = "j3YMu1MVNNo",
ExportName = "sceUserServiceInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceInitialize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "CdWp0oHWGr0",
ExportName = "sceUserServiceGetInitialUser",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetInitialUser(CpuContext ctx)
{
var userIdAddress = ctx[CpuRegister.Rdi];
if (userIdAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "fPhymKNvK-A",
ExportName = "sceUserServiceGetLoginUserIdList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetLoginUserIdList(CpuContext ctx)
{
var userIdListAddress = ctx[CpuRegister.Rdi];
if (userIdListAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x00..], PrimaryUserId);
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x04..], InvalidUserId);
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
return ctx.Memory.TryWrite(userIdListAddress, userIds)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+702 -20
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.Diagnostics.CodeAnalysis;
using System.Threading;
@@ -15,28 +16,79 @@ public static class VideoOutExports
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
private const int OrbisVideoOutErrorInvalidEventQueue = unchecked((int)0x8029000C);
private const int OrbisVideoOutErrorInvalidEvent = unchecked((int)0x8029000D);
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
private const int SceVideoOutBusTypeMain = 0;
private const int SceVideoOutBufferAttributeOptionNone = 0;
private const int SceVideoOutTilingModeLinear = 1;
private const int MaxOpenPorts = 4;
private const int MaxDisplayBuffers = 16;
private const int VideoOutBufferAttributeSize = 0x24;
private const int MaxDisplayBufferGroups = 4;
private const int MaxFrameDumps = 8;
private const int VideoOutBufferAttributeSize = 0x28;
private const int VideoOutBufferAttribute2Size = 0x50;
private const int VideoOutBuffersEntrySize = 0x20;
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
private const ulong SceVideoOutInternalEventFlip = 0x6;
private const short OrbisKernelEventFilterVideoOut = -13;
private static readonly object _stateGate = new();
private static readonly object _frameDumpGate = new();
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
private static int _nextHandle = 1;
private static int _frameDumpCount;
private static long _nextFrameDumpIndex;
private sealed class VideoOutPortState
{
public required int Handle { get; init; }
public int FlipRate { get; set; }
public ulong VblankCount { get; set; }
public int NextSetId { get; set; } = 1;
public HashSet<int> RegisteredSetIds { get; } = new();
public ulong FlipCount { get; set; }
public int CurrentBuffer { get; set; } = -1;
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> FlipEvents { get; } = new();
}
private sealed class VideoOutBufferGroup
{
public required int Index { get; init; }
public required BufferAttribute Attribute { get; init; }
}
private sealed class VideoOutBufferSlot
{
public int GroupIndex { get; set; } = -1;
public ulong AddressLeft { get; set; }
public ulong AddressRight { get; set; }
}
private readonly record struct FlipEventRegistration(ulong Equeue, ulong UserData);
private readonly record struct BufferAttribute(
ulong PixelFormat,
uint TilingMode,
uint AspectRatio,
uint Width,
uint Height,
uint PitchInPixel,
ulong Option);
internal readonly record struct DisplayBufferInfo(
ulong Address,
ulong PixelFormat,
uint TilingMode,
uint Width,
uint Height,
uint PitchInPixel);
[SysAbiExport(
Nid = "Up36PTk687E",
ExportName = "sceVideoOutOpen",
@@ -136,6 +188,146 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "HXzjK9yI30k",
ExportName = "sceVideoOutAddFlipEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutAddFlipEvent(CpuContext ctx)
{
var equeue = ctx[CpuRegister.Rdi];
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
var userData = ctx[CpuRegister.Rdx];
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
{
return OrbisVideoOutErrorInvalidEventQueue;
}
lock (_stateGate)
{
port.FlipEvents.Add(new FlipEventRegistration(equeue, userData));
}
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "U46NwOiJpys",
ExportName = "sceVideoOutSubmitFlip",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSubmitFlip(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var flipMode = unchecked((int)ctx[CpuRegister.Rdx]);
var flipArg = unchecked((long)ctx[CpuRegister.Rcx]);
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
}
[SysAbiExport(
Nid = "U2JJtSqNKZI",
ExportName = "sceVideoOutGetEventId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutGetEventId(CpuContext ctx)
{
var eventAddress = ctx[CpuRegister.Rdi];
if (eventAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
!TryReadInt16(ctx, eventAddress + 0x08, out var filter))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
{
return OrbisVideoOutErrorInvalidEvent;
}
return 0;
}
[SysAbiExport(
Nid = "rWUTcKdkUzQ",
ExportName = "sceVideoOutGetEventData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutGetEventData(CpuContext ctx)
{
var eventAddress = ctx[CpuRegister.Rdi];
var dataAddress = ctx[CpuRegister.Rsi];
if (eventAddress == 0 || dataAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
!TryReadInt16(ctx, eventAddress + 0x08, out var filter) ||
!ctx.TryReadUInt64(eventAddress + 0x10, out var data))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
{
return OrbisVideoOutErrorInvalidEvent;
}
return ctx.TryWriteUInt64(dataAddress, data >> 16)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) =>
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
{
info = default;
if (bufferIndex < 0 || bufferIndex >= MaxDisplayBuffers)
{
return false;
}
lock (_stateGate)
{
if (!_ports.TryGetValue(handle, out var port))
{
return false;
}
var slot = port.BufferSlots[bufferIndex];
if (slot.AddressLeft == 0 ||
slot.GroupIndex < 0 ||
slot.GroupIndex >= port.Groups.Length ||
port.Groups[slot.GroupIndex] is not { } group)
{
return false;
}
var attribute = group.Attribute;
info = new DisplayBufferInfo(
slot.AddressLeft,
attribute.PixelFormat,
attribute.TilingMode,
attribute.Width,
attribute.Height,
attribute.PitchInPixel);
return true;
}
}
[SysAbiExport(
Nid = "MTxxrOCeSig",
ExportName = "sceVideoOutSetWindowModeMargins",
@@ -173,9 +365,23 @@ public static class VideoOutExports
lock (_stateGate)
{
return port.RegisteredSetIds.Remove(attributeIndex)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidValue;
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
{
return OrbisVideoOutErrorInvalidValue;
}
port.Groups[attributeIndex] = null;
foreach (var slot in port.BufferSlots)
{
if (slot.GroupIndex == attributeIndex)
{
slot.GroupIndex = -1;
slot.AddressLeft = 0;
slot.AddressRight = 0;
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -292,16 +498,21 @@ public static class VideoOutExports
return OrbisVideoOutErrorInvalidValue;
}
if (!TryReadBufferAttribute(ctx, attributeAddress, false, out var attribute))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
for (var i = 0; i < bufferNum; i++)
{
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _))
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out addresses[i]))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
var setId = AllocateBufferSet(port);
return setId;
return RegisterBufferRange(port, startIndex, addresses[..bufferNum], attribute);
}
[SysAbiExport(
@@ -348,34 +559,458 @@ public static class VideoOutExports
return OrbisVideoOutErrorInvalidValue;
}
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
for (var i = 0; i < bufferNum; i++)
{
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) ||
if (!ctx.TryReadUInt64(entryAddress + 0x00, out addresses[i]) ||
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
lock (_stateGate)
{
port.RegisteredSetIds.Add(setIndex);
}
return setIndex;
var groupIndex = RegisterBufferRange(port, bufferIndexStart, addresses[..bufferNum], attribute, setIndex);
return groupIndex < 0 ? groupIndex : setIndex;
}
private static int AllocateBufferSet(VideoOutPortState port)
private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg)
{
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (bufferIndex < -1 || bufferIndex >= MaxDisplayBuffers)
{
return OrbisVideoOutErrorInvalidIndex;
}
ulong eventData;
List<FlipEventRegistration> flipEvents;
lock (_stateGate)
{
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
{
return OrbisVideoOutErrorInvalidIndex;
}
port.CurrentBuffer = bufferIndex;
port.FlipCount++;
var eventCount = Math.Min(port.FlipCount, 0xFUL);
var timeBits = (ulong)Environment.TickCount64 & 0xFFFUL;
eventData = timeBits | (eventCount << 12) | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
}
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
foreach (var flipEvent in flipEvents)
{
_ = KernelEventQueueCompatExports.EnqueueEvent(
flipEvent.Equeue,
new KernelEventQueueCompatExports.KernelQueuedEvent(
SceVideoOutInternalEventFlip,
OrbisKernelEventFilterVideoOut,
0,
0,
eventData,
flipEvent.UserData));
}
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
{
lock (_stateGate)
{
var setId = port.NextSetId++;
port.RegisteredSetIds.Add(setId);
return setId;
var groupIndex = requestedGroupIndex >= 0 ? requestedGroupIndex : FindFreeGroupIndex(port);
if (groupIndex < 0 || groupIndex >= MaxDisplayBufferGroups)
{
return OrbisVideoOutErrorInvalidValue;
}
if (port.Groups[groupIndex] is not null)
{
return OrbisVideoOutErrorResourceBusy;
}
for (var i = 0; i < addresses.Length; i++)
{
if (port.BufferSlots[startIndex + i].GroupIndex >= 0)
{
return OrbisVideoOutErrorResourceBusy;
}
}
port.Groups[groupIndex] = new VideoOutBufferGroup
{
Index = groupIndex,
Attribute = attribute,
};
for (var i = 0; i < addresses.Length; i++)
{
var slot = port.BufferSlots[startIndex + i];
slot.GroupIndex = groupIndex;
slot.AddressLeft = addresses[i];
slot.AddressRight = 0;
}
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
return groupIndex;
}
}
private static int FindFreeGroupIndex(VideoOutPortState port)
{
for (var i = 0; i < port.Groups.Length; i++)
{
if (port.Groups[i] is null)
{
return i;
}
}
return -1;
}
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
{
attribute = default;
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
{
return false;
}
if (attribute2)
{
if (!ctx.TryReadUInt64(attributeAddress + 0x18, out var option) ||
!ctx.TryReadUInt64(attributeAddress + 0x20, out var pixelFormat))
{
return false;
}
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat), tilingMode, 0, width, height, width, option);
return true;
}
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
{
return false;
}
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat32), tilingMode, aspectRatio, width, height, pitchInPixel, option32);
return true;
}
private static bool TryDumpFrame(CpuContext ctx, VideoOutPortState port, int bufferIndex, int flipMode, long flipArg)
{
if (bufferIndex < 0)
{
return false;
}
VideoOutBufferSlot slot;
VideoOutBufferGroup? group;
lock (_stateGate)
{
slot = port.BufferSlots[bufferIndex];
group = slot.GroupIndex >= 0 && slot.GroupIndex < port.Groups.Length
? port.Groups[slot.GroupIndex]
: null;
}
if (group is null || slot.AddressLeft == 0)
{
return false;
}
var attribute = group.Attribute;
if (attribute.Width == 0 || attribute.Height == 0 || attribute.Width > 8192 || attribute.Height > 8192)
{
return false;
}
var bytesPerPixel = GetBytesPerPixel(attribute.PixelFormat);
if (bytesPerPixel == 0)
{
return DumpRawFrame(ctx, port.Handle, slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "unsupported-format");
}
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
var rowBytes = checked((int)(pitch * bytesPerPixel));
var visibleRowBytes = checked((int)(attribute.Width * bytesPerPixel));
var frameBytes = checked((ulong)rowBytes * attribute.Height);
if (frameBytes > 256UL * 1024UL * 1024UL)
{
return false;
}
lock (_frameDumpGate)
{
if (_frameDumpCount >= MaxFrameDumps)
{
return false;
}
}
const ulong fnvOffsetBasis = 14695981039346656037UL;
const ulong fnvPrime = 1099511628211UL;
var fingerprint = fnvOffsetBasis;
var row = new byte[rowBytes];
for (uint y = 0; y < attribute.Height; y++)
{
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
{
return false;
}
foreach (var value in row.AsSpan(0, visibleRowBytes))
{
fingerprint = (fingerprint ^ value) * fnvPrime;
}
}
var fingerprintKey = (port.Handle, bufferIndex, slot.AddressLeft);
lock (_frameDumpGate)
{
if (_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
previousFingerprint == fingerprint)
{
return false;
}
if (_frameDumpCount >= MaxFrameDumps)
{
return false;
}
_lastFrameFingerprints[fingerprintKey] = fingerprint;
_frameDumpCount++;
}
var rgb = new byte[checked((int)(attribute.Width * attribute.Height * 3))];
var rgbOffset = 0;
for (uint y = 0; y < attribute.Height; y++)
{
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
{
return false;
}
ConvertRowToRgb(row.AsSpan(0, visibleRowBytes), rgb.AsSpan(rgbOffset, (int)attribute.Width * 3), attribute.PixelFormat);
rgbOffset += (int)attribute.Width * 3;
}
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
return true;
}
private static bool DumpRawFrame(CpuContext ctx, int handle, ulong address, BufferAttribute attribute, int bufferIndex, int flipMode, long flipArg, string reason)
{
var bytesPerPixel = Math.Max(GetBytesPerPixel(attribute.PixelFormat), 4u);
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
var byteCount = checked((ulong)pitch * attribute.Height * bytesPerPixel);
if (byteCount == 0 || byteCount > 256UL * 1024UL * 1024UL)
{
return false;
}
var bytes = new byte[(int)byteCount];
if (!ctx.Memory.TryRead(address, bytes))
{
return false;
}
var fingerprint = ComputeFingerprint(bytes);
var fingerprintKey = (handle, bufferIndex, address);
lock (_frameDumpGate)
{
if ((_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
previousFingerprint == fingerprint) ||
_frameDumpCount >= MaxFrameDumps)
{
return false;
}
_lastFrameFingerprints[fingerprintKey] = fingerprint;
_frameDumpCount++;
}
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
File.WriteAllBytes(basePath + ".raw", bytes);
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
return true;
}
private static ulong ComputeFingerprint(ReadOnlySpan<byte> bytes)
{
const ulong fnvOffsetBasis = 14695981039346656037UL;
const ulong fnvPrime = 1099511628211UL;
var fingerprint = fnvOffsetBasis;
foreach (var value in bytes)
{
fingerprint = (fingerprint ^ value) * fnvPrime;
}
return fingerprint;
}
private static uint GetBytesPerPixel(ulong pixelFormat) =>
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
SceVideoOutPixelFormatA8B8G8R8Srgb or
SceVideoOutPixelFormatA2R10G10B10 or
SceVideoOutPixelFormatA2R10G10B10Srgb or
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
? 4u
: 0u;
private static ulong NormalizePixelFormat(ulong pixelFormat)
{
if (GetBytesPerPixel(pixelFormat) != 0)
{
return pixelFormat;
}
var low = (uint)(pixelFormat & 0xFFFF_FFFFUL);
if (GetBytesPerPixel(low) != 0)
{
return low;
}
var high = (uint)(pixelFormat >> 32);
if (GetBytesPerPixel(high) != 0)
{
return high;
}
var packed = high | (low >> 16);
return GetBytesPerPixel(packed) != 0 ? packed : pixelFormat;
}
private static void ConvertRowToRgb(ReadOnlySpan<byte> source, Span<byte> destination, ulong pixelFormat)
{
var dst = 0;
for (var src = 0; src + 3 < source.Length; src += 4)
{
if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb)
{
destination[dst++] = source[src + 0];
destination[dst++] = source[src + 1];
destination[dst++] = source[src + 2];
}
else if (pixelFormat is SceVideoOutPixelFormatA2R10G10B10 or SceVideoOutPixelFormatA2R10G10B10Srgb or SceVideoOutPixelFormatA2R10G10B10Bt2020Pq)
{
var value = BinaryPrimitives.ReadUInt32LittleEndian(source[src..(src + 4)]);
destination[dst++] = (byte)(((value >> 20) & 0x3FF) >> 2);
destination[dst++] = (byte)(((value >> 10) & 0x3FF) >> 2);
destination[dst++] = (byte)((value & 0x3FF) >> 2);
}
else
{
destination[dst++] = source[src + 2];
destination[dst++] = source[src + 1];
destination[dst++] = source[src + 0];
}
}
}
private static string GetFrameDumpBasePath(long frameIndex, int handle, int bufferIndex)
{
var directory = GetLogsDirectory();
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"videoout_frame_{frameIndex:D4}_h{handle}_b{bufferIndex}");
}
private static string GetLogsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "SharpEmu.slnx")))
{
return Path.Combine(current.FullName, "logs");
}
current = current.Parent;
}
return Path.Combine(Directory.GetCurrentDirectory(), "logs");
}
private static void WriteBmp(string path, uint width, uint height, byte[] rgb)
{
var rowStride = checked((int)(((width * 3u) + 3u) & ~3u));
var pixelBytes = checked(rowStride * (int)height);
var fileSize = 54 + pixelBytes;
using var stream = File.Create(path);
Span<byte> header = stackalloc byte[54];
header[0] = (byte)'B';
header[1] = (byte)'M';
BinaryPrimitives.WriteUInt32LittleEndian(header[0x02..], (uint)fileSize);
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0A..], 54);
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0E..], 40);
BinaryPrimitives.WriteInt32LittleEndian(header[0x12..], (int)width);
BinaryPrimitives.WriteInt32LittleEndian(header[0x16..], -(int)height);
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1A..], 1);
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1C..], 24);
BinaryPrimitives.WriteUInt32LittleEndian(header[0x22..], (uint)pixelBytes);
stream.Write(header);
var row = new byte[rowStride];
var sourceStride = (int)width * 3;
var heightInt = (int)height;
var widthInt = (int)width;
for (var y = 0; y < heightInt; y++)
{
row.AsSpan().Clear();
var src = rgb.AsSpan(y * sourceStride, sourceStride);
for (var x = 0; x < widthInt; x++)
{
row[(x * 3) + 0] = src[(x * 3) + 2];
row[(x * 3) + 1] = src[(x * 3) + 1];
row[(x * 3) + 2] = src[(x * 3) + 0];
}
stream.Write(row);
}
}
private static void WriteFrameMetadata(
string path,
ulong address,
BufferAttribute attribute,
int bufferIndex,
int flipMode,
long flipArg,
string kind,
ulong fingerprint)
{
File.WriteAllText(
path,
$"kind={kind}\naddress=0x{address:X16}\nbuffer_index={bufferIndex}\nflip_mode={flipMode}\nflip_arg={flipArg}\nfingerprint=0x{fingerprint:X16}\npixel_format=0x{attribute.PixelFormat:X}\ntiling_mode={attribute.TilingMode}\nwidth={attribute.Width}\nheight={attribute.Height}\npitch_in_pixel={attribute.PitchInPixel}\noption=0x{attribute.Option:X}\n");
}
private static bool IsValidBufferRange(int startIndex, int bufferNum)
{
return startIndex >= 0 &&
@@ -393,6 +1028,17 @@ public static class VideoOutExports
}
}
private static VideoOutBufferSlot[] CreateBufferSlots()
{
var slots = new VideoOutBufferSlot[MaxDisplayBuffers];
for (var i = 0; i < slots.Length; i++)
{
slots[i] = new VideoOutBufferSlot();
}
return slots;
}
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
{
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
@@ -406,4 +1052,40 @@ public static class VideoOutExports
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
{
Span<byte> buffer = stackalloc byte[sizeof(short)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt16LittleEndian(buffer);
return true;
}
private static void TraceVideoOut(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
}
}