From 373100a6b04c85b15488257ca259b6905e14a54e Mon Sep 17 00:00:00 2001
From: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
Date: Tue, 14 Jul 2026 19:36:15 -0500
Subject: [PATCH] Add 21 missing SysAbi exports and GUI Environment tab for
SHARPEMU_* toggles (#189)
Fills NID gaps hit by PS5 titles during boot, controller setup, and
rendering, and surfaces the common runtime switches in the GUI. All
exports are additive (no behavior change to existing exports) and free of
NID and export-name collisions with upstream.
New export libraries:
- libSceBluetoothHid: Init/RegisterDevice/RegisterCallback success stubs so
titles proceed past Bluetooth controller setup (opt-out via
SHARPEMU_BTHID_UNAVAILABLE=1).
- libSceNpCppWebApi: Common::initialize no-op success; UE5 online titles
abort PS5-component startup on a negative SCE error.
Additions to existing libraries:
- libScePad: scePadOpenExt (shared PadOpenCore, accepts special ports 1/2 and
the ScePadOpenExtParam pointer), scePadClose, scePadGetExtControllerInformation.
- libSceVideoOut: sceVideoOutConfigureOutput, sceVideoOutInitializeOutputOptions.
- libSceAgc: DCB builders sceAgcDcbSetIndexCount, sceAgcDcbJump, DcbSetPredication,
SetPacketPredication (emit valid skippable packets; full draw processing TODO).
- libSceAmpr: measure and write KernelEventQueueOnCompletion pair.
- libKernel: scePthreadGet/Setschedparam, sceKernelChmod (validate and accept;
POSIX permission bits have no host equivalent on Windows).
- libSceNetCtl: sceNetCtlRegisterCallbackV6 (delegates to the v4 callback).
- libSceMouse: sceMouseInit.
- libSceUserService: sceUserServiceGetAgeLevel (adult, skips parental gates).
GUI: new Options Environment tab exposing common SHARPEMU_* switches as
toggles (BTHID_UNAVAILABLE, DISABLE_IMPORT_LOOP_GUARD, VK_VALIDATION,
DUMP_SPIRV, LOG_DIRECT_MEMORY, LOG_NP). Persisted in gui-settings.json and
applied to the emulator process environment at launch; localized with
English fallback.
---
src/SharpEmu.GUI/GuiSettings.cs | 3 +
src/SharpEmu.GUI/Languages/en.json | 9 ++
src/SharpEmu.GUI/MainWindow.axaml | 82 +++++++++++++++++
src/SharpEmu.GUI/MainWindow.axaml.cs | 64 +++++++++++++
src/SharpEmu.Libs/Agc/AgcExports.cs | 91 +++++++++++++++++++
src/SharpEmu.Libs/Ampr/AmprExports.cs | 46 ++++++++++
.../Kernel/KernelMemoryCompatExports.cs | 39 ++++++++
.../KernelPthreadExtendedCompatExports.cs | 41 +++++++++
src/SharpEmu.Libs/Mouse/MouseExports.cs | 11 +++
src/SharpEmu.Libs/Network/NetCtlExports.cs | 7 ++
src/SharpEmu.Libs/Np/NpCppWebApiExports.cs | 34 +++++++
src/SharpEmu.Libs/Pad/BluetoothHidExports.cs | 42 +++++++++
src/SharpEmu.Libs/Pad/PadExports.cs | 70 +++++++++++++-
.../UserService/UserServiceExports.cs | 25 +++++
src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 28 ++++++
15 files changed, 590 insertions(+), 2 deletions(-)
create mode 100644 src/SharpEmu.Libs/Np/NpCppWebApiExports.cs
create mode 100644 src/SharpEmu.Libs/Pad/BluetoothHidExports.cs
diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs
index 2b1ff4a3..d07a4949 100644
--- a/src/SharpEmu.GUI/GuiSettings.cs
+++ b/src/SharpEmu.GUI/GuiSettings.cs
@@ -48,6 +48,9 @@ public sealed class GuiSettings
/// Publish launcher/game status to Discord Rich Presence.
public bool DiscordRichPresence { get; set; } = true;
+ /// Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.
+ public List EnvironmentToggles { get; set; } = new();
+
///
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json
index 803d77f8..cbc1876c 100644
--- a/src/SharpEmu.GUI/Languages/en.json
+++ b/src/SharpEmu.GUI/Languages/en.json
@@ -26,6 +26,15 @@
"Library.Loading": "Loading library…",
"Options.General": "General",
+ "Options.Env.Tab": "Environment",
+ "Options.Section.Environment": "ENVIRONMENT VARIABLES",
+ "Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
+ "Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
+ "Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
+ "Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
+ "Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
+ "Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
+ "Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml
index 76c2b1f7..d818c076 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml
+++ b/src/SharpEmu.GUI/MainWindow.axaml
@@ -387,6 +387,88 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs
index 0dd8629e..9e30df80 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml.cs
+++ b/src/SharpEmu.GUI/MainWindow.axaml.cs
@@ -125,6 +125,18 @@ public partial class MainWindow : Window
UpdateDiscordPresence();
};
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
+ EnvBthidToggle.IsCheckedChanged += (_, _) =>
+ SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
+ EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
+ SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
+ EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
+ SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
+ EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
+ SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
+ EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
+ SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
+ EnvLogNpToggle.IsCheckedChanged += (_, _) =>
+ SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
@@ -403,6 +415,15 @@ public partial class MainWindow : Window
LoadingStateText.Text = loc.Get("Library.Loading");
GeneralTabItem.Header = loc.Get("Options.General");
+ EnvTabItem.Header = loc.Get("Options.Env.Tab");
+ EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
+ EnvDesc.Text = loc.Get("Options.Env.Desc");
+ EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
+ EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
+ EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
+ EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
+ EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
+ EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
@@ -584,9 +605,34 @@ public partial class MainWindow : Window
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
+ EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
+ EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
+ EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
+ EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
+ EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
+ EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
UpdateLogFilePathText();
}
+ // Environment variables set on this process at the previous launch; children
+ // inherit the process environment, so stale names must be cleared explicitly.
+ private readonly HashSet _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
+
+ private void SetEnvironmentToggle(string name, bool enabled)
+ {
+ if (enabled)
+ {
+ if (!_settings.EnvironmentToggles.Contains(name))
+ {
+ _settings.EnvironmentToggles.Add(name);
+ }
+ }
+ else
+ {
+ _settings.EnvironmentToggles.Remove(name);
+ }
+ }
+
private string SelectedLogLevel()
{
return LogLevelBox.SelectedIndex switch
@@ -1402,6 +1448,24 @@ public partial class MainWindow : Window
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
DimLineBrush);
+ // Apply the enabled switches to this process; both emulator launch paths
+ // (CreateProcessW and Process.Start) inherit it. Clear switches turned
+ // off since the previous launch.
+ foreach (var staleName in _appliedEnvironmentVariables)
+ {
+ if (!_settings.EnvironmentToggles.Contains(staleName))
+ {
+ Environment.SetEnvironmentVariable(staleName, null);
+ }
+ }
+
+ _appliedEnvironmentVariables.Clear();
+ foreach (var name in _settings.EnvironmentToggles)
+ {
+ Environment.SetEnvironmentVariable(name, "1");
+ _appliedEnvironmentVariables.Add(name);
+ }
+
var emulator = new EmulatorProcess();
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs
index 4d155033..109995e0 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.cs
@@ -29,6 +29,7 @@ public static class AgcExports
private const uint ItDispatchDirect = 0x15;
private const uint ItDispatchIndirect = 0x16;
private const uint ItWaitRegMem = 0x3C;
+ private const uint ItIndirectBuffer = 0x3F;
private const uint ItEventWrite = 0x46;
private const uint ItDmaData = 0x50;
private const uint ItSetContextReg = 0x69;
@@ -1943,6 +1944,96 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
+ // Guest draw-command builders: emit valid (skippable) packets and return a live
+ // command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
+ [SysAbiExport(
+ Nid = "8N2tmT3jmC8",
+ ExportName = "sceAgcDcbSetIndexCount",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAgc")]
+ public static int DcbSetIndexCount(CpuContext ctx)
+ {
+ var dcb = ctx[CpuRegister.Rdi];
+ var indexCount = (uint)ctx[CpuRegister.Rsi];
+ if (dcb == 0)
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
+ !ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
+ !ctx.TryWriteUInt32(cmd + 4, indexCount))
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ return ReturnPointer(ctx, cmd);
+ }
+
+ [SysAbiExport(
+ Nid = "xSAR0LTcRKM",
+ ExportName = "sceAgcDcbJump",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAgc")]
+ public static int DcbJump(CpuContext ctx)
+ {
+ var dcb = ctx[CpuRegister.Rdi];
+ var target = ctx[CpuRegister.Rsi];
+ var sizeDwords = (uint)ctx[CpuRegister.Rdx];
+ if (dcb == 0)
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
+ !ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
+ !ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
+ !ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
+ !ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ return ReturnPointer(ctx, cmd);
+ }
+
+ [SysAbiExport(
+ Nid = "bbFueFP+J4k",
+ ExportName = "sceAgcDcbSetPredication",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAgc")]
+ public static int DcbSetPredication(CpuContext ctx)
+ {
+ var dcb = ctx[CpuRegister.Rdi];
+ var address = ctx[CpuRegister.Rsi];
+ if (dcb == 0)
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
+ !ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
+ !ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
+ !ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ return ReturnPointer(ctx, cmd);
+ }
+
+ [SysAbiExport(
+ Nid = "w6Dj1VJt5qY",
+ ExportName = "sceAgcSetPacketPredication",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAgc")]
+ public static int SetPacketPredication(CpuContext ctx)
+ {
+ // Global predication toggle on a packet; a no-op is safe for rendering.
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "w2rJhmD+dsE",
ExportName = "sceAgcDriverAddEqEvent",
diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs
index 826dbe6e..0c41b05e 100644
--- a/src/SharpEmu.Libs/Ampr/AmprExports.cs
+++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs
@@ -348,6 +348,18 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(
+ Nid = "Zi3dBUjgyXI",
+ ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAmpr")]
+ public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
+ {
+ TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
+ ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
@@ -440,6 +452,40 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(
+ Nid = "o67gODLFpls",
+ ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAmpr")]
+ public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
+ {
+ var commandBuffer = ctx[CpuRegister.Rdi];
+ var equeue = ctx[CpuRegister.Rsi];
+ var ident = ctx[CpuRegister.Rdx];
+ var completionToken = ctx[CpuRegister.Rcx];
+ var userData = ctx[CpuRegister.R8];
+
+ if (commandBuffer == 0)
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
+ }
+
+ if (!AppendKernelEventQueueRecord(
+ ctx,
+ commandBuffer,
+ equeue,
+ ident,
+ completionToken,
+ userData))
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+ }
+
+ TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
index 5a363a47..eb6ceeb5 100644
--- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
@@ -1834,6 +1834,45 @@ public static class KernelMemoryCompatExports
}
}
+ [SysAbiExport(
+ Nid = "fgIsQ10xYVA",
+ ExportName = "sceKernelChmod",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int KernelChmod(CpuContext ctx)
+ {
+ var pathAddress = ctx[CpuRegister.Rdi];
+ var mode = unchecked((uint)ctx[CpuRegister.Rsi]);
+ if (pathAddress == 0)
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
+ }
+
+ if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+ }
+
+ var hostPath = ResolveGuestPath(guestPath);
+ if (IsReadOnlyGuestMutationPath(guestPath))
+ {
+ LogOpenTrace($"chmod readonly path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
+ }
+
+ if (!File.Exists(hostPath) && !Directory.Exists(hostPath))
+ {
+ AddNegativeStatCacheForGuestPath(guestPath);
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
+ }
+
+ // POSIX permission bits have no host equivalent on Windows; accept the call
+ // so guests that chmod their freshly created files/directories can proceed.
+ LogOpenTrace($"chmod path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "NNtFaKJbPt0",
ExportName = "_close",
diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
index ea836038..c8ab5089 100644
--- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
@@ -382,6 +382,47 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(
+ Nid = "oIRFTjoILbg",
+ ExportName = "scePthreadSetschedparam",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int PthreadSetschedparam(CpuContext ctx) => PosixPthreadSetschedparam(ctx);
+
+ [SysAbiExport(
+ Nid = "P41kTWUS3EI",
+ ExportName = "scePthreadGetschedparam",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int PthreadGetschedparam(CpuContext ctx)
+ {
+ var thread = ctx[CpuRegister.Rdi];
+ var outPolicyAddress = ctx[CpuRegister.Rsi];
+ var outSchedParamAddress = ctx[CpuRegister.Rdx];
+ if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
+ }
+
+ int policy;
+ int priority;
+ lock (_stateGate)
+ {
+ var state = GetOrCreateThreadStateLocked(thread);
+ policy = state.Attributes.SchedPolicy;
+ priority = state.Priority;
+ }
+
+ if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
+ !ctx.TryWriteInt32(outSchedParamAddress, priority))
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+ }
+
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "nsYoNRywwNg",
ExportName = "scePthreadAttrInit",
diff --git a/src/SharpEmu.Libs/Mouse/MouseExports.cs b/src/SharpEmu.Libs/Mouse/MouseExports.cs
index 7a74f4ff..92ebf167 100644
--- a/src/SharpEmu.Libs/Mouse/MouseExports.cs
+++ b/src/SharpEmu.Libs/Mouse/MouseExports.cs
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
public static class MouseExports
{
+ [SysAbiExport(
+ Nid = "Qs0wWulgl7U",
+ ExportName = "sceMouseInit",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceMouse")]
+ public static int MouseInit(CpuContext ctx)
+ {
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
// as an sceNgs2VoiceGetState alias.
[SysAbiExport(
diff --git a/src/SharpEmu.Libs/Network/NetCtlExports.cs b/src/SharpEmu.Libs/Network/NetCtlExports.cs
index 8c498309..5399fe98 100644
--- a/src/SharpEmu.Libs/Network/NetCtlExports.cs
+++ b/src/SharpEmu.Libs/Network/NetCtlExports.cs
@@ -146,6 +146,13 @@ public static class NetCtlExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
}
+ [SysAbiExport(
+ Nid = "1NE9OWdBIww",
+ ExportName = "sceNetCtlRegisterCallbackV6",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceNetCtl")]
+ public static int NetCtlRegisterCallbackV6(CpuContext ctx) => NetCtlRegisterCallback(ctx);
+
[SysAbiExport(
Nid = "obuxdTiwkF8",
ExportName = "sceNetCtlGetInfo",
diff --git a/src/SharpEmu.Libs/Np/NpCppWebApiExports.cs b/src/SharpEmu.Libs/Np/NpCppWebApiExports.cs
new file mode 100644
index 00000000..142dbcf0
--- /dev/null
+++ b/src/SharpEmu.Libs/Np/NpCppWebApiExports.cs
@@ -0,0 +1,34 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.HLE;
+
+namespace SharpEmu.Libs.Np;
+
+// Stub for sce::Np::CppWebApi: titles abort PS5-component startup if
+// Common::initialize returns a negative SCE error, so no-op success is required to boot.
+public static class NpCppWebApiExports
+{
+ [SysAbiExport(
+ Nid = "UYPxv8MIzGo",
+ ExportName = "_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceNpCppWebApi")]
+ public static int CppWebApiCommonInitialize(CpuContext ctx)
+ {
+ // int Common::initialize(const InitParams&, LibContext&) — 0 on success.
+ TraceCppWebApi("common_initialize", ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
+ return ctx.SetReturn(0);
+ }
+
+ private static void TraceCppWebApi(string operation, ulong arg0, ulong arg1)
+ {
+ if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ Console.Error.WriteLine(
+ $"[LOADER][TRACE] np_cppwebapi.{operation} arg0=0x{arg0:X16} arg1=0x{arg1:X16}");
+ }
+}
diff --git a/src/SharpEmu.Libs/Pad/BluetoothHidExports.cs b/src/SharpEmu.Libs/Pad/BluetoothHidExports.cs
new file mode 100644
index 00000000..44703382
--- /dev/null
+++ b/src/SharpEmu.Libs/Pad/BluetoothHidExports.cs
@@ -0,0 +1,42 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.HLE;
+
+namespace SharpEmu.Libs.Pad;
+
+// Minimal libSceBluetoothHid stub: no host Bluetooth passthrough, so report
+// success and let the Pad path provide input (SHARPEMU_BTHID_UNAVAILABLE=1 fails instead).
+public static class BluetoothHidExports
+{
+ private const int BluetoothHidUnavailable = unchecked((int)0x80960001);
+
+ private static readonly bool _reportUnavailable = string.Equals(
+ Environment.GetEnvironmentVariable("SHARPEMU_BTHID_UNAVAILABLE"),
+ "1",
+ StringComparison.Ordinal);
+
+ private static int Result(CpuContext ctx) =>
+ ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
+
+ [SysAbiExport(
+ Nid = "tul3-GzejQc",
+ ExportName = "sceBluetoothHidInit",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceBluetoothHid")]
+ public static int BluetoothHidInit(CpuContext ctx) => Result(ctx);
+
+ [SysAbiExport(
+ Nid = "4FUZ+c52d2k",
+ ExportName = "sceBluetoothHidRegisterDevice",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceBluetoothHid")]
+ public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
+
+ [SysAbiExport(
+ Nid = "4Ypfo9RIwfM",
+ ExportName = "sceBluetoothHidRegisterCallback",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceBluetoothHid")]
+ public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
+}
diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs
index 8b747e42..83a9a140 100644
--- a/src/SharpEmu.Libs/Pad/PadExports.cs
+++ b/src/SharpEmu.Libs/Pad/PadExports.cs
@@ -48,7 +48,18 @@ public static class PadExports
ExportName = "scePadOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
- public static int PadOpen(CpuContext ctx)
+ public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
+
+ [SysAbiExport(
+ Nid = "WFIiSfXGUq8",
+ ExportName = "scePadOpenExt",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
+
+ // scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
+ // ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
+ private static int PadOpenCore(CpuContext ctx, bool extended)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
@@ -64,7 +75,8 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
- if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
+ var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
+ if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
@@ -83,6 +95,19 @@ public static class PadExports
return ctx.SetReturn(PrimaryPadHandle);
}
+ [SysAbiExport(
+ Nid = "6ncge5+l5Qs",
+ ExportName = "scePadClose",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadClose(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ return handle == PrimaryPadHandle
+ ? ctx.SetReturn(0)
+ : ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
[SysAbiExport(
Nid = "clVvL4ZDntw",
ExportName = "scePadSetMotionSensorState",
@@ -131,6 +156,47 @@ public static class PadExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
+ [SysAbiExport(
+ Nid = "hGbf2QTBmqc",
+ ExportName = "scePadGetExtControllerInformation",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadGetExtControllerInformation(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ var informationAddress = ctx[CpuRegister.Rsi];
+ if (handle != PrimaryPadHandle)
+ {
+ return ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
+ if (informationAddress == 0)
+ {
+ return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ }
+
+ // Base ScePadControllerInformation + device-class/connection fields: report a connected
+ // DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
+ Span information = stackalloc byte[0x40];
+ information.Clear();
+ BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
+ BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
+ BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
+ information[0x08] = 30;
+ information[0x09] = 30;
+ information[0x0A] = StandardPortType;
+ information[0x0B] = 1; // connected count
+ information[0x0C] = 1; // connected
+ BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
+ information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
+ information[0x1D] = 1; // connected (ext)
+ information[0x1E] = 0; // connectionType: local
+
+ return ctx.Memory.TryWrite(informationAddress, information)
+ ? ctx.SetReturn(0)
+ : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
[SysAbiExport(
Nid = "YndgXqQVV7c",
ExportName = "scePadReadState",
diff --git a/src/SharpEmu.Libs/UserService/UserServiceExports.cs b/src/SharpEmu.Libs/UserService/UserServiceExports.cs
index 5190ff25..dfb1cb8f 100644
--- a/src/SharpEmu.Libs/UserService/UserServiceExports.cs
+++ b/src/SharpEmu.Libs/UserService/UserServiceExports.cs
@@ -185,6 +185,31 @@ public static class UserServiceExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
+ [SysAbiExport(
+ Nid = "woNpu+45RLk",
+ ExportName = "sceUserServiceGetAgeLevel",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceUserService")]
+ public static int UserServiceGetAgeLevel(CpuContext ctx)
+ {
+ var userId = unchecked((int)ctx[CpuRegister.Rdi]);
+ var ageLevelAddress = ctx[CpuRegister.Rsi];
+ if (userId != 1000)
+ {
+ return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
+ }
+
+ if (ageLevelAddress == 0)
+ {
+ return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
+ }
+
+ // Report an adult account so titles skip parental-restriction paths.
+ return ctx.TryWriteInt32(ageLevelAddress, 21)
+ ? ctx.SetReturn(0)
+ : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
private static void TraceUserService(string message)
{
if (_traceUserService)
diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
index 05dffd18..3aff3866 100644
--- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
+++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
@@ -403,6 +403,34 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(
+ Nid = "+I4K03i3EL0",
+ ExportName = "sceVideoOutInitializeOutputOptions",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceVideoOut")]
+ public static int VideoOutInitializeOutputOptions(CpuContext ctx)
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
+ [SysAbiExport(
+ Nid = "w0hLuNarQxY",
+ ExportName = "sceVideoOutConfigureOutput",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceVideoOut")]
+ public static int VideoOutConfigureOutput(CpuContext ctx)
+ {
+ // Accept the requested output configuration; the presenter always renders
+ // at the display buffer's native size.
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ if (!TryGetPort(handle, out _))
+ {
+ return OrbisVideoOutErrorInvalidHandle;
+ }
+
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "utPrVdxio-8",
ExportName = "sceVideoOutGetOutputStatus",