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
+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);
}