mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
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:
@@ -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}");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. 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. 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" />
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
public sealed class KernelMemoryCompatExportsTests
|
||||
{
|
||||
[Fact]
|
||||
public void PosixStat_MissingFileReturnsMinusOne()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong pathAddress = memoryBase + 0x100;
|
||||
const ulong statAddress = memoryBase + 0x400;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/shader.cache");
|
||||
context[CpuRegister.Rdi] = pathAddress;
|
||||
context[CpuRegister.Rsi] = statAddress;
|
||||
|
||||
var result = KernelMemoryCompatExports.PosixStat(context);
|
||||
|
||||
Assert.Equal(-1, result);
|
||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong destinationAddress = memoryBase + 0x100;
|
||||
const ulong formatAddress = memoryBase + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(formatAddress, "%.4f");
|
||||
context[CpuRegister.Rdi] = destinationAddress;
|
||||
context[CpuRegister.Rsi] = formatAddress;
|
||||
context.SetXmmRegister(
|
||||
0,
|
||||
unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)),
|
||||
0);
|
||||
|
||||
var result = KernelMemoryCompatExports.Sprintf(context);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(6UL, context[CpuRegister.Rax]);
|
||||
Span<byte> output = stackalloc byte[7];
|
||||
Assert.True(memory.TryRead(destinationAddress, output));
|
||||
Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user