Compare commits

...

16 Commits

Author SHA1 Message Date
ParantezTech 994438963d [ci] temporary fix 2026-04-13 15:59:19 +03:00
ParantezTech 233af123af More new HLE's and fixes 2026-04-13 15:54:38 +03:00
ParantezTech 02eb9b30e9 correct patch 2026-03-28 18:40:54 +03:00
ParantezTech fa46819030 fix for actions (again) 2026-03-28 18:23:46 +03:00
ParantezTech 7a915e88dc fix dotnet version on Github Actions 2026-03-28 18:15:14 +03:00
ParantezTech 812879aa81 PlayGo, VideoOut minimum HLE implements and fix some direct runner 2026-03-28 18:02:37 +03:00
ParantezTech 71ba5cf1db More HLE implements: wcs* 2026-03-19 23:57:48 +03:00
ParantezTech 8f108ca01d Upgrade GitHub Actions to Node 24 2026-03-19 13:33:19 +03:00
ParantezTech cecf55bf2e fix: dereference of a possibly null reference. 2026-03-16 21:19:49 +03:00
ParantezTech 0ede7c70a9 [ci] update actions Node 20 deprecation 2026-03-16 21:18:58 +03:00
ParantezTech 8c39a55501 reuse 2026-03-16 20:49:40 +03:00
ParantezTech 0a4b36ffcb [dotnet/ci] constant version 2026-03-16 20:46:47 +03:00
ParantezTech 1a1d61b21c rework TLS allocations, more HLE's, increase import loop history 2026-03-16 20:22:58 +03:00
ParantezTech 3cf96f38de [github] bug_report fix 2026-03-16 20:14:58 +03:00
ParantezTech d8c44bb64f [github] added bug report issue template 2026-03-16 20:11:56 +03:00
ParantezTech a4b5171586 [github] added issue config & reuse 2026-03-16 20:01:08 +03:00
18 changed files with 2996 additions and 213 deletions
+59
View File
@@ -0,0 +1,59 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Bug Report
description: Report a bug in the emulator
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
### Before creating an issue
- Do not create issues involving **software piracy**. Our rules specifically prohibit this.
Issues related to piracy will be closed and the author may be banned from the repository.
- The issue tracker is **not for asking questions**.
Question-type issues will be closed.
- Only open an issue if you are reporting an actual **bug in the emulator**.
- type: input
id: emulator_version
attributes:
label: Emulator Version / Commit
description: The emulator version or git commit where the bug occurred
placeholder: e.g. v0.0.2 / a1b2c3d
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: Describe the problem and how to reproduce it
placeholder: |
Steps to reproduce:
1. Launch emulator
2. Load game
3. Crash occurs
Expected behavior:
...
Actual behavior:
...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log File
description: |
Drag & drop your log file here, or paste the log contents.
Supported: .log / .txt / .zip
Large logs should be uploaded as a file.
render: shell
+4
View File
@@ -0,0 +1,4 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
blank_issues_enabled: false
@@ -1,3 +1,6 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Game Compatibility Report
description: Report compatibility status of a game
labels: ["game-compatibility"]
+6 -3
View File
@@ -1,3 +1,6 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Build and Release
on:
@@ -79,7 +82,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
dotnet-version: 10.0.103
cache: true
cache-dependency-path: |
Directory.Packages.props
@@ -106,7 +109,7 @@ jobs:
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ needs.init.outputs.artifact-name }}
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
@@ -123,7 +126,7 @@ jobs:
contents: write
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ${{ needs.init.outputs.artifact-name }}
path: release
+1
View File
@@ -3,6 +3,7 @@ version = 1
[[annotations]]
path = [
"REUSE.toml",
"global.json",
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "disable"
}
}
@@ -154,15 +154,57 @@ public sealed partial class DirectExecutionBackend
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
bool flag3 = num >= 1020 && num <= 1040;
if (!flag0 && (num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
bool logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal);
string importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
bool flag4 = !string.IsNullOrWhiteSpace(importFilter);
bool flag5 = false;
ExportedFunction matchedExport = null;
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
{
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
matchedExport = export;
if (flag4)
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
flag5 = export.LibraryName.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|| export.Name.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|| importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
}
}
else if (flag4)
{
flag5 = importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
}
bool flag6 = logAllImports || flag5;
if (!flag0 && (flag6 || num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
{
if (matchedExport != null)
{
if (flag6)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{num}: {matchedExport.LibraryName}:{matchedExport.Name} ({importStubEntry.Nid}) " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {matchedExport.LibraryName}:{matchedExport.Name} ({importStubEntry.Nid})");
}
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}");
if (flag6)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid} " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}");
}
}
if (flag6)
{
Console.Error.Flush();
}
}
if (!flag0)
@@ -212,28 +254,6 @@ public sealed partial class DirectExecutionBackend
}
TryBypassStackChkFailTrap(num, num7);
}
if (importStubEntry.Nid == "9rAeANT2tyE" || importStubEntry.Nid == "1j3S3n-tTW4")
{
ulong rspDbg = _cpuContext[CpuRegister.Rsp];
Console.Error.WriteLine(
$"[LOADER][TRACE] ImportDbg#{num}: nid={importStubEntry.Nid} " +
$"ret=0x{num7:X16} rsp=0x{rspDbg:X16} rsp&7=0x{(rspDbg & 7):X} rsp&F=0x{(rspDbg & 0xF):X}");
if (_cpuContext.TryReadUInt64(rspDbg, out var s0))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x00] = 0x{s0:X16}");
if (_cpuContext.TryReadUInt64(rspDbg + 8, out var s8))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x08] = 0x{s8:X16}");
if (_cpuContext.TryReadUInt64(rspDbg + 16, out var s10))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x10] = 0x{s10:X16}");
if (_cpuContext.TryReadUInt64(rspDbg + 24, out var s18))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x18] = 0x{s18:X16}");
ProbeReturnRip(num7, num);
Console.Error.Flush();
}
ProbeReturnRip(0x0000000800EA020Eul, 999001);
ProbeReturnRip(0x0000000800EA0213ul, 999002);
ProbeReturnRip(0x0000000800EA040Aul, 999003);
try
{
OrbisGen2Result orbisGen2Result;
@@ -426,7 +446,20 @@ public sealed partial class DirectExecutionBackend
return false;
}
int num2 = CountDistinctImportLoopValuesFromTail(_importLoopReturnRips, sampleCount, 3);
return num2 <= 2;
if (num2 > 2)
{
return false;
}
int num3 = Math.Min(_importLoopSignatureCount, Math.Max(sampleCount * 8, ImportLoopWideDiversityWindow));
if (num3 <= sampleCount)
{
return true;
}
if (CountDistinctImportLoopValuesFromTail(_importLoopNidHashes, num3, 3) > 2)
{
return false;
}
return CountDistinctImportLoopValuesFromTail(_importLoopReturnRips, num3, 3) <= 2;
}
private int CountDistinctImportLoopValuesFromTail(ulong[] source, int sampleCount, int stopAfter)
@@ -13,6 +13,10 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IDisposable
{
private const int ImportLoopHistoryLength = 2048;
private const int ImportLoopWideDiversityWindow = 768;
private readonly struct ImportStubEntry
{
public ulong Address { get; }
@@ -89,7 +93,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private const uint PAGE_EXECUTE_READ = 32u;
private const int TlsHandlerRegionSize = 4096;
private const int TlsHandlerRegionSize = 16384;
private const ulong TlsModuleAllocStart = 140726751354880uL;
@@ -179,11 +183,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private ulong _entryReturnSentinelRip;
private readonly ulong[] _importLoopSignatures = new ulong[192];
private readonly ulong[] _importLoopSignatures = new ulong[ImportLoopHistoryLength];
private readonly ulong[] _importLoopNidHashes = new ulong[192];
private readonly ulong[] _importLoopNidHashes = new ulong[ImportLoopHistoryLength];
private readonly ulong[] _importLoopReturnRips = new ulong[192];
private readonly ulong[] _importLoopReturnRips = new ulong[ImportLoopHistoryLength];
private int _importLoopSignatureCount;
@@ -601,14 +605,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
return exportName switch
{
"_init_env" or
"atexit" or
"strlen" or
"strnlen" or
"strcmp" or
"strncmp" or
"strcpy" or
"strncpy" or
"memcpy" or
"memmove" or
"memset" or
@@ -40,6 +40,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")]
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
@@ -351,6 +354,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return true;
}
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
@@ -389,6 +397,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return true;
}
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
@@ -494,6 +507,48 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
}
private static uint GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
}
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
{
if (size == 0 || !region.IsReservedOnly)
{
return true;
}
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
return false;
}
if (info.State == MEM_COMMIT)
{
continue;
}
if (info.State != MEM_RESERVE)
{
return false;
}
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
{
return false;
}
}
return true;
}
private bool TryTemporarilyProtectForRead(
ulong address,
ulong size,
@@ -558,4 +613,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
public bool IsReservedOnly { get; set; }
public uint Protection { get; set; }
}
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
}
+38 -2
View File
@@ -125,6 +125,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
public OrbisGen2Result Run(string ebootPath)
{
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
@@ -132,8 +134,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastBasicBlockTrace = null;
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(ebootPath);
var normalizedEbootPath = Path.GetFullPath(ebootPath);
var image = LoadImage(normalizedEbootPath);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
@@ -350,6 +351,41 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{
const string app0VariableName = "SHARPEMU_APP0_DIR";
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(app0VariableName)))
{
return null;
}
var app0Root = Path.GetDirectoryName(normalizedEbootPath);
if (string.IsNullOrWhiteSpace(app0Root))
{
return null;
}
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
return new App0BindingScope(app0VariableName);
}
private sealed class App0BindingScope(string variableName) : IDisposable
{
private readonly string _variableName = variableName;
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
Environment.SetEnvironmentVariable(_variableName, null);
_disposed = true;
}
}
private OrbisGen2Result? RunAllInitializers(
SelfImage mainImage,
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
@@ -2,11 +2,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
[SysAbiExport(
Nid = "R9lA82OraNs",
ExportName = "sceAppContentInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentInitialize(CpuContext ctx)
{
var initParamAddress = ctx[CpuRegister.Rdi];
var bootParamAddress = ctx[CpuRegister.Rsi];
if (initParamAddress == 0 || bootParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> attrBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(attrBytes, 0);
if (!ctx.Memory.TryWrite(bootParamAddress + BootParamAttrOffset, attrBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "xnd8BJzAxmk",
ExportName = "sceAppContentGetAddcontInfoList",
@@ -14,7 +44,67 @@ public static class AppContentExports
LibraryName = "libSceAppContent")]
public static int AppContentGetAddcontInfoList(CpuContext ctx)
{
_ = ctx;
var hitCountAddress = ctx[CpuRegister.Rcx];
if (hitCountAddress != 0)
{
Span<byte> hitCountBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(hitCountBytes, 0);
if (!ctx.Memory.TryWrite(hitCountAddress, hitCountBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentTemporaryDataMount2(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rsi];
if (mountPointAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Directory.CreateDirectory(ResolveTemp0Root());
var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0");
if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
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(Path.GetTempPath(), "SharpEmu", appName, "temp0");
Environment.SetEnvironmentVariable(temp0VariableName, root);
return root;
}
}
+9 -84
View File
@@ -264,22 +264,14 @@ public static class KernelExports
ExportName = "open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Open(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
[SysAbiExport(
Nid = "1G3lF1Gg1k8",
ExportName = "sceKernelOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
[SysAbiExport(
Nid = "hcuQgD53UxM",
@@ -290,84 +282,17 @@ public static class KernelExports
{
ulong fmtPtr = ctx[CpuRegister.Rdi];
string fmt = ReadCString(ctx, fmtPtr, 4096);
ulong[] args =
string outStr = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, fmt, firstGpArgIndex: 1);
if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
{
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
};
int argIndex = 0;
var sb = new System.Text.StringBuilder(fmt.Length + 64);
for (int i = 0; i < fmt.Length; i++)
Console.Write($"[DEBUG][PRINF] {outStr}");
}
else
{
char ch = fmt[i];
if (ch != '%')
{
sb.Append(ch);
continue;
}
if (i + 1 < fmt.Length && fmt[i + 1] == '%')
{
sb.Append('%');
i++;
continue;
}
int j = i + 1;
while (j < fmt.Length && "-+ #0".IndexOf(fmt[j]) >= 0) j++;
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
if (j < fmt.Length && fmt[j] == '.')
{
j++;
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
}
if (j >= fmt.Length) break;
char spec = fmt[j];
i = j;
ulong a = argIndex < args.Length ? args[argIndex++] : 0;
switch (spec)
{
case 's':
sb.Append(a == 0 ? "(null)" : ReadCString(ctx, a, 4096));
break;
case 'd':
case 'i':
sb.Append(unchecked((int)a));
break;
case 'u':
sb.Append(unchecked((uint)a));
break;
case 'x':
sb.Append(unchecked((uint)a).ToString("x"));
break;
case 'p':
sb.Append("0x").Append(a.ToString("x16"));
break;
default:
sb.Append('%').Append(spec);
break;
}
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
}
string outStr = sb.ToString();
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
ctx[CpuRegister.Rax] = (ulong)outStr.Length;
ctx[CpuRegister.Rax] = (ulong)System.Text.Encoding.UTF8.GetByteCount(outStr);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -11,7 +12,9 @@ public static class KernelPthreadCompatExports
private const int MutexTypeDefault = 1;
private const int MutexTypeErrorCheck = 1;
private const int MutexTypeRecursive = 2;
private const int MutexTypeNormal = 4;
private const int MutexTypeNormal = 3;
private const int MutexTypeAdaptiveNp = 4;
private const ulong StaticAdaptiveMutexInitializer = 1;
private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000;
private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000;
private const ulong SyntheticCondHandleBase = 0x00006002_0000_0000;
@@ -86,18 +89,14 @@ public static class KernelPthreadCompatExports
ExportName = "scePthreadGetthreadid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetthreadid(CpuContext ctx)
{
var currentThreadId = KernelPthreadState.GetCurrentThreadUniqueId();
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress != 0 && !ctx.TryWriteUInt64(outAddress, currentThreadId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
public static int PthreadGetthreadid(CpuContext ctx) => PthreadGetthreadidCore(ctx);
ctx[CpuRegister.Rax] = currentThreadId;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3eqs37G74-s",
ExportName = "pthread_getthreadid_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadGetthreadidNp(CpuContext ctx) => PthreadGetthreadidCore(ctx);
[SysAbiExport(
Nid = "cmo1RIYva9o",
@@ -169,6 +168,12 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true);
private static int PthreadGetthreadidCore(CpuContext ctx)
{
ctx[CpuRegister.Rax] = KernelPthreadState.GetCurrentThreadUniqueId();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "F8bUHwAG284",
ExportName = "scePthreadMutexattrInit",
@@ -232,6 +237,13 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadCondInit(CpuContext ctx) => PthreadCondInitCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "0TyVk4MSLt0",
ExportName = "pthread_cond_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondInit(CpuContext ctx) => PthreadCondInitCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "g+PZd2hiacg",
ExportName = "scePthreadCondDestroy",
@@ -258,14 +270,14 @@ public static class KernelPthreadCompatExports
ExportName = "scePthreadCondSignal",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: false);
public static int PthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "JGgj7Uvrl+A",
ExportName = "scePthreadCondBroadcast",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
public static int PthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: true);
[SysAbiExport(
Nid = "Op8TBGY5KHg",
@@ -279,7 +291,14 @@ public static class KernelPthreadCompatExports
ExportName = "pthread_cond_broadcast",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
public static int PosixPthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: true);
[SysAbiExport(
Nid = "2MOy+rUfuhQ",
ExportName = "pthread_cond_signal",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "m5-2bsNfv7s",
@@ -402,6 +421,23 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
{
if (tryOnly)
{
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
// Under the current single-host-thread guest execution model,
// treating them as nested ownership keeps init paths moving
// without turning a would-block path into a hard error.
state.RecursionCount++;
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
@@ -606,7 +642,7 @@ public static class KernelPthreadCompatExports
return mutexAddress;
}
private static bool TryResolveMutexState(CpuContext ctx, ulong mutexAddress, bool createIfZero, out ulong resolvedAddress, out PthreadMutexState? state)
private static bool TryResolveMutexState(CpuContext ctx, ulong mutexAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{
resolvedAddress = 0;
state = null;
@@ -629,6 +665,11 @@ public static class KernelPthreadCompatExports
return false;
}
if (pointedHandle == StaticAdaptiveMutexInitializer)
{
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeAdaptiveNp, out resolvedAddress, out state);
}
if (pointedHandle != 0)
{
lock (_stateGate)
@@ -651,18 +692,7 @@ public static class KernelPthreadCompatExports
return false;
}
var createdState = new PthreadMutexState();
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
lock (_stateGate)
{
_mutexStates[mutexAddress] = createdState;
_mutexStates[syntheticHandle] = createdState;
}
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
resolvedAddress = syntheticHandle;
state = createdState;
return true;
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state);
}
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
@@ -739,7 +769,7 @@ public static class KernelPthreadCompatExports
return condAddress;
}
private static bool TryResolveCondState(CpuContext? ctx, ulong condAddress, bool createIfZero, out ulong resolvedAddress, out PthreadCondState? state)
private static bool TryResolveCondState(CpuContext? ctx, ulong condAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadCondState? state)
{
resolvedAddress = 0;
state = null;
@@ -901,14 +931,14 @@ public static class KernelPthreadCompatExports
return waitResult;
}
private static int PthreadCondSignalCore(ulong condAddress, bool broadcast)
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
{
if (condAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveCondState(null, condAddress, createIfZero: false, out _, out var state))
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -952,11 +982,43 @@ public static class KernelPthreadCompatExports
1 => MutexTypeErrorCheck,
2 => MutexTypeRecursive,
3 => MutexTypeNormal,
4 => MutexTypeNormal,
4 => MutexTypeAdaptiveNp,
_ => MutexTypeDefault,
};
}
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{
var createdState = new PthreadMutexState
{
Type = type,
};
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
lock (_stateGate)
{
if (_mutexStates.TryGetValue(mutexAddress, out state))
{
resolvedAddress = mutexAddress;
return true;
}
if (_mutexStates.TryGetValue(syntheticHandle, out state))
{
resolvedAddress = syntheticHandle;
return true;
}
_mutexStates[mutexAddress] = createdState;
_mutexStates[syntheticHandle] = createdState;
}
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
resolvedAddress = syntheticHandle;
state = createdState;
return true;
}
private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadHandle)
{
if (!ShouldTracePthread())
@@ -5,6 +5,7 @@ using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -23,13 +24,12 @@ public static class KernelPthreadExtendedCompatExports
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
private static readonly Dictionary<ulong, ReaderWriterLockSlim> _rwlockStates = new();
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
private static int _nextTlsKey = 1;
private static long _nextSyntheticRwlockHandleId = 1;
[ThreadStatic]
private static Dictionary<int, ulong>? _threadLocalSpecific;
private static readonly Dictionary<ulong, Dictionary<int, ulong>> _threadLocalSpecific = new();
private sealed class ThreadState
{
@@ -40,6 +40,47 @@ public static class KernelPthreadExtendedCompatExports
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
private sealed class PthreadRwlockState
{
public object SyncRoot { get; } = new();
public Dictionary<ulong, int> ReaderCounts { get; } = new();
public int ReaderTotalCount { get; set; }
public ulong WriterThreadId { get; set; }
public int WaitingWriters { get; set; }
public int GetReaderCount(ulong threadId)
{
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
}
public void AddReader(ulong threadId)
{
ReaderCounts.TryGetValue(threadId, out var currentCount);
ReaderCounts[threadId] = currentCount + 1;
ReaderTotalCount++;
}
public bool RemoveReader(ulong threadId)
{
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
{
return false;
}
if (currentCount == 1)
{
ReaderCounts.Remove(threadId);
}
else
{
ReaderCounts[threadId] = currentCount - 1;
}
ReaderTotalCount = Math.Max(0, ReaderTotalCount - 1);
return true;
}
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
@@ -599,10 +640,9 @@ public static class KernelPthreadExtendedCompatExports
var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress);
if (_rwlockStates.Remove(resolvedAddress, out var existing))
{
existing.Dispose();
}
var rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
var rwlock = new PthreadRwlockState();
_rwlockStates[rwlockAddress] = rwlock;
_rwlockStates[syntheticHandle] = rwlock;
}
@@ -633,14 +673,10 @@ public static class KernelPthreadExtendedCompatExports
}
var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress);
ReaderWriterLockSlim? state;
PthreadRwlockState? state;
lock (_stateGate)
{
_rwlockStates.Remove(resolvedAddress, out state);
if (resolvedAddress != rwlockAddress)
{
_rwlockStates.Remove(rwlockAddress);
}
_rwlockStates.TryGetValue(resolvedAddress, out state);
}
if (state is null)
@@ -648,8 +684,24 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
lock (state.SyncRoot)
{
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
}
lock (_stateGate)
{
_rwlockStates.Remove(resolvedAddress);
if (resolvedAddress != rwlockAddress)
{
_rwlockStates.Remove(rwlockAddress);
}
}
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
state.Dispose();
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -702,29 +754,38 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
try
{
if (rwlock.IsWriteLockHeld)
lock (rwlock.SyncRoot)
{
rwlock.ExitWriteLock();
}
else if (rwlock.IsReadLockHeld)
{
rwlock.ExitReadLock();
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
if (rwlock.WriterThreadId == currentThreadId)
{
rwlock.WriterThreadId = 0;
Monitor.PulseAll(rwlock.SyncRoot);
}
else if (rwlock.RemoveReader(currentThreadId))
{
if (rwlock.ReaderTotalCount == 0 || rwlock.WaitingWriters > 0)
{
Monitor.PulseAll(rwlock.SyncRoot);
}
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
}
}
catch (SynchronizationLockException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -793,9 +854,13 @@ public static class KernelPthreadExtendedCompatExports
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
foreach (var entry in _threadLocalSpecific)
{
entry.Value.Remove(key);
}
}
_threadLocalSpecific?.Remove(key);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -816,16 +881,23 @@ public static class KernelPthreadExtendedCompatExports
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var value = ctx[CpuRegister.Rsi];
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
lock (_stateGate)
{
if (!_tlsKeys.TryGetValue(key, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (!_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values))
{
values = new Dictionary<int, ulong>();
_threadLocalSpecific[currentThreadHandle] = values;
}
values[key] = value;
}
_threadLocalSpecific ??= new Dictionary<int, ulong>();
_threadLocalSpecific[key] = value;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -845,6 +917,8 @@ public static class KernelPthreadExtendedCompatExports
public static int PosixPthreadGetspecific(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
ulong value = 0;
lock (_stateGate)
{
if (!_tlsKeys.TryGetValue(key, out _))
@@ -852,12 +926,15 @@ public static class KernelPthreadExtendedCompatExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
values.TryGetValue(key, out var storedValue))
{
value = storedValue;
}
}
ctx[CpuRegister.Rax] =
_threadLocalSpecific is not null && _threadLocalSpecific.TryGetValue(key, out var value)
? value
: 0UL;
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -880,21 +957,47 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (rwlock.SyncRoot)
{
if (write)
{
rwlock.EnterWriteLock();
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
rwlock.WaitingWriters++;
try
{
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
{
Monitor.Wait(rwlock.SyncRoot);
}
}
finally
{
rwlock.WaitingWriters--;
}
rwlock.WriterThreadId = currentThreadId;
}
else
{
rwlock.EnterReadLock();
if (rwlock.WriterThreadId == currentThreadId)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (rwlock.WriterThreadId != 0 ||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
{
Monitor.Wait(rwlock.SyncRoot);
}
rwlock.AddReader(currentThreadId);
}
}
catch (LockRecursionException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -928,7 +1031,7 @@ public static class KernelPthreadExtendedCompatExports
return rwlockAddress;
}
private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, out ReaderWriterLockSlim? rwlock)
private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadRwlockState? rwlock)
{
resolvedAddress = 0;
rwlock = null;
@@ -973,7 +1076,7 @@ public static class KernelPthreadExtendedCompatExports
return false;
}
var createdRwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
var createdRwlock = new PthreadRwlockState();
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockHandleBase, ref _nextSyntheticRwlockHandleId);
lock (_stateGate)
{
@@ -31,6 +31,7 @@ public static class KernelRuntimeCompatExports
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
private const ulong PrtAreaSize = 0x000000EC00000000UL;
private const int AioInitParamSize = 0x3C;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint PageExecuteReadWrite = 0x40;
@@ -780,6 +781,48 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nu4a0-arQis",
ExportName = "sceKernelAioInitializeParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAioInitializeParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> zero = stackalloc byte[AioInitParamSize];
zero.Clear();
if (!ctx.Memory.TryWrite(paramAddress, zero))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vYU8P9Td2Zo",
ExportName = "sceKernelAioInitializeImpl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAioInitializeImpl(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var size = unchecked((int)ctx[CpuRegister.Rsi]);
if (paramAddress == 0 || size < AioInitParamSize)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eR2bZFAAU0Q",
ExportName = "sceSysmoduleUnloadModule",
+176
View File
@@ -0,0 +1,176 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.PlayGo;
public static class PlayGoExports
{
private const int OrbisPlayGoErrorInvalidArgument = unchecked((int)0x80B20004);
private const int OrbisPlayGoErrorNotInitialized = unchecked((int)0x80B20005);
private const int OrbisPlayGoErrorAlreadyInitialized = unchecked((int)0x80B20006);
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
private const ulong PlayGoInitBufAddrOffset = 0;
private const ulong PlayGoInitBufSizeOffset = 8;
private const uint PlayGoMinimumInitBufferSize = 0x200000;
private const uint PlayGoHandle = 1;
private static readonly object _stateGate = new();
private static bool _initialized;
private static bool _hasPlayGoData;
[SysAbiExport(
Nid = "ts6GlZOKRrE",
ExportName = "scePlayGoInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoInitialize(CpuContext ctx)
{
var initParamsAddress = ctx[CpuRegister.Rdi];
if (initParamsAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (!ctx.TryReadUInt64(initParamsAddress + PlayGoInitBufAddrOffset, out var bufferAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
Span<byte> bufferSizeBytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(initParamsAddress + PlayGoInitBufSizeOffset, bufferSizeBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var bufferSize = BinaryPrimitives.ReadUInt32LittleEndian(bufferSizeBytes);
if (bufferAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (bufferSize < PlayGoMinimumInitBufferSize)
{
return OrbisPlayGoErrorBadSize;
}
lock (_stateGate)
{
if (_initialized)
{
return OrbisPlayGoErrorAlreadyInitialized;
}
_hasPlayGoData = HasPlayGoChunkData();
_initialized = true;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "M1Gma1ocrGE",
ExportName = "scePlayGoOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoOpen(CpuContext ctx)
{
var outHandleAddress = ctx[CpuRegister.Rdi];
var paramAddress = ctx[CpuRegister.Rsi];
if (outHandleAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (paramAddress != 0)
{
return OrbisPlayGoErrorInvalidArgument;
}
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (!_hasPlayGoData)
{
return OrbisPlayGoErrorNotSupportPlayGo;
}
}
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(handleBytes, PlayGoHandle);
if (!ctx.Memory.TryWrite(outHandleAddress, handleBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MPe0EeBGM-E",
ExportName = "scePlayGoTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoTerminate(CpuContext ctx)
{
_ = ctx;
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
_initialized = false;
_hasPlayGoData = false;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Uco1I0dlDi8",
ExportName = "scePlayGoClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoClose(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (handle != PlayGoHandle)
{
return OrbisPlayGoErrorBadHandle;
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool HasPlayGoChunkData()
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return false;
}
var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
return File.Exists(hostPath);
}
}
@@ -2,9 +2,408 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
namespace SharpEmu.Libs.VideoOut;
public static class VideoOutExports
{
private const int OrbisVideoOutErrorInvalidValue = unchecked((int)0x80290001);
private const int OrbisVideoOutErrorInvalidAddress = unchecked((int)0x80290002);
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
private const int SceVideoOutBusTypeMain = 0;
private const int SceVideoOutBufferAttributeOptionNone = 0;
private const int MaxOpenPorts = 4;
private const int MaxDisplayBuffers = 16;
private const int VideoOutBufferAttributeSize = 0x24;
private const int VideoOutBufferAttribute2Size = 0x50;
private const int VideoOutBuffersEntrySize = 0x20;
private static readonly object _stateGate = new();
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
private static int _nextHandle = 1;
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();
}
[SysAbiExport(
Nid = "Up36PTk687E",
ExportName = "sceVideoOutOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var busType = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
_ = ctx[CpuRegister.Rcx];
if (busType != SceVideoOutBusTypeMain || index != 0)
{
return OrbisVideoOutErrorInvalidValue;
}
if (userId != 0 && userId != 255)
{
return OrbisVideoOutErrorInvalidValue;
}
lock (_stateGate)
{
if (_ports.Count >= MaxOpenPorts)
{
return OrbisVideoOutErrorResourceBusy;
}
var handle = _nextHandle++;
_ports[handle] = new VideoOutPortState
{
Handle = handle,
};
return handle;
}
}
[SysAbiExport(
Nid = "uquVH4-Du78",
ExportName = "sceVideoOutClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
_ports.Remove(handle);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "CBiu4mCE1DA",
ExportName = "sceVideoOutSetFlipRate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetFlipRate(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var rate = unchecked((int)ctx[CpuRegister.Rsi]);
if (rate is < 0 or > 2)
{
return OrbisVideoOutErrorInvalidValue;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
port.FlipRate = rate;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "j6RaAUlaLv0",
ExportName = "sceVideoOutWaitVblank",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutWaitVblank(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
Thread.Sleep(1);
lock (_stateGate)
{
port.VblankCount++;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MTxxrOCeSig",
ExportName = "sceVideoOutSetWindowModeMargins",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetWindowModeMargins(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
_ = unchecked((int)ctx[CpuRegister.Rsi]);
_ = unchecked((int)ctx[CpuRegister.Rdx]);
return TryGetPort(handle, out _)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidHandle;
}
[SysAbiExport(
Nid = "N5KDtkIjjJ4",
ExportName = "sceVideoOutUnregisterBuffers",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutUnregisterBuffers(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var attributeIndex = unchecked((int)ctx[CpuRegister.Rsi]);
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (attributeIndex < 0)
{
return OrbisVideoOutErrorInvalidValue;
}
lock (_stateGate)
{
return port.RegisteredSetIds.Remove(attributeIndex)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidValue;
}
}
[SysAbiExport(
Nid = "i6-sR91Wt-4",
ExportName = "sceVideoOutSetBufferAttribute",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetBufferAttribute(CpuContext ctx)
{
var attributeAddress = ctx[CpuRegister.Rdi];
var pixelFormat = unchecked((uint)ctx[CpuRegister.Rsi]);
var tilingMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var aspectRatio = unchecked((uint)ctx[CpuRegister.Rcx]);
var width = unchecked((uint)ctx[CpuRegister.R8]);
var height = unchecked((uint)ctx[CpuRegister.R9]);
if (!TryReadStackUInt32(ctx, 0, out var pitchInPixel))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
Span<byte> attribute = stackalloc byte[VideoOutBufferAttributeSize];
attribute.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x00..0x04], pixelFormat);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x04..0x08], tilingMode);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x08..0x0C], aspectRatio);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x0C..0x10], width);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x10..0x14], height);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x14..0x18], pitchInPixel);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x18..0x1C], SceVideoOutBufferAttributeOptionNone);
if (!ctx.Memory.TryWrite(attributeAddress, attribute))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "PjS5uASwcV8",
ExportName = "sceVideoOutSetBufferAttribute2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetBufferAttribute2(CpuContext ctx)
{
var attributeAddress = ctx[CpuRegister.Rdi];
var pixelFormat = ctx[CpuRegister.Rsi];
var tilingMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var width = unchecked((uint)ctx[CpuRegister.Rcx]);
var height = unchecked((uint)ctx[CpuRegister.R8]);
var option = ctx[CpuRegister.R9];
if (!TryReadStackUInt32(ctx, 0, out var dccControl) ||
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x10, out var dccClearColor))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
Span<byte> attribute = stackalloc byte[VideoOutBufferAttribute2Size];
attribute.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x04..0x08], tilingMode);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x0C..0x10], width);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x10..0x14], height);
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x18..0x20], option);
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x20..0x28], pixelFormat);
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x28..0x30], dccClearColor);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x30..0x34], dccControl);
if (!ctx.Memory.TryWrite(attributeAddress, attribute))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "w3BY+tAEiQY",
ExportName = "sceVideoOutRegisterBuffers",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutRegisterBuffers(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var startIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var addressesAddress = ctx[CpuRegister.Rdx];
var bufferNum = unchecked((int)ctx[CpuRegister.Rcx]);
var attributeAddress = ctx[CpuRegister.R8];
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (addressesAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidOption;
}
if (!IsValidBufferRange(startIndex, bufferNum))
{
return OrbisVideoOutErrorInvalidValue;
}
for (var i = 0; i < bufferNum; i++)
{
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
var setId = AllocateBufferSet(port);
return setId;
}
[SysAbiExport(
Nid = "rKBUtgRrtbk",
ExportName = "sceVideoOutRegisterBuffers2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutRegisterBuffers2(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var setIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var bufferIndexStart = unchecked((int)ctx[CpuRegister.Rdx]);
var buffersAddress = ctx[CpuRegister.Rcx];
var bufferNum = unchecked((int)ctx[CpuRegister.R8]);
var attributeAddress = ctx[CpuRegister.R9];
if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x08, out var categoryRaw) ||
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x10, out var option))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (buffersAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidOption;
}
if (!IsValidBufferRange(bufferIndexStart, bufferNum))
{
return OrbisVideoOutErrorInvalidValue;
}
if (categoryRaw != 0 || option != 0)
{
return OrbisVideoOutErrorInvalidValue;
}
for (var i = 0; i < bufferNum; i++)
{
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) ||
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
lock (_stateGate)
{
port.RegisteredSetIds.Add(setIndex);
}
return setIndex;
}
private static int AllocateBufferSet(VideoOutPortState port)
{
lock (_stateGate)
{
var setId = port.NextSetId++;
port.RegisteredSetIds.Add(setId);
return setId;
}
}
private static bool IsValidBufferRange(int startIndex, int bufferNum)
{
return startIndex >= 0 &&
startIndex < MaxDisplayBuffers &&
bufferNum >= 1 &&
bufferNum <= MaxDisplayBuffers &&
startIndex + bufferNum <= MaxDisplayBuffers;
}
private static bool TryGetPort(int handle, [NotNullWhen(true)] out VideoOutPortState? port)
{
lock (_stateGate)
{
return _ports.TryGetValue(handle, out port);
}
}
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
{
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
}