Compare commits

...

6 Commits

Author SHA1 Message Date
PandaCatz f43f7cde9c [cpu] Implement SysV variadic float ABI (xmm0-7 capture, float returns, printf %f) (#59)
* [cpu] Implement SysV variadic float ABI (xmm0-7 capture, float returns, printf %f)

The import trampoline spilled only xmm0 and never reloaded a return xmm0. The
guest uses the System V AMD64 ABI: variadic float args pass in xmm0..xmm7 and
float/double returns come back in xmm0. As a result variadic float args past
the first were unavailable to HLE handlers, float returns never reached the
guest, and direct printf read %f/%e/%g from GP registers instead of XMM,
printing garbage and desynchronizing every following argument.

- Trampoline: spill xmm0..xmm7 into a 0x80-byte save area below the GP argpack
  (r12 stays at the argpack base) and reload the return xmm0 in the epilogue.
- Gateway: read xmm0..7 from the save area into CpuContext and write the
  handler's xmm0 back. XMM is caller-saved in SysV, so restoring xmm0 on return
  is safe for non-float imports too.
- RegisterPrintfArgumentSource: read float args from xmm0..7 with independent
  GP/FP counters and a shared stack-overflow cursor.

Every emitted byte was decoded; a unit test confirms float args read xmm0..7
(not GP) and interleaved "%d %f %d %f" stays synchronized. Build 0/0.

* [cpu] Document the scalar-only leaf-import constraint at its registration site

- IsLeafImport: spell out the no-XMM-args / no-XMM-return invariant the fast
  path relies on and what breaks if it is violated; record the 2026-07-11 audit.
- Name every previously uncommented NID in the leaf list (mutex lock/unlock,
  usleep, the Ampr/Apr command-buffer block, the unknown AGC packet NID).
- IsNoBlockLeafImport: document that it is a sub-filter of IsLeafImport and
  that its five extra entries currently take the full gateway path; fix the
  mislabeled K-jXhbt2gn4 comment (pthread_mutex_trylock, not
  scePthreadMutexTrylock, which is upoVrzMHFeE).
- Point the DispatchImport call-site note at the audited list.

Comment-only change: the comment-stripped diff is empty and the solution
builds with 0 warnings / 0 errors.
2026-07-11 17:41:25 +03:00
j92580498-max ef74680167 More logger improvements (#58)
* fix

* fix

---------

Co-authored-by: j92580498-max <j92580498-max@users.noreply.github.com>
2026-07-11 17:34:49 +03:00
Mike Saito 65a40773fa core: expand clock_gettime fast clocks, unify timespec writes, fix NULL EINVAL (#62)
Refactored parts of the time subsystem to improve POSIX/Orbis compliance and secure guest memory boundaries.

**1. POSIX `clock_gettime` updates:**
- Added `CLOCK_REALTIME_FAST` (10) and `CLOCK_MONOTONIC_FAST` (12) support for games using FreeBSD fast clock extensions.
- Fixed `NULL` pointer handling for `timespecAddress == 0`. It now returns `-1` with `EINVAL` (22) instead of `EFAULT` to match Orbis runtime behavior.
- Invalid `clock_id` values now properly fallback to `default` -> `-1` + `EINVAL`.

**2. Memory safety & monotonic tracking:**
- Replaced dual 8-byte scalar writes in both POSIX `clock_gettime` and Orbis `sceKernelClockGettime` with a single 16-byte write via `stackalloc byte[16]` and `BinaryPrimitives`. This prevents partial memory corruption at page boundaries.
- Bad non-NULL guest addresses now fail cleanly as `EFAULT` (POSIX) or `MEMORY_FAULT` (Orbis).
- Extracted core monotonic math into `GetProcessMonotonicTime()` in `KernelRuntimeCompatExports.cs` so both clock paths share the exact same `_processStartCounter` base.

**3. Host RDTSC optimization:**
- Fixed `CreateRdtscReader()` to copy stack-allocated opcode bytes into host `VirtualAlloc` memory via `unsafe { Buffer.MemoryCopy(...) }`. This completely gets rid of the redundant `.ToArray()` allocation on the hot path.

**Out of scope:** `sceKernelGettimeofday` / POSIX `gettimeofday` partial-write hardening; stricter clock validation in `sceKernelClockGettime`.
2026-07-11 17:32:48 +03:00
Mike Saito 9ddc09ea91 core: page-aware TryReadUtf8Z and unify exit/_exit handling (#57)
Read guest C strings in page-bounded chunks without heap allocations.
Return false when the buffer fills without a null terminator. Route exit
and _exit through RequestProcessExit.
2026-07-11 15:42:43 +03:00
Brando c4326a1143 Update README.md for Discord (#55)
* Added Discord link
2026-07-11 14:21:06 +03:00
ParantezTech 347e33f3c9 [GUI] update icon to .ico format 2026-07-11 14:03:55 +03:00
12 changed files with 465 additions and 97 deletions
+14
View File
@@ -32,4 +32,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
<DebugType>none</DebugType> <DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols> <DebugSymbols>false</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
top of the log. Populated from GitHub Actions environment variables when
building in CI; empty locally. -->
<ItemGroup>
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
</ItemGroup>
</Project> </Project>
BIN
View File
Binary file not shown.
+2
View File
@@ -76,6 +76,8 @@ internal static partial class Program
SharpEmuLog.MinimumLevel = logLevel; SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
ebootPath = Path.GetFullPath(ebootPath); ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}"); Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
@@ -99,6 +99,12 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})"); Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
_lastReportedRawSentinelRecoveries = num2; _lastReportedRawSentinelRecoveries = num2;
} }
// Leaf imports take a scalar-only fast path that reads its own operands and
// intentionally bypasses the SysV variadic XMM marshalling below. This is safe
// only while the leaf set contains no float-variadic or float-returning function.
// The constraint and the audited list live on IsLeafImport — do not add a
// function that consumes xmm0-7 args or returns in xmm0 to the leaf set;
// such functions must stay on the full gateway path below.
if (IsLeafImport(importStubEntry.Nid) && if (IsLeafImport(importStubEntry.Nid) &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult)) TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
{ {
@@ -118,10 +124,17 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72); cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80); cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88); cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext.SetXmmRegister( // The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
0, // 0x80 bytes immediately below the GP argpack, so variadic float args (printf
*(ulong*)(argPackPtr - 16), // %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
*(ulong*)(argPackPtr - 8)); for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
{
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
cpuContext.SetXmmRegister(
xmmIndex,
*(ulong*)xmmSlot,
*(ulong*)(xmmSlot + 8));
}
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
ulong value = cpuContext[CpuRegister.Rdi]; ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi]; ulong value2 = cpuContext[CpuRegister.Rsi];
@@ -491,6 +504,13 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush(); Console.Error.Flush();
} }
} }
// Publish the handler's XMM0 back into the argpack's xmm0 save slot; the
// trampoline epilogue reloads it into the guest's XMM0, delivering float/double
// return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is
// volatile across a SysV call, so the guest never relies on a preserved XMM0).
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
return cpuContext[CpuRegister.Rax]; return cpuContext[CpuRegister.Rax];
} }
catch (Exception ex) catch (Exception ex)
@@ -622,6 +642,13 @@ public sealed partial class DirectExecutionBackend
return true; return true;
} }
// Subset of the leaf set that additionally skips the import-call-frame
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
// as IsLeafImport applies — see the note there before adding entries.
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
// pthread_mutex_trylock) currently take the full gateway path.
private static bool IsNoBlockLeafImport(string nid) => private static bool IsNoBlockLeafImport(string nid) =>
nid is nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
@@ -648,7 +675,7 @@ public sealed partial class DirectExecutionBackend
"xk0AcarP3V4" or // scePadOpen "xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent "yH17Q6NWtVg" or // sceUserServiceGetEvent
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting "D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
"K-jXhbt2gn4"; // scePthreadMutexTrylock "K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
private bool ShouldLogImportResult(string nid, OrbisGen2Result result) private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
{ {
@@ -715,22 +742,41 @@ public sealed partial class DirectExecutionBackend
"1G3lF1Gg1k8" or // sceKernelOpen "1G3lF1Gg1k8" or // sceKernelOpen
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes "gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
// LEAF-IMPORT REGISTRATION — scalar-only constraint.
//
// TryDispatchLeafImport is a fast path that never copies the trampoline's XMM
// save area into CpuContext and never publishes an XMM0 return back to the
// guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH:
// 1. no float/double parameters (nothing passed in xmm0..xmm7), and
// 2. no float/double return value (nothing returned in xmm0).
// Integer/pointer arguments and returns only. va_list-based functions
// (vsnprintf) are safe: SysV va_list floats are read from the caller-built
// reg_save_area in guest memory, not from the callee's incoming XMM registers.
//
// If a function that consumes XMM arguments or returns in xmm0 (e.g. powf,
// logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its
// float arguments will silently arrive stale and its return value will be
// dropped. Such functions must stay on the full gateway path — do not list them.
//
// Audited 2026-07-11 (PR #59): every NID below maps to a registered
// SysAbiExport whose handler neither reads nor writes CpuContext XMM state
// and whose known guest signature is integer/pointer only.
private bool IsLeafImport(string nid) private bool IsLeafImport(string nid)
{ {
if (nid == "1jfXLRVzisc") if (nid == "1jfXLRVzisc") // sceKernelUsleep
{ {
return !_logUsleep; return !_logUsleep;
} }
return nid is return nid is
"9UK1vLZQft4" or "9UK1vLZQft4" or // scePthreadMutexLock
"tn3VlD0hG60" or "tn3VlD0hG60" or // scePthreadMutexUnlock
"7H0iTOciTLo" or "7H0iTOciTLo" or // pthread_mutex_lock
"2Z+PpY6CaJg" or "2Z+PpY6CaJg" or // pthread_mutex_unlock
"8aI7R7WaOlc" or "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending "zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress "V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet "qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out)
"LtTouSCZjHM" or // sceAgcCbNop "LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch "k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect "UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
@@ -753,24 +799,24 @@ public sealed partial class DirectExecutionBackend
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset "IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress "3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress "0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
"a8uLzYY--tM" or "a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
"Qs1xtplKo0U" or "Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
"GuchCTefuZw" or "GuchCTefuZw" or // sceAmprCommandBufferDestructor
"N-FSPA4S3nI" or "N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
"baQO9ez2gL4" or "baQO9ez2gL4" or // sceAmprCommandBufferReset
"ULvXMDz56po" or "ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
"mQ16-QdKv7k" or "mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
"vWU-odnS+fU" or "vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
"sSAUCCU1dv4" or "sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
"C+IEj+BsAFM" or "C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
"tZDDEo2tE5k" or "tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
"GnxKOHEawhk" or "GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
"H896Pt-yB4I" or "H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
"sJXyWHjP-F8" or "sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
"ASoW5WE-UPo" or "ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
"rqwFKI4PAiM" or "rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
"eE4Szl8sil8" or "eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or "qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen "Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3 "TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch "dyIhnXq-0SM" or // sceSaveDataDirNameSearch
@@ -1568,7 +1568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private unsafe nint CreateImportHandlerTrampoline(int importIndex) private unsafe nint CreateImportHandlerTrampoline(int importIndex)
{ {
void* ptr = VirtualAlloc(null, 192u, 12288u, 64u); void* ptr = VirtualAlloc(null, 256u, 12288u, 64u);
if (ptr == null) if (ptr == null)
{ {
return 0; return 0;
@@ -1596,20 +1596,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 82; ptr2[num++] = 82;
ptr2[num++] = 86; ptr2[num++] = 86;
ptr2[num++] = 87; ptr2[num++] = 87;
ptr2[num++] = 72; // sub rsp, 0x80 — reserve 8*16 bytes for the SysV variadic XMM save area
ptr2[num++] = 131; ptr2[num++] = 0x48; ptr2[num++] = 0x81; ptr2[num++] = 0xEC;
ptr2[num++] = 236; ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
ptr2[num++] = 16; // movdqu [rsp + i*0x10], xmm{i} for i = 0..7 (F3 0F 7F /r, SIB=0x24 base=rsp, disp8)
ptr2[num++] = 243; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x44; ptr2[num++] = 0x24; ptr2[num++] = 0x00; // xmm0
ptr2[num++] = 15; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x4C; ptr2[num++] = 0x24; ptr2[num++] = 0x10; // xmm1
ptr2[num++] = 127; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x54; ptr2[num++] = 0x24; ptr2[num++] = 0x20; // xmm2
ptr2[num++] = 4; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x5C; ptr2[num++] = 0x24; ptr2[num++] = 0x30; // xmm3
ptr2[num++] = 36; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x64; ptr2[num++] = 0x24; ptr2[num++] = 0x40; // xmm4
ptr2[num++] = 76; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x6C; ptr2[num++] = 0x24; ptr2[num++] = 0x50; // xmm5
ptr2[num++] = 141; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x74; ptr2[num++] = 0x24; ptr2[num++] = 0x60; // xmm6
ptr2[num++] = 100; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x7C; ptr2[num++] = 0x24; ptr2[num++] = 0x70; // xmm7
ptr2[num++] = 36; // lea r12, [rsp + 0x80] — r12 = argpack base (the 12 pushed GP regs), past the XMM area
ptr2[num++] = 16; ptr2[num++] = 0x4C; ptr2[num++] = 0x8D; ptr2[num++] = 0xA4; ptr2[num++] = 0x24;
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
ptr2[num++] = 72; ptr2[num++] = 72;
ptr2[num++] = 131; ptr2[num++] = 131;
ptr2[num++] = 236; ptr2[num++] = 236;
@@ -1657,6 +1658,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 131; ptr2[num++] = 131;
ptr2[num++] = 196; ptr2[num++] = 196;
ptr2[num++] = 40; ptr2[num++] = 40;
// movdqu xmm0, [r12 - 0x80] — reload the return XMM0 the gateway wrote into the
// argpack's xmm0 save slot (float/double returns: powf/logf/wcstod). SysV/Win64
// XMM regs are volatile across calls, so an unconditional reload is ABI-safe.
ptr2[num++] = 0xF3; ptr2[num++] = 0x41; ptr2[num++] = 0x0F; ptr2[num++] = 0x6F;
ptr2[num++] = 0x84; ptr2[num++] = 0x24; ptr2[num++] = 0x80; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF;
ptr2[num++] = 76; ptr2[num++] = 76;
ptr2[num++] = 137; ptr2[num++] = 137;
ptr2[num++] = 228; ptr2[num++] = 228;
@@ -1680,12 +1686,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 95; ptr2[num++] = 95;
ptr2[num++] = 195; ptr2[num++] = 195;
uint num2 = default(uint); uint num2 = default(uint);
if (!VirtualProtect(ptr, 192u, 32u, &num2)) if (!VirtualProtect(ptr, 256u, 32u, &num2))
{ {
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}"); Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
return 0; return 0;
} }
FlushInstructionCache(GetCurrentProcess(), ptr, 192u); FlushInstructionCache(GetCurrentProcess(), ptr, 256u);
return (nint)ptr; return (nint)ptr;
} }
catch catch
@@ -16,6 +16,7 @@ using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.IO;
namespace SharpEmu.Core.Runtime; namespace SharpEmu.Core.Runtime;
@@ -141,6 +142,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var image = LoadImage(normalizedEbootPath); var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version); VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId); SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false); RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress); KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Log.Info($"Entry: 0x{image.EntryPoint:X16}"); Log.Info($"Entry: 0x{image.EntryPoint:X16}");
@@ -357,6 +359,66 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result; return result;
} }
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
{
var executableName = Path.GetFileName(ebootPath);
if (string.IsNullOrWhiteSpace(executableName))
{
executableName = "eboot.bin";
}
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
var builder = new StringBuilder();
builder.AppendLine("App bundle info:");
builder.AppendLine($"- Display name: {displayName}");
builder.AppendLine($"- Version: {version}");
builder.AppendLine($"- Title ID: {titleId}");
builder.AppendLine($"- Content ID: {contentId}");
builder.AppendLine($"- Executable: {executableName}");
builder.Append("- Platform: PlayStation 5");
Log.Info(builder.ToString());
}
private static string? ResolveContentId(string? bundleRoot)
{
if (string.IsNullOrEmpty(bundleRoot))
{
return null;
}
foreach (var candidate in new[]
{
Path.Combine(bundleRoot, "sce_sys", "param.json"),
Path.Combine(bundleRoot, "param.json"),
})
{
if (!File.Exists(candidate))
{
continue;
}
try
{
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
{
return contentId.GetString();
}
}
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
{
}
break;
}
return null;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath) private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{ {
const string app0VariableName = "SHARPEMU_APP0_DIR"; const string app0VariableName = "SHARPEMU_APP0_DIR";
+2 -1
View File
@@ -43,7 +43,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<!-- Title bar --> <!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}"> <Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" /> <Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" />
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" /> <TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
<Border Classes="pill"> <Border Classes="pill">
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" /> <TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
-1
View File
@@ -21,7 +21,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" /> <AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
</ItemGroup> </ItemGroup>
+11 -13
View File
@@ -52,14 +52,7 @@ public static class KernelExports
ExportName = "exit", ExportName = "exit",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int Exit(CpuContext ctx) public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "XKRegsFpEpk", Nid = "XKRegsFpEpk",
@@ -172,11 +165,7 @@ public static class KernelExports
ExportName = "_exit", ExportName = "_exit",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int UnderscoreExit(CpuContext ctx) public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "Ac86z8q7T8A", Nid = "Ac86z8q7T8A",
@@ -437,4 +426,13 @@ public static class KernelExports
{ {
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal); return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
} }
private static int RequestProcessExit(CpuContext ctx, string syscallName)
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
} }
@@ -67,6 +67,13 @@ public static class KernelMemoryCompatExports
private const int Einval = 22; private const int Einval = 22;
private const int Erange = 34; private const int Erange = 34;
private const int Struncate = 80; private const int Struncate = 80;
private const int ClockRealtime = 0;
private const int ClockVirtual = 1;
private const int ClockProf = 2;
private const int ClockMonotonic = 4;
private const int ClockUptime = 5;
private const int ClockRealtimeFast = 10;
private const int ClockMonotonicFast = 12;
private const nuint DefaultLibcHeapAlignment = 16; private const nuint DefaultLibcHeapAlignment = 16;
private const ushort KernelStatModeDirectory = 0x41FF; private const ushort KernelStatModeDirectory = 0x41FF;
private const ushort KernelStatModeRegular = 0x81FF; private const ushort KernelStatModeRegular = 0x81FF;
@@ -1993,23 +2000,55 @@ public static class KernelMemoryCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int ClockGettime(CpuContext ctx) public static int ClockGettime(CpuContext ctx)
{ {
var clockId = unchecked((int)ctx[CpuRegister.Rdi]);
var timespecAddress = ctx[CpuRegister.Rsi]; var timespecAddress = ctx[CpuRegister.Rsi];
if (timespecAddress == 0) if (timespecAddress == 0)
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
return -1;
} }
var now = DateTimeOffset.UtcNow; long seconds;
var seconds = now.ToUnixTimeSeconds(); long nanoseconds;
var nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100; switch (clockId)
if (!ctx.TryWriteUInt64(timespecAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), unchecked((ulong)nanoseconds)))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; case ClockRealtime:
case ClockRealtimeFast:
case ClockVirtual:
case ClockProf:
{
var now = DateTimeOffset.UtcNow;
seconds = now.ToUnixTimeSeconds();
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
break;
}
case ClockMonotonic:
case ClockMonotonicFast:
case ClockUptime:
KernelRuntimeCompatExports.GetProcessMonotonicTime(out seconds, out nanoseconds);
break;
default:
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
return -1;
}
Span<byte> timespecBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
if (!ctx.Memory.TryWrite(timespecAddress, timespecBuffer))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
return -1;
} }
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return 0;
} }
[SysAbiExport( [SysAbiExport(
@@ -3846,33 +3885,59 @@ public static class KernelMemoryCompatExports
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
{ {
// SysV AMD64: variadic integer/pointer args use the GP registers (rdi..r9, up to
// 6) and variadic float/double args use xmm0..xmm7 (8) — INDEPENDENT counters.
// Args beyond the registers spill to the stack in source order, so GP-overflow
// and FP-overflow share one stack cursor.
private readonly CpuContext _ctx; private readonly CpuContext _ctx;
private int _gpIndex; private int _gpIndex;
private int _fpIndex;
private int _stackIndex;
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex) public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
{ {
_ctx = ctx; _ctx = ctx;
_gpIndex = gpIndex; _gpIndex = gpIndex;
_fpIndex = 0;
_stackIndex = 0;
} }
public ulong NextGpArg() public ulong NextGpArg()
{ {
var index = _gpIndex++; var index = _gpIndex;
return index switch if (index < 6)
{ {
0 => _ctx[CpuRegister.Rdi], _gpIndex++;
1 => _ctx[CpuRegister.Rsi], return index switch
2 => _ctx[CpuRegister.Rdx], {
3 => _ctx[CpuRegister.Rcx], 0 => _ctx[CpuRegister.Rdi],
4 => _ctx[CpuRegister.R8], 1 => _ctx[CpuRegister.Rsi],
5 => _ctx[CpuRegister.R9], 2 => _ctx[CpuRegister.Rdx],
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8) 3 => _ctx[CpuRegister.Rcx],
}; 4 => _ctx[CpuRegister.R8],
_ => _ctx[CpuRegister.R9],
};
}
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
} }
public double NextFloatArg() public double NextFloatArg()
{ {
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg())); // Float/double variadic args live in xmm0..xmm7 (low 64 bits), NOT the GP
// registers. Reading them from GP prints garbage and desynchronizes every
// subsequent argument.
ulong bits;
if (_fpIndex < 8)
{
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
}
else
{
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
}
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
} }
} }
@@ -194,13 +194,14 @@ public static class KernelRuntimeCompatExports
} }
else else
{ {
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter; GetProcessMonotonicTime(out seconds, out nanoseconds);
seconds = elapsedTicks / Stopwatch.Frequency;
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
} }
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) || Span<byte> timespecBuffer = stackalloc byte[16];
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds))) BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -617,6 +618,13 @@ public static class KernelRuntimeCompatExports
return address != 0 && ctx.TryWriteInt32(address, value); return address != 0 && ctx.TryWriteInt32(address, value);
} }
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
{
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
seconds = elapsedTicks / Stopwatch.Frequency;
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
}
[SysAbiExport( [SysAbiExport(
Nid = "bnZxYgAFeA0", Nid = "bnZxYgAFeA0",
ExportName = "sceKernelGetSanitizerNewReplaceExternal", ExportName = "sceKernelGetSanitizerNewReplaceExternal",
@@ -1469,26 +1477,42 @@ public static class KernelRuntimeCompatExports
return false; return false;
} }
var bytes = new List<byte>(Math.Min(maxLength, 64)); const int pageSize = 4096;
Span<byte> one = stackalloc byte[1]; const int inlineChunkSize = 64;
for (var i = 0; i < maxLength; i++) Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
var length = 0;
for (var offset = 0; offset < maxLength && length < buffer.Length;)
{ {
if (!ctx.Memory.TryRead(address + (ulong)i, one)) var current = address + (ulong)offset;
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
var chunkSize = Math.Min(
buffer.Length - length,
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
if (chunkSize <= 0)
{ {
return false; return false;
} }
if (one[0] == 0) var chunk = buffer.Slice(length, chunkSize);
if (!ctx.Memory.TryRead(current, chunk))
{ {
value = Encoding.UTF8.GetString(bytes.ToArray()); return false;
}
var nulIndex = chunk.IndexOf((byte)0);
if (nulIndex >= 0)
{
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
return true; return true;
} }
bytes.Add(one[0]); length += chunkSize;
offset += chunkSize;
} }
value = Encoding.UTF8.GetString(bytes.ToArray()); return false;
return true;
} }
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset) private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
@@ -1697,7 +1721,7 @@ public static class KernelRuntimeCompatExports
return null; return null;
} }
Span<byte> stub = stackalloc byte[] ReadOnlySpan<byte> stub = stackalloc byte[]
{ {
0x0F, 0x31, 0x0F, 0x31,
0x48, 0xC1, 0xE2, 0x20, 0x48, 0xC1, 0xE2, 0x20,
@@ -1705,7 +1729,14 @@ public static class KernelRuntimeCompatExports
0xC3, 0xC3,
}; };
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length); unsafe
{
fixed (byte* src = stub)
{
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
}
}
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress); return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
} }
catch catch
+144
View File
@@ -0,0 +1,144 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
namespace SharpEmu.Logging;
/// <summary>
/// Build provenance for the running emulator, populated at compile time from
/// GitHub Actions environment variables via <c>[AssemblyMetadata]</c> and
/// surfaced as a touchHLE-style banner at the top of the log.
/// </summary>
public static class BuildInfo
{
private const string ProjectUrl = "https://github.com/par274/sharpemu";
private const string CanonicalRepository = "par274/sharpemu";
/// <summary>Short commit hash the build was produced from, or <c>null</c>.</summary>
public static string? CommitSha { get; }
/// <summary>Branch the build was produced from, or <c>null</c>.</summary>
public static string? Branch { get; }
/// <summary>Repository slug (<c>owner/name</c>) the build came from, or <c>null</c>.</summary>
public static string? Repository { get; }
/// <summary>URL of the GitHub Actions workflow run that produced the build, or <c>null</c>.</summary>
public static string? WorkflowRunUrl { get; }
/// <summary>Build configuration (e.g. <c>Debug</c> or <c>Release</c>), or <c>null</c>.</summary>
public static string? Configuration { get; }
/// <summary>
/// Whether this build is an official release: a Release-configuration CI build
/// from the canonical repository, produced by a push to <c>main</c> or a manual
/// workflow dispatch (matching the CI <c>release</c> job). All other builds
/// (pull requests, forks, feature branches, and local/Debug builds) are
/// considered unofficial.
/// </summary>
public static bool IsOfficialRelease { get; }
static BuildInfo()
{
var metadata = ReadMetadata();
CommitSha = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildSha"));
if (CommitSha is { Length: > 7 })
{
CommitSha = CommitSha[..7];
}
Branch = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildBranch"));
Repository = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRepository"));
Configuration = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildConfiguration"));
var serverUrl = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildServerUrl"));
var runId = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRunId"));
if (serverUrl is not null && Repository is not null && runId is not null)
{
WorkflowRunUrl = $"{serverUrl.TrimEnd('/')}/{Repository}/actions/runs/{runId}";
}
var eventName = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildEventName"));
var gitRef = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRef"));
var isReleaseConfig = string.Equals(Configuration, "Release", StringComparison.OrdinalIgnoreCase);
var isCanonicalRepo = string.Equals(Repository, CanonicalRepository, StringComparison.OrdinalIgnoreCase);
var isReleaseTrigger =
string.Equals(eventName, "workflow_dispatch", StringComparison.OrdinalIgnoreCase) ||
(string.Equals(eventName, "push", StringComparison.OrdinalIgnoreCase) &&
string.Equals(gitRef, "refs/heads/main", StringComparison.OrdinalIgnoreCase));
IsOfficialRelease = isReleaseConfig && isCanonicalRepo && isReleaseTrigger && CommitSha is not null;
}
/// <summary>
/// The multi-line banner, e.g.
/// <code>
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/par274/sharpemu
///
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
/// </code>
/// Official release builds drop the <c>UNOFFICIAL</c> tag. Falls back to a
/// local-build line when no CI provenance is present.
/// </summary>
public static string Banner
{
get
{
string version;
if (CommitSha is null)
{
version = "UNOFFICIAL";
}
else if (IsOfficialRelease)
{
version = CommitSha;
}
else
{
version = $"UNOFFICIAL {CommitSha}";
}
var header = $"SharpEmu {version} — {ProjectUrl}";
if (Branch is null || Repository is null || WorkflowRunUrl is null)
{
return $"{header}{Environment.NewLine}{Environment.NewLine}Local build (not produced by CI).";
}
return $"{header}{Environment.NewLine}{Environment.NewLine}" +
$"Built from branch \"{Branch}\" of \"{Repository}\" by GitHub Actions workflow run {WorkflowRunUrl}.";
}
}
private static Dictionary<string, string> ReadMetadata()
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
var assemblies = new[] { Assembly.GetEntryAssembly(), typeof(BuildInfo).Assembly };
foreach (var assembly in assemblies)
{
if (assembly is null)
{
continue;
}
foreach (var attribute in assembly.GetCustomAttributes<AssemblyMetadataAttribute>())
{
if (attribute.Key.StartsWith("SharpEmu.Build", StringComparison.Ordinal) &&
!result.ContainsKey(attribute.Key) &&
attribute.Value is not null)
{
result[attribute.Key] = attribute.Value;
}
}
}
return result;
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}