Fix/deadcells crash (#262)

* Boot compatibility fixes for UE titles, GUI toggles, and DeS render/boot work

Checkpoint of the Monster Truck Championship and Demon's Souls boot work.
Each piece is independently useful and verified against the titles.

- playgo: scePlayGoGetLocus now returns BAD_CHUNK_ID for chunk ids outside
  the known set, matching real firmware. Titles enumerate chunk ids until
  that error; answering OK for every id made the scan wrap the ushort range
  and spin forever. Missing-sidecar and no-app0 fallbacks report a
  fully-installed single chunk 0 so scePlayGoOpen keeps succeeding.
- kernel: restore the SHARPEMU_WRITABLE_APP0 opt-in. Unpackaged UE dumps
  write their Saved tree under /app0 during PS5 component init and treat
  the denial as a fatal boot error.
- pad: accept handle 0 as the primary pad across all pad calls. Real
  firmware hands out small non-negative handles and some titles read state
  with handle 0.
- bthid: env-gated experiment hooks for the Thrustmaster wheel middleware
  investigation (fail-only-RegisterCallback modes and a synthetic
  enumeration callback with a zeroed event struct). All default off.
- gui: add SHARPEMU_LOG_IO and SHARPEMU_WRITABLE_APP0 toggles to the
  Environment tab.
- videoout: per-swapchain-image render-finished semaphores (the shared
  semaphore raced the swapchain); whole-mip-chain layout init for offscreen
  guest images (sampled binds read mips stuck in Undefined); GPU-resident
  texture availability now canonicalizes through the texture format table
  and accepts compatibility-class aliases, cutting per-frame CPU texture
  re-reads (143 GB -> 55 GB per 300 s in Demon's Souls, 0.2 -> 0.5 fps).
- hle: add sceSystemServiceGetNoticeScreenSkipFlag,
  sceSystemServiceGetMainAppTitleId (title id published from the runtime),
  and sceNpWebApi2CreateUserContext (refuses so the online layer backs off).
- rtc: SHARPEMU_RTC_PROBE_RANGE diagnostic dumps the code around a busy-wait
  caller of sceRtcGetCurrentTick once; costs nothing when unset.

* [ShaderCompiler] Fix VReadlaneB32 scalar destination field

The scalar destination lives in the low vdst byte (bits 0-7); it was read
from bits 8-14, the VOP3B carry-out field readlane does not have, sending
every readlane result to s0. Verified against raw gfx10 encodings and
LLVM's assembler tests (v_readlane_b32 s5, v1, s2 -> low byte 0x05).

* [VideoOut] Survive device loss and flip-order asserts without dying

Two ways a frame could take down the whole presenter:

- Device loss between any two Vulkan calls in a frame unwound the window
  thread, and the Dispose-time fence check then threw again, masking the
  original error. Catch the loss at the frame boundary, retire
  presentations and guest submissions whose fences can never signal, and
  keep the window loop pumping so the game (audio, logic) carries on.
- The ordered-flip capture invariant is violated ~100 times per run by
  Demon's Souls (PPSA01342); on debug builds the Debug.Assert fail-fasts
  the process with nothing in the log. Downgrade it to a once-per-version
  warning until the capture/wait ordering is understood.

* Fix Dead Cells shader cache regression

---------

Co-authored-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
This commit is contained in:
Spooks
2026-07-16 04:57:16 -06:00
committed by GitHub
parent 7b91c964fc
commit b4b95014f1
14 changed files with 567 additions and 47 deletions
@@ -12,6 +12,7 @@ using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using SharpEmu.Libs.Fiber;
using SharpEmu.Libs.SystemService;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
@@ -141,6 +142,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
SystemServiceExports.ConfigureApplicationInfo(image.TitleId);
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
+2
View File
@@ -31,9 +31,11 @@
"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.WritableApp0.Desc": "Allow titles to create and write files inside their install folder.\nNeeded by unpackaged dumps that write their save or config data under /app0.",
"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.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
+22
View File
@@ -420,6 +420,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_WRITABLE_APP0" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvWritableApp0Desc"
Text="Allow titles to create and write files inside their install folder.&#10;Needed by unpackaged dumps that write their save or config data under /app0."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvWritableApp0Toggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
@@ -453,6 +464,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_IO" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogIoDesc"
Text="Log file open, read, and path-resolve activity to the console.&#10;Use when a game cannot find its data files during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogIoToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
+8
View File
@@ -136,12 +136,16 @@ public partial class MainWindow : Window
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.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);
EnvLogIoToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_IO", EnvLogIoToggle.IsChecked == true);
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
@@ -427,9 +431,11 @@ public partial class MainWindow : Window
EnvDesc.Text = loc.Get("Options.Env.Desc");
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.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");
EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
@@ -614,9 +620,11 @@ public partial class MainWindow : Window
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
EnvWritableApp0Toggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_WRITABLE_APP0");
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");
EnvLogIoToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_IO");
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
UpdateLogFilePathText();
}
@@ -1574,7 +1574,27 @@ public static partial class KernelMemoryCompatExports
ExportName = "stat",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int PosixStat(CpuContext ctx) => KernelStat(ctx);
public static int PosixStat(CpuContext ctx)
{
var result = KernelStat(ctx);
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return 0;
}
// stat(2) follows the libc/POSIX ABI: failures return -1 and expose
// the reason through errno. Returning the raw Orbis kernel code here
// makes callers treat a missing file as a non-negative success value.
var errno = result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
_ => 2, // ENOENT
};
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
[SysAbiExport(
Nid = "gEpBkcwxUjw",
@@ -4219,31 +4239,50 @@ public static partial class KernelMemoryCompatExports
{
private readonly CpuContext _ctx;
private int _gpIndex;
private int _fpIndex;
private int _stackIndex;
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
{
_ctx = ctx;
_gpIndex = gpIndex;
_fpIndex = 0;
_stackIndex = 0;
}
public ulong NextGpArg()
{
var index = _gpIndex++;
return index switch
var index = _gpIndex;
if (index < 6)
{
0 => _ctx[CpuRegister.Rdi],
1 => _ctx[CpuRegister.Rsi],
2 => _ctx[CpuRegister.Rdx],
3 => _ctx[CpuRegister.Rcx],
4 => _ctx[CpuRegister.R8],
5 => _ctx[CpuRegister.R9],
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
};
_gpIndex++;
return index switch
{
0 => _ctx[CpuRegister.Rdi],
1 => _ctx[CpuRegister.Rsi],
2 => _ctx[CpuRegister.Rdx],
3 => _ctx[CpuRegister.Rcx],
4 => _ctx[CpuRegister.R8],
_ => _ctx[CpuRegister.R9],
};
}
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
}
public double NextFloatArg()
{
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
ulong bits;
if (_fpIndex < 8)
{
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
}
else
{
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
}
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
}
}
@@ -4806,8 +4845,19 @@ public static partial class KernelMemoryCompatExports
private static bool IsMutatingOpen(int flags) =>
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
// Dev-build dumps (unpackaged UE titles, etc.) may write their Saved/ tree under
// /app0, which is read-only on retail hardware. Opt in via SHARPEMU_WRITABLE_APP0=1
// to allow those writes so such dumps can boot; defaults off to keep retail semantics.
private static readonly bool _writableApp0 =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_WRITABLE_APP0"), "1", StringComparison.Ordinal);
public static bool IsReadOnlyGuestMutationPath(string guestPath)
{
if (_writableApp0)
{
return false;
}
var normalized = NormalizeGuestStatCachePath(guestPath);
return normalized is not null &&
(string.Equals(normalized, "/app0", StringComparison.OrdinalIgnoreCase) ||
+13
View File
@@ -43,6 +43,19 @@ public static class NpWebApi2Exports
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "sk54bi6FtYM",
ExportName = "sceNpWebApi2CreateUserContext",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2CreateUserContext(CpuContext ctx)
{
// No PSN backend: refuse user-context creation so the title's online
// layer backs off instead of driving a half-created context handle.
TraceNpWebApi2("create-user-context", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
}
[SysAbiExport(
Nid = "bEvXpcEk200",
ExportName = "sceNpWebApi2Terminate",
+106 -2
View File
@@ -1,6 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Pad;
@@ -16,6 +18,19 @@ public static class BluetoothHidExports
"1",
StringComparison.Ordinal);
// EXPERIMENT: fire the registered callback once with a zeroed event struct.
// Direct execution shares the host address space with the guest, so an
// AllocHGlobal buffer is directly readable by guest code.
// SHARPEMU_BTHID_FIRE_CALLBACK=1 enables; SHARPEMU_BTHID_EVENT_CODE and
// SHARPEMU_BTHID_EVENT_SIZE (default 0 / 256) shape the synthetic event.
private static readonly bool _fireCallback = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_FIRE_CALLBACK"),
"1",
StringComparison.Ordinal);
private static ulong _callbackFunction;
private static int _fired;
private static int Result(CpuContext ctx) =>
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
@@ -31,12 +46,101 @@ public static class BluetoothHidExports
ExportName = "sceBluetoothHidRegisterDevice",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceBluetoothHid")]
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
public static int BluetoothHidRegisterDevice(CpuContext ctx)
{
var result = Result(ctx);
if (_fireCallback && _callbackFunction >= 0x10000 &&
Interlocked.Exchange(ref _fired, 1) == 0)
{
FireEnumerationComplete(ctx);
}
return result;
}
[SysAbiExport(
Nid = "4Ypfo9RIwfM",
ExportName = "sceBluetoothHidRegisterCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceBluetoothHid")]
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
public static int BluetoothHidRegisterCallback(CpuContext ctx)
{
_callbackFunction = ctx[CpuRegister.Rdi];
// EXPERIMENT: failing ONLY the callback registration (with a generic kernel
// error, unlike the BT-specific unavailable code) may make wheel/FFB
// middleware disable its Bluetooth search loop instead of polling forever.
// SHARPEMU_BTHID_CB_FAIL=nf -> NOT_FOUND, =ni -> NOT_IMPLEMENTED.
var mode = Environment.GetEnvironmentVariable("SHARPEMU_BTHID_CB_FAIL");
if (string.Equals(mode, "nf", StringComparison.OrdinalIgnoreCase))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (string.Equals(mode, "ni", StringComparison.OrdinalIgnoreCase))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
}
return Result(ctx);
}
private static void FireEnumerationComplete(CpuContext ctx)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return;
}
var eventCode = ParseEnvUInt64("SHARPEMU_BTHID_EVENT_CODE", 0);
var eventSize = (int)ParseEnvUInt64("SHARPEMU_BTHID_EVENT_SIZE", 256);
if (eventSize < 1)
{
eventSize = 256;
}
// Leaked by design: the guest may retain the pointer past the callback.
var eventStruct = Marshal.AllocHGlobal(eventSize);
for (var offset = 0; offset < eventSize; offset++)
{
Marshal.WriteByte(eventStruct, offset, 0);
}
Console.Error.WriteLine(
$"[BTHID][EXPERIMENT] firing callback=0x{_callbackFunction:X} code={eventCode} size={eventSize} struct=0x{eventStruct.ToInt64():X}");
if (!scheduler.TryCallGuestFunction(
ctx,
_callbackFunction,
unchecked((ulong)eventStruct.ToInt64()),
eventCode,
0,
0,
"sceBluetoothHid synthetic enumeration event",
out var error))
{
Console.Error.WriteLine($"[BTHID][EXPERIMENT] callback failed: {error}");
}
else
{
Console.Error.WriteLine("[BTHID][EXPERIMENT] callback returned OK");
}
}
private static ulong ParseEnvUInt64(string name, ulong fallback)
{
var value = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrWhiteSpace(value))
{
return fallback;
}
value = value.Trim();
var hex = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
return ulong.TryParse(
hex ? value[2..] : value,
hex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.Integer,
null,
out var parsed) ? parsed : fallback;
}
}
+15 -10
View File
@@ -23,6 +23,11 @@ public static class PadExports
private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
// Real firmware hands out small non-negative handles; 0 is valid. Some titles
// (Monster Truck Championship) read pad state with handle 0, and rejecting it
// leaves their controller/FFB init path polling a never-valid state forever.
private static bool IsPrimaryPadHandle(int handle) => handle is 0 or PrimaryPadHandle;
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
[ThreadStatic]
@@ -104,7 +109,7 @@ public static class PadExports
public static int PadClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -117,7 +122,7 @@ public static class PadExports
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -131,7 +136,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -166,7 +171,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -207,7 +212,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -232,7 +237,7 @@ public static class PadExports
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -266,7 +271,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -310,7 +315,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -340,7 +345,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@@ -369,7 +374,7 @@ public static class PadExports
public static int PadResetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (handle != PrimaryPadHandle)
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
+28 -7
View File
@@ -418,12 +418,18 @@ public static class PlayGoExports
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
$"known=[{string.Join(',', knownChunkIds)}]");
}
// Real firmware rejects chunk ids outside the package's chunk set.
// Titles rely on this as an enumeration terminator: Monster Truck
// scans ids 0,1,2,... until BAD_CHUNK_ID, and answering OK for every
// id makes that scan wrap the ushort range and spin forever.
return OrbisPlayGoErrorBadChunkId;
}
loci[i] = PlayGoLocusLocalFast;
}
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
TracePlayGoLocus(ctx, numberOfEntries, chunkIds, outLoci);
return ctx.Memory.TryWrite(outLoci, loci)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -663,7 +669,7 @@ public static class PlayGoExports
{
lock (_stateGate)
{
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
return Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
}
}
@@ -672,7 +678,9 @@ public static class PlayGoExports
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return PlayGoMetadata.Empty;
// No app0 override to probe for sidecar files: same fully-installed
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
return new PlayGoMetadata(true, [(ushort)0]);
}
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
@@ -682,12 +690,23 @@ public static class PlayGoExports
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata)
{
// Full installs may omit PlayGo sidecar metadata.
TracePlayGo("metadata_missing; using fully-installed default chunk");
// No PlayGo sidecar: report a fully-installed single chunk. Available must
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
// init failure for UE titles); chunk 0 reports LocalFast and every other id
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
TracePlayGo("metadata_missing; fully-installed single chunk");
return new PlayGoMetadata(true, [(ushort)0]);
}
var chunkIds = LoadChunkIds(chunkDefsXml);
if (chunkIds.Length == 0)
{
// Unreadable/empty sidecar: fall back to chunk 0, not an empty set
// (which IsKnownChunkId would treat as "every id valid").
TracePlayGo("metadata_unreadable; fully-installed single chunk");
return new PlayGoMetadata(true, [(ushort)0]);
}
return new PlayGoMetadata(true, chunkIds);
}
@@ -738,7 +757,7 @@ public static class PlayGoExports
}
}
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
private static void TracePlayGoLocus(CpuContext ctx, uint entries, ulong chunkIds, ulong outLoci)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
{
@@ -748,8 +767,10 @@ public static class PlayGoExports
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
if (entries != 1 || count <= 32 || count % 1000 == 0)
{
_ = ctx.TryReadUInt16(chunkIds, out var firstChunkId);
Console.Error.WriteLine(
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
$"[LOADER][TRACE] playgo.get_locus entries={entries} first_chunk={firstChunkId} " +
$"chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
}
}
+63
View File
@@ -241,6 +241,64 @@ public static class RtcExports
LibraryName = "libSceRtc")]
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
// Diagnostic: middleware busy-wait loops typically poll sceRtcGetCurrentTick, so the
// caller's return address pinpoints the loop. SHARPEMU_RTC_PROBE_RANGE=<start>-<end>
// (hex guest addresses) dumps 0x100 bytes of code around the first matching caller,
// once, for offline disassembly. Costs nothing when the variable is unset.
private static readonly ulong[]? _rtcProbeRange = ParseRtcProbeRange();
private static int _rtcProbeDone;
private static ulong[]? ParseRtcProbeRange()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_RTC_PROBE_RANGE");
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var parts = value.Split('-', 2, StringSplitOptions.TrimEntries);
return parts.Length == 2 &&
TryParseHexAddress(parts[0], out var start) &&
TryParseHexAddress(parts[1], out var end) &&
start < end
? [start, end]
: null;
}
private static bool TryParseHexAddress(string value, out ulong address)
{
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
value = value[2..];
}
return ulong.TryParse(
value,
System.Globalization.NumberStyles.HexNumber,
null,
out address);
}
private static void ProbeRtcCaller(CpuContext ctx)
{
if (Volatile.Read(ref _rtcProbeDone) != 0 ||
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var ret) ||
ret < _rtcProbeRange![0] ||
ret >= _rtcProbeRange[1] ||
Interlocked.CompareExchange(ref _rtcProbeDone, 1, 0) != 0)
{
return;
}
var start = ret - 0x60;
Span<byte> code = stackalloc byte[0x100];
if (ctx.Memory.TryRead(start, code))
{
Console.Error.WriteLine(
$"[LOADER][DIAG] rtc.caller_code ret=0x{ret:X} @0x{start:X}: {System.Convert.ToHexString(code)}");
}
}
[SysAbiExport(
Nid = "18B2NS1y9UU",
ExportName = "sceRtcGetCurrentTick",
@@ -254,6 +312,11 @@ public static class RtcExports
return unchecked((int)0x80B50002);
}
if (_rtcProbeRange is not null)
{
ProbeRtcCaller(ctx);
}
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
@@ -15,6 +15,62 @@ public static class SystemServiceExports
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
private const int HdrToneMapLuminanceSize = sizeof(float) * 3;
private const int TitleIdFieldSize = 0x10;
private static string? _mainAppTitleId;
public static void ConfigureApplicationInfo(string? titleId)
{
_mainAppTitleId = string.IsNullOrWhiteSpace(titleId) ? null : titleId.Trim();
}
[SysAbiExport(
Nid = "3RQ5aQfnstU",
ExportName = "sceSystemServiceGetNoticeScreenSkipFlag",
Target = Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceGetNoticeScreenSkipFlag(CpuContext ctx)
{
var flagAddress = ctx[CpuRegister.Rdi];
if (flagAddress == 0)
{
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
}
// No system notice screen to skip in the emulator; report "do not skip".
Span<byte> flagBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(flagBytes, 0);
return ctx.Memory.TryWrite(flagAddress, flagBytes)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "4veE0XiIugA",
ExportName = "sceSystemServiceGetMainAppTitleId",
Target = Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceGetMainAppTitleId(CpuContext ctx)
{
var titleIdAddress = ctx[CpuRegister.Rdi];
if (titleIdAddress == 0)
{
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
}
// Title IDs are a fixed 9-char format written into a 0x10-byte field;
// bound the length so a malformed param.json cannot drive an unbounded
// stack allocation or overrun the guest buffer.
var titleId = _mainAppTitleId ?? "PPSA00000";
var length = Math.Min(titleId.Length, TitleIdFieldSize - 1);
Span<byte> titleIdBytes = stackalloc byte[TitleIdFieldSize];
titleIdBytes.Clear();
System.Text.Encoding.ASCII.GetBytes(titleId.AsSpan(0, length), titleIdBytes);
return ctx.Memory.TryWrite(titleIdAddress, titleIdBytes[..(length + 1)])
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "fZo48un7LK4",
ExportName = "sceSystemServiceParamGetInt",
@@ -4160,6 +4160,48 @@ internal static unsafe class VulkanVideoPresenter
_submitTimeline;
}
private void TransitionNewGuestImageToSampled(Image image, uint mipLevels)
{
var commandBuffer = AllocateGuestCommandBuffer();
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
};
Check(
_vk.BeginCommandBuffer(commandBuffer, &beginInfo),
"vkBeginCommandBuffer(guest image init)");
var barrier = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = 0,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.Undefined,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = ColorSubresourceRange(0, mipLevels),
};
_vk.CmdPipelineBarrier(
commandBuffer,
PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.AllCommandsBit,
0,
0,
null,
0,
null,
1,
&barrier);
Check(
_vk.EndCommandBuffer(commandBuffer),
"vkEndCommandBuffer(guest image init)");
// Same-queue submission order makes the transition visible to any
// later use of the image; no CPU-side wait is needed.
SubmitGuestCommandBuffer(commandBuffer, [], []);
}
private void EnsureGuestSubmissionCapacity()
{
CollectCompletedGuestSubmissions(waitForOldest: false);
@@ -4220,23 +4262,43 @@ internal static unsafe class VulkanVideoPresenter
return;
}
Check(result, $"vkWaitForFences(guest: {oldest.DebugName})");
if (result == Result.ErrorDeviceLost)
{
_deviceLost = true;
}
else
{
Check(result, $"vkWaitForFences(guest: {oldest.DebugName})");
}
}
while (_pendingGuestSubmissions.TryPeek(out var submission))
{
var status = _vk.GetFenceStatus(_device, submission.Fence);
if (status == Result.NotReady)
if (status == Result.NotReady && !_deviceLost)
{
break;
}
Check(status, $"vkGetFenceStatus(guest: {submission.DebugName})");
if (status == Result.ErrorDeviceLost)
{
// Pending fences never signal on a lost device; retire the
// submission anyway so teardown and back-pressure survive.
_deviceLost = true;
}
else if (status != Result.NotReady)
{
Check(status, $"vkGetFenceStatus(guest: {submission.DebugName})");
}
_pendingGuestSubmissions.Dequeue();
foreach (var image in submission.TraceImages)
if (!_deviceLost)
{
TraceGuestImageContents(image);
foreach (var image in submission.TraceImages)
{
TraceGuestImageContents(image);
}
}
foreach (var resources in submission.Resources)
@@ -4514,13 +4576,20 @@ internal static unsafe class VulkanVideoPresenter
$"queue={_activeGuestQueue.Name} submission={_activeGuestQueue.SubmissionId} " +
$"handle={work.VideoOutHandle} index={work.DisplayBufferIndex} " +
$"capture_complete={(captured ? 1 : 0)}");
#if DEBUG
System.Diagnostics.Debug.Assert(
work.Version == 0 || captured,
"An ordered wait-safe marker must execute after its flip capture.");
#endif
// Demon's Souls executes wait-safe markers before their flip capture;
// an assert here would fail-fast the process, so warn once instead.
// Dedup on a flag, not the (per-frame-unique) version, to bound growth.
if (work.Version != 0 && !captured && !_loggedFlipWaitOrderViolation)
{
_loggedFlipWaitOrderViolation = true;
Console.Error.WriteLine(
$"[LOADER][WARN] vk.flip_wait_order version={work.Version} " +
"executed before its flip capture; continuing.");
}
}
private bool _loggedFlipWaitOrderViolation;
private GuestImageResource CreateGuestFlipSnapshot(
GuestImageResource source,
long version)
@@ -8300,7 +8369,7 @@ internal static unsafe class VulkanVideoPresenter
return (properties.OptimalTilingFeatures & FormatFeatureFlags.ColorAttachmentBit) != 0;
}
private static Format GetTextureFormat(uint format, uint numberType) =>
internal static Format GetTextureFormat(uint format, uint numberType) =>
(format, numberType) switch
{
(9, _) => Format.A2B10G10R10UnormPack32,
@@ -10170,6 +10239,9 @@ internal static unsafe class VulkanVideoPresenter
_vk.AllocateMemory(_device, &allocationInfo, null, out var memory),
"vkAllocateMemory(offscreen)");
Check(_vk.BindImageMemory(_device, image, memory, 0), "vkBindImageMemory(offscreen)");
// Rendering and uploads only define the mips they touch; define the whole
// chain once so full-chain sampled binds never read Undefined layout.
TransitionNewGuestImageToSampled(image, mipLevels);
var viewInfo = new ImageViewCreateInfo
{
@@ -10937,7 +11009,7 @@ internal static unsafe class VulkanVideoPresenter
return view;
}
private static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat)
internal static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat)
{
if (imageFormat == viewFormat)
{
@@ -11003,6 +11075,23 @@ internal static unsafe class VulkanVideoPresenter
}
private void Render(double _)
{
try
{
RenderCore();
}
catch (Exception exception)
{
// Device loss can strike between any two Vulkan calls in the frame;
// keep the window loop pumping instead of tearing the presenter down.
if (!TryMarkDeviceLost(exception))
{
throw;
}
}
}
private void RenderCore()
{
if (Volatile.Read(ref _presenterCloseRequested))
{
@@ -11016,6 +11105,18 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (_deviceLost)
{
// Drain queued work so producers aren't back-pressured, then
// return without any Vulkan call (fences never signal post-loss).
while (TryTakeGuestWork(out var lostWork))
{
CompleteGuestWork(lostWork);
}
return;
}
// Reuse of a frame slot waits only on that slot's fence, keeping
// up to MaxFramesInFlight frames pipelined between CPU and GPU.
var frameSlot = _currentFrameSlot;
@@ -13836,20 +13937,38 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (result == Result.ErrorDeviceLost)
{
throw new VulkanDeviceLostException(operation);
}
throw new InvalidOperationException($"{operation} failed with {result}.");
}
private static void Check(Result result, string operation)
{
if (result == Result.ErrorDeviceLost)
{
throw new VulkanDeviceLostException(operation);
}
if (result != Result.Success)
{
throw new InvalidOperationException($"{operation} failed with {result}.");
}
}
// Typed so the frame-boundary catch can recognize device loss without
// depending on the exact wording of the exception message.
private sealed class VulkanDeviceLostException(string operation)
: InvalidOperationException($"{operation} failed with {Result.ErrorDeviceLost}.");
private bool TryMarkDeviceLost(Exception exception)
{
if (!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
// Prefer the typed signal; fall back to the message for losses that
// surface through other layers (e.g. Silk.NET bindings).
if (exception is not VulkanDeviceLostException &&
!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
{
return false;
}
@@ -1767,9 +1767,9 @@ public static class Gen5ShaderTranslator
destinations = [Gen5Operand.Vector(word & 0xFF)];
if (opcode == "VReadlaneB32")
{
// VReadlaneB32 writes to scalar destination (bits 8-14), not vector.
// Bits 0-7 are unused for this opcode.
destinations = [Gen5Operand.Scalar((word >> 8) & 0x7F)];
// The scalar destination lives in the low vdst byte (bits 0-7);
// bits 8-14 are the VOP3B carry-out sdst, which readlane lacks.
destinations = [Gen5Operand.Scalar(word & 0xFF)];
}
var isVop3B = IsVop3BOpcode((word >> 16) & 0x3FF);
control = new Gen5Vop3Control(