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",