diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs
index 2b1ff4a..d07a494 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 803d77f..cbc1876 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 76c2b1f..d818c07 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 0dd8629..9e30df8 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 4d15503..109995e 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 826dbe6..0c41b05 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 5a363a4..eb6ceeb 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 ea83603..c8ab508 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 7a74f4f..92ebf16 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 8c49830..5399fe9 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 0000000..142dbcf
--- /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 0000000..4470338
--- /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 8b747e4..83a9a14 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 5190ff2..dfb1cb8 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 05dffd1..3aff386 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",