mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 459ae7e3f7 | |||
| 8f405caebe | |||
| dabf723b3e | |||
| 2129a12684 | |||
| 2db1fae282 | |||
| 33f96252da | |||
| f7981a7ed7 | |||
| e10efa3ae1 | |||
| 16a2131b67 | |||
| 9883a9445d | |||
| f2d9051358 | |||
| 53d414e096 | |||
| 52d2874fa8 | |||
| c5a82c1065 | |||
| a8be1daca4 | |||
| 26dbad8ac8 | |||
| 2196a1f786 | |||
| 8db2c0622d | |||
| 53da00fa89 | |||
| b4b95014f1 | |||
| 7b91c964fc | |||
| 9bacb883f1 |
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.1</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
@@ -23,11 +23,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<strong>Join our Discord for development updates, compatibility discussions, support, and community chat.</strong>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> [!NOTE]
|
||||
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
|
||||
> can run the macOS x64 build through Rosetta 2.
|
||||
---
|
||||
|
||||
> [!NOTE]
|
||||
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
|
||||
> can run the macOS x64 build through Rosetta 2.
|
||||
|
||||
> [!WARNING]
|
||||
> SharpEmu is an experimental PS5 emulator developed from scratch in C#. The current focus is on accuracy and infrastructure setup rather than game-specific compatibility.
|
||||
@@ -60,51 +60,43 @@ Current capabilities include:
|
||||
|
||||
Some games have reached like `sceVideoOut` and AGC stages.
|
||||
|
||||
SharpEmu supports Windows, Linux, and macOS hosts. Video output uses Vulkan on
|
||||
Windows and Linux, and MoltenVK on macOS. Platform support is still experimental,
|
||||
so compatibility and performance vary by game, operating system, and GPU driver.
|
||||
|
||||
## Using
|
||||
|
||||
Download the release archive for your operating system, extract it, and launch
|
||||
SharpEmu with the path to a legally obtained game's `eboot.bin`.
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
|
||||
Tee-Object -FilePath "SharpEmu.log"
|
||||
```
|
||||
|
||||
Linux and macOS:
|
||||
|
||||
```bash
|
||||
chmod +x ./SharpEmu
|
||||
|
||||
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
|
||||
tee SharpEmu.log
|
||||
```
|
||||
|
||||
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||
release includes the MoltenVK Vulkan implementation.
|
||||
SharpEmu supports Windows, Linux, and macOS hosts. Video output uses Vulkan on
|
||||
Windows and Linux, and MoltenVK on macOS. Platform support is still experimental,
|
||||
so compatibility and performance vary by game, operating system, and GPU driver.
|
||||
|
||||
## Using
|
||||
|
||||
Download the release archive for your operating system, extract it, and launch
|
||||
SharpEmu with the path to a legally obtained game's `eboot.bin`.
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
|
||||
Tee-Object -FilePath "SharpEmu.log"
|
||||
```
|
||||
|
||||
Linux and macOS:
|
||||
|
||||
```bash
|
||||
chmod +x ./SharpEmu
|
||||
|
||||
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
|
||||
tee SharpEmu.log
|
||||
```
|
||||
|
||||
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||
release includes the MoltenVK Vulkan implementation.
|
||||
|
||||
## Games Tested
|
||||
|
||||
* **Demon's Souls Remake**
|
||||
* [Demon's Souls [PPSA01341]](https://github.com/sharpemu/sharpemu/issues/2)
|
||||
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this.
|
||||

|
||||
| Demons Souls Remake | Dreaming Sarah |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
* **Poppy Playtime Chapter 1**
|
||||
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/sharpemu/sharpemu/issues/3)
|
||||
|
||||
* **SILENT HILL: The Short Message**
|
||||
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/sharpemu/sharpemu/issues/4)
|
||||
|
||||
* **Dreaming Sarah**
|
||||
* [Dreaming Sarah [PPSA02929]](https://github.com/sharpemu/sharpemu/issues/9)
|
||||
* Real texture rendering for this game;
|
||||

|
||||
| Void Terrarium | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -114,7 +106,7 @@ release includes the MoltenVK Vulkan implementation.
|
||||
|
||||
## Build
|
||||
|
||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||
2. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
|
||||
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
||||
4. Build the project: `dotnet build` or `dotnet publish`
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 653 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -63,6 +63,11 @@ internal static partial class Program
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
if (Updater.TryApply(args, out var updateExitCode))
|
||||
{
|
||||
return updateExitCode;
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register
|
||||
/// bit-manipulation instructions.
|
||||
///
|
||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's
|
||||
/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises
|
||||
/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the
|
||||
/// register-only arithmetic so the exception handler can finish the instruction in software and
|
||||
/// resume, instead of aborting the title.
|
||||
///
|
||||
/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the
|
||||
/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the
|
||||
/// backend adapter. Each method returns the result already masked to the operand width and, where
|
||||
/// the instruction is defined to touch flags, updates <paramref name="eflags"/> in place. Flags
|
||||
/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays
|
||||
/// deterministic across hosts.
|
||||
/// </summary>
|
||||
public static class BmiInstructionEmulator
|
||||
{
|
||||
private const uint FlagCarry = 1u << 0;
|
||||
private const uint FlagZero = 1u << 6;
|
||||
private const uint FlagSign = 1u << 7;
|
||||
private const uint FlagOverflow = 1u << 11;
|
||||
|
||||
private static ulong WidthMask(GprOperandSize size) =>
|
||||
size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL;
|
||||
|
||||
private static int WidthBits(GprOperandSize size) => (int)size;
|
||||
|
||||
private static bool SignSet(ulong value, GprOperandSize size) =>
|
||||
size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0;
|
||||
|
||||
private static uint WithFlag(uint eflags, uint flag, bool set) =>
|
||||
set ? eflags | flag : eflags & ~flag;
|
||||
|
||||
// Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow
|
||||
// the result, and the caller supplies CF because each instruction defines it differently.
|
||||
private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry)
|
||||
{
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
eflags = WithFlag(eflags, FlagSign, SignSet(result, size));
|
||||
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||
return eflags;
|
||||
}
|
||||
|
||||
/// <summary>ANDN: <c>dest = (~src1) & src2</c>. CF and OF are cleared.</summary>
|
||||
public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var result = (~src1 & src2) & WidthMask(size);
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSI: isolate the lowest set bit, <c>dest = (-src) & src</c>. CF = (src != 0).</summary>
|
||||
public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((0UL - s) & s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSMSK: mask up to and including the lowest set bit, <c>dest = (src - 1) ^ src</c>. CF = (src == 0).</summary>
|
||||
public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((s - 1) ^ s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>BLSR: reset the lowest set bit, <c>dest = (src - 1) & src</c>. CF = (src == 0).</summary>
|
||||
public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var result = ((s - 1) & s) & mask;
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BEXTR: extract <c>len</c> bits of <paramref name="src"/> starting at bit <c>start</c>, where
|
||||
/// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined.
|
||||
/// </summary>
|
||||
public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var start = (int)(control & 0xFF);
|
||||
var length = (int)((control >> 8) & 0xFF);
|
||||
|
||||
ulong result;
|
||||
if (start >= bits)
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var shifted = (src & WidthMask(size)) >> start;
|
||||
if (length == 0)
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
else if (length >= bits)
|
||||
{
|
||||
result = shifted;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = shifted & ((1UL << length) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
result &= WidthMask(size);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
eflags = WithFlag(eflags, FlagCarry, false);
|
||||
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BZHI: zero the bits of <paramref name="src"/> from bit position <c>index[7:0]</c> upward.
|
||||
/// CF is set when the requested position is at or beyond the operand width.
|
||||
/// </summary>
|
||||
public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var n = (int)(index & 0xFF);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (n >= bits)
|
||||
{
|
||||
result = s;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = s & ((1UL << n) - 1);
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = ApplyLogicFlags(eflags, result, size, carry);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||
public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var s = src & WidthMask(size);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (s == 0)
|
||||
{
|
||||
result = (ulong)bits;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (ulong)(size == GprOperandSize.Bits64
|
||||
? BitOperations.TrailingZeroCount(s)
|
||||
: BitOperations.TrailingZeroCount((uint)s));
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||
public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var s = src & WidthMask(size);
|
||||
|
||||
ulong result;
|
||||
bool carry;
|
||||
if (s == 0)
|
||||
{
|
||||
result = (ulong)bits;
|
||||
carry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (ulong)(size == GprOperandSize.Bits64
|
||||
? BitOperations.LeadingZeroCount(s)
|
||||
: BitOperations.LeadingZeroCount((uint)s));
|
||||
carry = false;
|
||||
}
|
||||
|
||||
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>RORX: rotate <paramref name="src"/> right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Rorx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var rotate = count & (bits - 1);
|
||||
if (rotate == 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
return ((s >> rotate) | (s << (bits - rotate))) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SARX: arithmetic shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Sarx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var shift = count & (bits - 1);
|
||||
if (size == GprOperandSize.Bits64)
|
||||
{
|
||||
return (ulong)((long)src >> shift);
|
||||
}
|
||||
|
||||
return (ulong)(uint)((int)(uint)src >> shift) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SHLX: logical shift left by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Shlx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var shift = count & (bits - 1);
|
||||
return (src << shift) & mask;
|
||||
}
|
||||
|
||||
/// <summary>SHRX: logical shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||
public static ulong Shrx(ulong src, int count, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var mask = WidthMask(size);
|
||||
var s = src & mask;
|
||||
var shift = count & (bits - 1);
|
||||
return s >> shift;
|
||||
}
|
||||
|
||||
/// <summary>PDEP: deposit contiguous low bits of <paramref name="src"/> into the positions selected by <paramref name="mask"/>. No flags.</summary>
|
||||
public static ulong Pdep(ulong src, ulong mask, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var selector = mask & WidthMask(size);
|
||||
ulong result = 0;
|
||||
var bit = 0;
|
||||
for (var i = 0; i < bits; i++)
|
||||
{
|
||||
var position = 1UL << i;
|
||||
if ((selector & position) != 0)
|
||||
{
|
||||
if (((src >> bit) & 1UL) != 0)
|
||||
{
|
||||
result |= position;
|
||||
}
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return result & WidthMask(size);
|
||||
}
|
||||
|
||||
/// <summary>PEXT: gather the bits of <paramref name="src"/> selected by <paramref name="mask"/> into contiguous low bits. No flags.</summary>
|
||||
public static ulong Pext(ulong src, ulong mask, GprOperandSize size)
|
||||
{
|
||||
var bits = WidthBits(size);
|
||||
var selector = mask & WidthMask(size);
|
||||
ulong result = 0;
|
||||
var bit = 0;
|
||||
for (var i = 0; i < bits; i++)
|
||||
{
|
||||
if ((selector & (1UL << i)) != 0)
|
||||
{
|
||||
if (((src >> i) & 1UL) != 0)
|
||||
{
|
||||
result |= 1UL << bit;
|
||||
}
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return result & WidthMask(size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Operand width for the emulated general-purpose-register instructions. The numeric value is the
|
||||
/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result.
|
||||
/// </summary>
|
||||
public enum GprOperandSize
|
||||
{
|
||||
Bits32 = 32,
|
||||
Bits64 = 64,
|
||||
}
|
||||
@@ -128,6 +128,11 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverIllegalInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (IsBenignHostDebugException(exceptionCode))
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using Iced.Intel;
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
|
||||
//
|
||||
// Guest code runs natively, so the guest and host share the same virtual address space and the
|
||||
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
|
||||
// one of these extensions it raises #UD instead of executing the opcode; without this the title
|
||||
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
|
||||
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
|
||||
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
|
||||
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
|
||||
// does not fully model.
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
|
||||
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
|
||||
private const int CTX_EFLAGS = 68;
|
||||
|
||||
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
|
||||
private const uint StatusIllegalInstruction = 0xC000001Du;
|
||||
|
||||
private const int MaxInstructionBytes = 15;
|
||||
|
||||
// Instruction-window sizes tried in turn so a fault near a page boundary still decodes.
|
||||
private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 };
|
||||
|
||||
private static int _bmiSoftwareFallbackAnnounced;
|
||||
private static long _bmiInstructionsEmulated;
|
||||
|
||||
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Op0Kind != OpKind.Register ||
|
||||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, destOffset, result);
|
||||
if (flagsChanged)
|
||||
{
|
||||
WriteCtxU32(contextRecord, CTX_EFLAGS, eflags);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
Interlocked.Increment(ref _bmiInstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " +
|
||||
"emulating those instructions in software.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryEvaluate(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
GprOperandSize size,
|
||||
out ulong result,
|
||||
out bool flagsChanged,
|
||||
out uint eflags)
|
||||
{
|
||||
result = 0;
|
||||
flagsChanged = false;
|
||||
eflags = ReadCtxU32(contextRecord, CTX_EFLAGS);
|
||||
|
||||
switch (instruction.Mnemonic)
|
||||
{
|
||||
case Mnemonic.Andn:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Blsi:
|
||||
case Mnemonic.Blsmsk:
|
||||
case Mnemonic.Blsr:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags),
|
||||
Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags),
|
||||
_ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags),
|
||||
};
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Bextr:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Bzhi:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Tzcnt:
|
||||
case Mnemonic.Lzcnt:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic == Mnemonic.Tzcnt
|
||||
? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags)
|
||||
: BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags);
|
||||
flagsChanged = true;
|
||||
return true;
|
||||
|
||||
case Mnemonic.Rorx:
|
||||
if (instruction.Op2Kind != OpKind.Immediate8 ||
|
||||
!TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size);
|
||||
return true;
|
||||
|
||||
case Mnemonic.Sarx:
|
||||
case Mnemonic.Shlx:
|
||||
case Mnemonic.Shrx:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic switch
|
||||
{
|
||||
Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size),
|
||||
Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size),
|
||||
_ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size),
|
||||
};
|
||||
return true;
|
||||
|
||||
case Mnemonic.Pdep:
|
||||
case Mnemonic.Pext:
|
||||
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) ||
|
||||
!TryReadOperand(contextRecord, in instruction, 2, size, out var packMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = instruction.Mnemonic == Mnemonic.Pdep
|
||||
? BmiInstructionEmulator.Pdep(packSrc, packMask, size)
|
||||
: BmiInstructionEmulator.Pext(packSrc, packMask, size);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
|
||||
{
|
||||
// Try the full instruction window first, then shrink so a fault near the end of a mapped
|
||||
// page (where fewer than 15 bytes are readable) still decodes.
|
||||
foreach (var attempt in DecodeWindowSizes)
|
||||
{
|
||||
var buffer = new byte[attempt];
|
||||
if (!TryReadHostBytes(rip, buffer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
|
||||
decoder.IP = rip;
|
||||
decoder.Decode(out instruction);
|
||||
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
instruction = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe bool TryReadOperand(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
int operandIndex,
|
||||
GprOperandSize size,
|
||||
out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
switch (instruction.GetOpKind(operandIndex))
|
||||
{
|
||||
case OpKind.Register:
|
||||
if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var raw = ReadCtxU64(contextRecord, offset);
|
||||
value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL;
|
||||
return true;
|
||||
|
||||
case OpKind.Memory:
|
||||
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
|
||||
var buffer = new byte[byteCount];
|
||||
if (!TryReadHostBytes(address, buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = byteCount == 8
|
||||
? BinaryPrimitives.ReadUInt64LittleEndian(buffer)
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
|
||||
// FS/GS-relative operands need the guest segment base, which is not modelled here.
|
||||
if (instruction.SegmentPrefix != Register.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.IsIPRelativeMemoryOperand)
|
||||
{
|
||||
address = instruction.IPRelativeMemoryAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
var effective = instruction.MemoryDisplacement64;
|
||||
if (instruction.MemoryBase != Register.None)
|
||||
{
|
||||
if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
effective += ReadCtxU64(contextRecord, baseOffset);
|
||||
}
|
||||
|
||||
if (instruction.MemoryIndex != Register.None)
|
||||
{
|
||||
if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale;
|
||||
}
|
||||
|
||||
address = effective;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies.
|
||||
private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true;
|
||||
case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true;
|
||||
case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true;
|
||||
case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true;
|
||||
default:
|
||||
if (TryGetGpr64Offset(register, out offset))
|
||||
{
|
||||
size = GprOperandSize.Bits64;
|
||||
return true;
|
||||
}
|
||||
|
||||
size = GprOperandSize.Bits32;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetGpr64Offset(Register register, out int offset)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.RAX: offset = CTX_RAX; return true;
|
||||
case Register.RCX: offset = CTX_RCX; return true;
|
||||
case Register.RDX: offset = CTX_RDX; return true;
|
||||
case Register.RBX: offset = CTX_RBX; return true;
|
||||
case Register.RSP: offset = CTX_RSP; return true;
|
||||
case Register.RBP: offset = CTX_RBP; return true;
|
||||
case Register.RSI: offset = CTX_RSI; return true;
|
||||
case Register.RDI: offset = CTX_RDI; return true;
|
||||
case Register.R8: offset = CTX_R8; return true;
|
||||
case Register.R9: offset = CTX_R9; return true;
|
||||
case Register.R10: offset = CTX_R10; return true;
|
||||
case Register.R11: offset = CTX_R11; return true;
|
||||
case Register.R12: offset = CTX_R12; return true;
|
||||
case Register.R13: offset = CTX_R13; return true;
|
||||
case Register.R14: offset = CTX_R14; return true;
|
||||
case Register.R15: offset = CTX_R15; return true;
|
||||
default: offset = 0; return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,7 +598,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
*(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub);
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
if (_logFiber)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " +
|
||||
@@ -1292,7 +1292,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
int returnValue;
|
||||
if (IsNoBlockLeafImport(importStubEntry.Nid))
|
||||
if (importStubEntry.IsNoBlockLeaf)
|
||||
{
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
returnValue = export.Function(cpuContext);
|
||||
|
||||
@@ -48,6 +48,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
// hashing are hoisted to stub-setup time.
|
||||
public bool IsLeaf { get; }
|
||||
|
||||
public bool IsNoBlockLeaf { get; }
|
||||
|
||||
public bool SuppressStrlenTrace { get; }
|
||||
|
||||
public bool IsLoopGuardBoundary { get; }
|
||||
@@ -59,6 +61,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
string nid,
|
||||
ExportedFunction? export,
|
||||
bool isLeaf,
|
||||
bool isNoBlockLeaf,
|
||||
bool suppressStrlenTrace,
|
||||
bool isLoopGuardBoundary,
|
||||
ulong nidHash)
|
||||
@@ -67,6 +70,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
Nid = nid;
|
||||
Export = export;
|
||||
IsLeaf = isLeaf;
|
||||
IsNoBlockLeaf = isNoBlockLeaf;
|
||||
SuppressStrlenTrace = suppressStrlenTrace;
|
||||
IsLoopGuardBoundary = isLoopGuardBoundary;
|
||||
NidHash = nidHash;
|
||||
@@ -318,6 +322,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private bool _logUsleep;
|
||||
|
||||
private bool _logFiber;
|
||||
|
||||
private bool _logBootstrap;
|
||||
|
||||
private bool _logAllImports;
|
||||
@@ -1067,6 +1073,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_ignoredGuestInt41Count = 0;
|
||||
_logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
|
||||
_logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
|
||||
_logFiber = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal);
|
||||
_logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
|
||||
_logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal);
|
||||
_logImportFrames = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FRAMES"), "1", StringComparison.Ordinal);
|
||||
@@ -1164,6 +1171,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
text2,
|
||||
resolvedExport,
|
||||
IsLeafImport(text2),
|
||||
IsNoBlockLeafImport(text2),
|
||||
ShouldSuppressStrlenTrace(text2),
|
||||
IsImportLoopGuardBoundary(text2),
|
||||
StableHash64(text2));
|
||||
@@ -1760,14 +1768,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
frameAddress = 0;
|
||||
transferStub = 0;
|
||||
error = null;
|
||||
if (target.Rip < 65536 || target.Rsp == 0)
|
||||
if (ActiveCpuContext is not { } activeContext)
|
||||
{
|
||||
error = $"invalid guest context transfer target rip=0x{target.Rip:X16} rsp=0x{target.Rsp:X16}";
|
||||
error = "guest context transfer without an active CPU context";
|
||||
return false;
|
||||
}
|
||||
if (target.Rsp < sizeof(ulong) ||
|
||||
ActiveCpuContext is not { } activeContext ||
|
||||
!activeContext.TryWriteUInt64(target.Rsp - sizeof(ulong), target.Rip))
|
||||
if (!TryValidateGuestContextTransferTarget(activeContext.Memory, target, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!activeContext.TryWriteUInt64(target.Rsp - sizeof(ulong), target.Rip))
|
||||
{
|
||||
error = $"guest context transfer slot is not writable at 0x{target.Rsp - sizeof(ulong):X16}";
|
||||
return false;
|
||||
@@ -1811,6 +1821,30 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryValidateGuestContextTransferTarget(
|
||||
ICpuMemory memory,
|
||||
in GuestCpuContinuation target,
|
||||
out string? error)
|
||||
{
|
||||
if (target.Rip < 65536 || target.Rsp < sizeof(ulong))
|
||||
{
|
||||
error = $"invalid guest context transfer target rip=0x{target.Rip:X16} rsp=0x{target.Rsp:X16}";
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> ripProbe = stackalloc byte[1];
|
||||
if (!memory.TryRead(target.Rip, ripProbe))
|
||||
{
|
||||
error =
|
||||
$"guest context transfer target rip=0x{target.Rip:X16} is not mapped guest memory " +
|
||||
$"(rsp=0x{target.Rsp:X16})";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe nint GetOrCreateGuestContextTransferStub()
|
||||
{
|
||||
if (Volatile.Read(ref _guestContextTransferStub) != 0)
|
||||
@@ -3264,31 +3298,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
Pump(callerContext, reason);
|
||||
|
||||
var threads = SnapshotGuestThreads();
|
||||
if (threads.Length == 0)
|
||||
// Tally run states under the lock without allocating a snapshot every
|
||||
// spin (this loop can iterate rapidly); the full snapshot is only
|
||||
// materialized for the gated diagnostic dump below.
|
||||
GetGuestThreadActivity(out var threadCount, out var hasReadyThread, out var hasRunningThread, out var hasBlockedThread);
|
||||
if (threadCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hasReadyThread = false;
|
||||
var hasRunningThread = false;
|
||||
var hasBlockedThread = false;
|
||||
foreach (var thread in threads)
|
||||
{
|
||||
switch (thread.State)
|
||||
{
|
||||
case GuestThreadRunState.Ready:
|
||||
hasReadyThread = true;
|
||||
break;
|
||||
case GuestThreadRunState.Running:
|
||||
hasRunningThread = true;
|
||||
break;
|
||||
case GuestThreadRunState.Blocked:
|
||||
hasBlockedThread = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReadyThread)
|
||||
{
|
||||
continue;
|
||||
@@ -3301,7 +3319,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
if (_logGuestThreads && Stopwatch.GetTimestamp() >= nextSnapshotTimestamp)
|
||||
{
|
||||
foreach (var thread in threads)
|
||||
foreach (var thread in SnapshotGuestThreads())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] guest_thread.idle_wait reason={reason} handle=0x{thread.ThreadHandle:X16} " +
|
||||
@@ -3321,7 +3339,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
using (LockGate("SnapshotGuestThreads"))
|
||||
{
|
||||
return _guestThreads.Values.ToArray();
|
||||
var snapshot = new GuestThreadState[_guestThreads.Count];
|
||||
_guestThreads.Values.CopyTo(snapshot, 0);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocation-free run-state tally for the idle spin loop.
|
||||
private void GetGuestThreadActivity(out int count, out bool hasReady, out bool hasRunning, out bool hasBlocked)
|
||||
{
|
||||
hasReady = false;
|
||||
hasRunning = false;
|
||||
hasBlocked = false;
|
||||
using (LockGate("GetGuestThreadActivity"))
|
||||
{
|
||||
count = _guestThreads.Count;
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
switch (thread.State)
|
||||
{
|
||||
case GuestThreadRunState.Ready:
|
||||
hasReady = true;
|
||||
break;
|
||||
case GuestThreadRunState.Running:
|
||||
hasRunning = true;
|
||||
break;
|
||||
case GuestThreadRunState.Blocked:
|
||||
hasBlocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -23,9 +23,6 @@ public sealed class GuiSettings
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>GPU implementation selected for newly launched games.</summary>
|
||||
public string RenderingBackend { get; set; } = "Legacy";
|
||||
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
@@ -51,6 +48,8 @@ public sealed class GuiSettings
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
||||
|
||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||
public List<string> EnvironmentToggles { get; set; } = new();
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -43,12 +45,6 @@
|
||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.RenderingBackend.Label": "Rendering backend",
|
||||
"Options.RenderingBackend.Desc": "Graphics implementation used for newly launched games. Native Vulkan requires a separate library.",
|
||||
"Options.RenderingBackend.Native": "Native Vulkan (Experimental)",
|
||||
"Options.RenderingBackend.Legacy": "Silk.NET Vulkan (Default)",
|
||||
"Launch.NativeBackendMissing": "WARNING: Native Vulkan is selected, but {0} was not found next to SharpEmu. Download it from https://github.com/sharpemu/sharpemu.vulkan or select Silk.NET Vulkan in Options.",
|
||||
|
||||
"Options.Strict.Label": "Strict dynlib resolution",
|
||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||
|
||||
@@ -145,8 +141,25 @@
|
||||
"Options.About" : "About",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Source code, issues and project development.",
|
||||
"About.Github.LatestCommitLabel": "Latest Commit",
|
||||
"About.Github.LatestCommitDescription": "Latest commit on the main branch",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||
"About.GithubButton": "Contribute in GitHub!",
|
||||
"About.DiscordButton": "Join our Discord!"
|
||||
"About.DiscordButton": "Join our Discord!",
|
||||
|
||||
"Updater.Auto.Label": "Check for updates on startup",
|
||||
"Updater.Auto.Desc": "Checks GitHub without delaying startup.",
|
||||
"Updater.Label": "Updates",
|
||||
"Updater.Check": "Check for updates",
|
||||
"Updater.DownloadRestart": "Download and restart",
|
||||
"Updater.Status.Ready": "Current build: {0}",
|
||||
"Updater.Status.Checking": "Checking for updates…",
|
||||
"Updater.Status.Current": "You are up to date ({0}).",
|
||||
"Updater.Status.Available": "A new build is available: {0}",
|
||||
"Updater.Status.Downloading": "Downloading update… {0}%",
|
||||
"Updater.Status.Installing": "Installing update…",
|
||||
"Updater.Status.Timeout": "Update check timed out after 10 seconds.",
|
||||
"Updater.Status.Failed": "Could not check for updates.",
|
||||
"Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build."
|
||||
}
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Github.LatestCommitLabel": "Último Commit",
|
||||
"About.Github.LatestCommitDescription": "Último commit en la rama main",
|
||||
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||
"About.GithubButton": "Contribuye en GitHub!",
|
||||
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||
|
||||
@@ -125,5 +125,20 @@
|
||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||
"Dialog.LogFiles": "Günlük Dosyaları",
|
||||
|
||||
"Updater.Auto.Label": "Açılışta güncellemeleri denetle",
|
||||
"Updater.Auto.Desc": "Açılışı geciktirmeden GitHub'ı denetler.",
|
||||
"Updater.Label": "Güncellemeler",
|
||||
"Updater.Check": "Güncellemeleri denetle",
|
||||
"Updater.DownloadRestart": "İndir ve yeniden başlat",
|
||||
"Updater.Status.Ready": "Mevcut build: {0}",
|
||||
"Updater.Status.Checking": "Güncellemeler denetleniyor…",
|
||||
"Updater.Status.Current": "Güncelsiniz ({0}).",
|
||||
"Updater.Status.Available": "Yeni build mevcut: {0}",
|
||||
"Updater.Status.Downloading": "Güncelleme indiriliyor… %{0}",
|
||||
"Updater.Status.Installing": "Güncelleme kuruluyor…",
|
||||
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
|
||||
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
|
||||
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
|
||||
}
|
||||
|
||||
@@ -203,19 +203,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="RenderingBackendLabel" Text="Rendering backend" FontSize="13" />
|
||||
<TextBlock x:Name="RenderingBackendDesc" Text="Graphics implementation used for newly launched games."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="RenderingBackendBox" Width="200" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="RenderingBackendLegacyItem" Content="Silk.NET Vulkan (Default)" />
|
||||
<ComboBoxItem x:Name="RenderingBackendNativeItem" Content="Native Vulkan (Experimental)" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||
@@ -328,6 +315,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="AutoUpdateLabel" Text="Check for updates on startup" FontSize="13" />
|
||||
<TextBlock x:Name="AutoUpdateDesc" Text="Checks GitHub without delaying startup."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
|
||||
IsChecked="True" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="card">
|
||||
@@ -335,6 +332,40 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TextBlock x:Name="AboutSectionTitle"
|
||||
Classes="sectionTitle"
|
||||
Text="ABOUT" />
|
||||
|
||||
<!--Latest commit info-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||
Width="24" Height="24" VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
|
||||
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1" x:Name="LatestCommitHashText" Classes="ghost"
|
||||
Content="Loading…" FontSize="12" FontFamily="Consolas,monospace"
|
||||
VerticalAlignment="Center" IsEnabled="False" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<!--Update-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/update-icon.png"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
||||
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
||||
Content="Check for updates" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!--Github-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
@@ -433,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_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" />
|
||||
@@ -466,6 +508,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" />
|
||||
|
||||
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
@@ -57,6 +58,9 @@ public partial class MainWindow : Window
|
||||
private bool _isRunning;
|
||||
private int _autoScrollTicks;
|
||||
private int _activePageIndex;
|
||||
private Updater.UpdateInfo? _availableUpdate;
|
||||
private string _updateStatusKey = "Updater.Status.Ready";
|
||||
private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"];
|
||||
|
||||
// Discord Rich Presence state.
|
||||
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
@@ -75,6 +79,10 @@ public partial class MainWindow : Window
|
||||
private long _navUpNextAt;
|
||||
private long _navDownNextAt;
|
||||
|
||||
//Github http client for latest commit
|
||||
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
|
||||
private string? _latestCommitSha;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -116,8 +124,6 @@ public partial class MainWindow : Window
|
||||
// The settings page edits _settings live, so a launch started while
|
||||
// it is open already uses the new values.
|
||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||
RenderingBackendBox.SelectionChanged += (_, _) =>
|
||||
_settings.RenderingBackend = SelectedRenderingBackend();
|
||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||
@@ -133,17 +139,24 @@ public partial class MainWindow : Window
|
||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||
UpdateDiscordPresence();
|
||||
};
|
||||
AutoUpdateToggle.IsCheckedChanged += (_, _) =>
|
||||
_settings.CheckForUpdatesOnStartup = AutoUpdateToggle.IsChecked == true;
|
||||
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
|
||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||
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();
|
||||
@@ -187,6 +200,21 @@ public partial class MainWindow : Window
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
|
||||
LatestCommitHashText.Click += (_, _) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_latestCommitSha))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName =
|
||||
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,6 +256,91 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Github http client config ----
|
||||
// This is for getting lash commit id
|
||||
private static HttpClient CreateGithubHttpClient()
|
||||
{
|
||||
var client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(15)
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("SharpEmu/1.0");
|
||||
client.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/vnd.github.sha"));
|
||||
|
||||
client.DefaultRequestHeaders.Add(
|
||||
"X-GitHub-Api-Version",
|
||||
"2026-03-10");
|
||||
|
||||
return client;
|
||||
}
|
||||
private async Task LoadLatestCommitAsync()
|
||||
{
|
||||
const string apiUrl =
|
||||
"https://api.github.com/repos/sharpemu/sharpemu/commits/main";
|
||||
|
||||
_latestCommitSha = null;
|
||||
LatestCommitHashText.Content = "Loading…";
|
||||
LatestCommitHashText.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await GithubHttpClient.GetAsync(apiUrl);
|
||||
var responseBody =
|
||||
(await response.Content.ReadAsStringAsync()).Trim();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
LatestCommitHashText.Content =
|
||||
$"HTTP {(int)response.StatusCode}";
|
||||
|
||||
ToolTip.SetTip(
|
||||
LatestCommitHashText,
|
||||
string.IsNullOrWhiteSpace(responseBody)
|
||||
? response.ReasonPhrase
|
||||
: responseBody);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseBody.Length < 7)
|
||||
{
|
||||
LatestCommitHashText.Content = "Invalid response";
|
||||
ToolTip.SetTip(LatestCommitHashText, responseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the complete SHA for the URL.
|
||||
_latestCommitSha = responseBody;
|
||||
|
||||
// Display only the short SHA.
|
||||
LatestCommitHashText.Content =
|
||||
responseBody[..Math.Min(7, responseBody.Length)];
|
||||
|
||||
LatestCommitHashText.IsEnabled = true;
|
||||
|
||||
ToolTip.SetTip(
|
||||
LatestCommitHashText,
|
||||
$"Open commit {_latestCommitSha}");
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
LatestCommitHashText.Content = "Timeout";
|
||||
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
LatestCommitHashText.Content = "Connection error";
|
||||
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LatestCommitHashText.Content = "Error";
|
||||
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Controller navigation ----
|
||||
|
||||
private void PollGamepad()
|
||||
@@ -373,6 +486,12 @@ public partial class MainWindow : Window
|
||||
ApplySettingsToControls();
|
||||
LocateEmulator();
|
||||
UpdateDiscordPresence();
|
||||
_ = LoadLatestCommitAsync();
|
||||
|
||||
if (_settings.CheckForUpdatesOnStartup)
|
||||
{
|
||||
_ = CheckForUpdatesAsync();
|
||||
}
|
||||
await RescanLibraryAsync();
|
||||
}
|
||||
|
||||
@@ -429,9 +548,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");
|
||||
@@ -441,11 +562,6 @@ public partial class MainWindow : Window
|
||||
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
||||
|
||||
RenderingBackendLabel.Text = loc.Get("Options.RenderingBackend.Label");
|
||||
RenderingBackendDesc.Text = loc.Get("Options.RenderingBackend.Desc");
|
||||
RenderingBackendNativeItem.Content = loc.Get("Options.RenderingBackend.Native");
|
||||
RenderingBackendLegacyItem.Content = loc.Get("Options.RenderingBackend.Legacy");
|
||||
|
||||
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||
|
||||
@@ -479,8 +595,10 @@ public partial class MainWindow : Window
|
||||
|
||||
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
||||
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
||||
AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
|
||||
AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
|
||||
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle })
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
|
||||
{
|
||||
toggle.OnContent = loc.Get("Common.On");
|
||||
toggle.OffContent = loc.Get("Common.Off");
|
||||
@@ -504,6 +622,10 @@ public partial class MainWindow : Window
|
||||
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
||||
GithubButton.Content = loc.Get("About.GithubButton");
|
||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||
UpdateLabel.Text = loc.Get("Updater.Label");
|
||||
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
|
||||
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
|
||||
RefreshUpdateText();
|
||||
|
||||
UpdateEmptyStateTexts();
|
||||
UpdateSelectedGameTexts();
|
||||
@@ -603,10 +725,6 @@ public partial class MainWindow : Window
|
||||
|
||||
private void ApplySettingsToControls()
|
||||
{
|
||||
RenderingBackendBox.SelectedIndex = string.Equals(
|
||||
_settings.RenderingBackend,
|
||||
"Native",
|
||||
StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||
{
|
||||
"trace" => 0,
|
||||
@@ -623,15 +741,87 @@ public partial class MainWindow : Window
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
|
||||
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();
|
||||
}
|
||||
|
||||
private async Task OnUpdateButtonAsync()
|
||||
{
|
||||
if (_availableUpdate is null)
|
||||
{
|
||||
await CheckForUpdatesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
var progress = new Progress<int>(value =>
|
||||
SetUpdateStatus("Updater.Status.Downloading", value));
|
||||
await Updater.DownloadAndRestartAsync(_availableUpdate, progress);
|
||||
SetUpdateStatus("Updater.Status.Installing");
|
||||
Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Failed");
|
||||
UpdateButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
_availableUpdate = null;
|
||||
UpdateButton.IsEnabled = false;
|
||||
SetUpdateStatus("Updater.Status.Checking");
|
||||
try
|
||||
{
|
||||
_availableUpdate = await Updater.CheckAsync(BuildInfo.CommitSha);
|
||||
SetUpdateStatus(
|
||||
_availableUpdate is null ? "Updater.Status.Current" : "Updater.Status.Available",
|
||||
_availableUpdate?.Sha ?? BuildInfo.CommitSha ?? "dev");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Timeout");
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Unsupported");
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateButton.IsEnabled = true;
|
||||
RefreshUpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUpdateStatus(string key, params object?[] args)
|
||||
{
|
||||
_updateStatusKey = key;
|
||||
_updateStatusArgs = args;
|
||||
RefreshUpdateText();
|
||||
}
|
||||
|
||||
private void RefreshUpdateText()
|
||||
{
|
||||
UpdateStatusText.Text = Localization.Instance.Format(_updateStatusKey, _updateStatusArgs);
|
||||
UpdateButton.Content = Localization.Instance.Get(
|
||||
_availableUpdate is null ? "Updater.Check" : "Updater.DownloadRestart");
|
||||
}
|
||||
|
||||
// Environment variables set on this process at the previous launch; children
|
||||
// inherit the process environment, so stale names must be cleared explicitly.
|
||||
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -665,9 +855,6 @@ public partial class MainWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
private string SelectedRenderingBackend() =>
|
||||
RenderingBackendBox.SelectedIndex == 1 ? "Native" : "Legacy";
|
||||
|
||||
private void UpdateLogFilePathText()
|
||||
{
|
||||
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||
@@ -1487,31 +1674,6 @@ public partial class MainWindow : Window
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
// The GUI owns this setting when it launches the emulator. Set both
|
||||
// choices explicitly so a SHARPEMU_GPU_BACKEND value inherited by the
|
||||
// launcher cannot silently override the Options menu.
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_GPU_BACKEND",
|
||||
string.Equals(_settings.RenderingBackend, "Legacy", StringComparison.OrdinalIgnoreCase)
|
||||
? "legacy"
|
||||
: "native");
|
||||
|
||||
if (string.Equals(_settings.RenderingBackend, "Native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var nativeLibraryName = OperatingSystem.IsWindows()
|
||||
? "sharpemu_gpu_vulkan.dll"
|
||||
: OperatingSystem.IsMacOS()
|
||||
? "libsharpemu_gpu_vulkan.dylib"
|
||||
: "libsharpemu_gpu_vulkan.so";
|
||||
var emulatorDirectory = Path.GetDirectoryName(_emulatorExePath) ?? AppContext.BaseDirectory;
|
||||
if (!File.Exists(Path.Combine(emulatorDirectory, nativeLibraryName)))
|
||||
{
|
||||
AppendConsoleLine(
|
||||
Localization.Instance.Format("Launch.NativeBackendMissing", nativeLibraryName),
|
||||
WarningLineBrush);
|
||||
}
|
||||
}
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||
|
||||
@@ -32,6 +32,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>Self-contained Windows updater; the emulator layers do not depend on it.</summary>
|
||||
public static class Updater
|
||||
{
|
||||
private const string ApplyArgument = "--sharpemu-apply-update";
|
||||
private const string LatestReleaseUrl = "https://api.github.com/repos/sharpemu/sharpemu/releases/latest";
|
||||
private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10);
|
||||
private static readonly HttpClient Http = CreateHttpClient();
|
||||
|
||||
public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size);
|
||||
|
||||
public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var platform = CurrentPlatform();
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(CheckTimeout);
|
||||
|
||||
using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return ParseRelease(
|
||||
await response.Content.ReadAsStringAsync(timeout.Token),
|
||||
currentSha,
|
||||
platform.Rid,
|
||||
platform.Extension);
|
||||
}
|
||||
|
||||
public static async Task DownloadAndRestartAsync(
|
||||
UpdateInfo update,
|
||||
IProgress<int>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "SharpEmu.Update");
|
||||
var payload = Path.Combine(root, "payload");
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(root);
|
||||
var archive = Path.Combine(root, update.Name);
|
||||
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
await using var output = File.Create(archive);
|
||||
var buffer = new byte[81920];
|
||||
long written = 0;
|
||||
int read;
|
||||
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||
written += read;
|
||||
progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
|
||||
}
|
||||
|
||||
if (written != update.Size)
|
||||
{
|
||||
throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
|
||||
}
|
||||
}
|
||||
|
||||
var platform = CurrentPlatform();
|
||||
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
|
||||
|
||||
var start = new ProcessStartInfo(stagedExe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = payload,
|
||||
};
|
||||
start.ArgumentList.Add(ApplyArgument);
|
||||
start.ArgumentList.Add(Environment.ProcessId.ToString());
|
||||
start.ArgumentList.Add(AppContext.BaseDirectory);
|
||||
using var helper = Process.Start(start)
|
||||
?? throw new InvalidOperationException("The update installer could not be started.");
|
||||
}
|
||||
|
||||
/// <summary>Runs from the downloaded executable after the old GUI exits.</summary>
|
||||
public static bool TryApply(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = 0;
|
||||
if (args.Length != 3 || args[0] != ApplyArgument)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (int.TryParse(args[1], out var oldPid))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Process.GetProcessById(oldPid).WaitForExit(30_000))
|
||||
{
|
||||
throw new TimeoutException("SharpEmu did not close within 30 seconds.");
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// The old process has already exited.
|
||||
}
|
||||
}
|
||||
|
||||
var source = AppContext.BaseDirectory;
|
||||
var target = Path.GetFullPath(args[2]);
|
||||
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relative = Path.GetRelativePath(source, file);
|
||||
if (relative.Equals("gui-settings.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("user" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("logs" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("Languages" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destination = Path.Combine(target, relative);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
File.Copy(file, destination, overwrite: true);
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(destination, File.GetUnixFileMode(file));
|
||||
}
|
||||
}
|
||||
|
||||
using var restarted = Process.Start(new ProcessStartInfo(
|
||||
Path.Combine(target, CurrentPlatform().ExecutableName))
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = target,
|
||||
}) ?? throw new InvalidOperationException("The updated SharpEmu could not be started.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exitCode = 1;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort diagnostics only.
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static UpdateInfo? ParseRelease(
|
||||
string json,
|
||||
string? currentSha,
|
||||
string rid,
|
||||
string extension)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>();
|
||||
foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray())
|
||||
{
|
||||
var name = asset.GetProperty("name").GetString() ?? "";
|
||||
var marker = $"-{rid}-";
|
||||
var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ||
|
||||
markerIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sha = name[(markerIndex + marker.Length)..^extension.Length];
|
||||
if (sha.Length < 7 || !sha.All(Uri.IsHexDigit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add((
|
||||
asset.GetProperty("created_at").GetDateTimeOffset(),
|
||||
new UpdateInfo(
|
||||
sha,
|
||||
name,
|
||||
asset.GetProperty("browser_download_url").GetString()!,
|
||||
asset.GetProperty("size").GetInt64())));
|
||||
}
|
||||
|
||||
var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update;
|
||||
return latest is null || string.Equals(latest.Sha, currentSha, StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: latest;
|
||||
}
|
||||
|
||||
private static string ExtractArchive(
|
||||
string archive,
|
||||
string payload,
|
||||
string extension,
|
||||
string executableName)
|
||||
{
|
||||
if (extension == ".zip")
|
||||
{
|
||||
ZipFile.ExtractToDirectory(archive, payload);
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(payload);
|
||||
using var compressed = File.OpenRead(archive);
|
||||
using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
|
||||
TarFile.ExtractToDirectory(gzip, payload, overwriteFiles: false);
|
||||
}
|
||||
|
||||
var executable = Path.Combine(payload, executableName);
|
||||
if (!File.Exists(executable))
|
||||
{
|
||||
throw new InvalidDataException($"The update archive does not contain {executableName}.");
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(executable, File.GetUnixFileMode(executable) | UnixFileMode.UserExecute);
|
||||
}
|
||||
|
||||
return executable;
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient()
|
||||
{
|
||||
var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SharpEmu", "0.0.1"));
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||
return client;
|
||||
}
|
||||
|
||||
private static PlatformInfo CurrentPlatform()
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
throw new PlatformNotSupportedException("SharpEmu releases require an x64 process.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows()) return new("win-x64", ".zip", "SharpEmu.exe");
|
||||
if (OperatingSystem.IsLinux()) return new("linux-x64", ".tar.gz", "SharpEmu");
|
||||
if (OperatingSystem.IsMacOS()) return new("osx-x64", ".tar.gz", "SharpEmu");
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName);
|
||||
}
|
||||
@@ -163,13 +163,18 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
private const int PROT_EXEC = 0x4;
|
||||
|
||||
private const int MAP_PRIVATE = 0x02;
|
||||
private const int MAP_FIXED = 0x10;
|
||||
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
||||
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
||||
|
||||
// Linux-only: fail instead of clobbering an existing mapping.
|
||||
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
||||
|
||||
private const int KERN_SUCCESS = 0;
|
||||
|
||||
// On Darwin fixed placement is represented by the absence of
|
||||
// VM_FLAGS_ANYWHERE. Do not add VM_FLAGS_OVERWRITE: overlap must fail.
|
||||
private const int VM_FLAGS_FIXED = 0;
|
||||
|
||||
private static readonly nint MAP_FAILED = -1;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
@@ -181,6 +186,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
public ulong Size;
|
||||
public uint DefaultProtect;
|
||||
public Dictionary<ulong, uint>? PageProtects;
|
||||
public bool UsesMachAllocation;
|
||||
|
||||
public ulong End => Base + Size;
|
||||
|
||||
@@ -251,6 +257,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
}
|
||||
|
||||
nint result;
|
||||
var usesMachAllocation = false;
|
||||
if (address != null)
|
||||
{
|
||||
// Win32 maps at exactly the requested address or fails
|
||||
@@ -280,15 +287,17 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
if (result == MAP_FAILED && OperatingSystem.IsMacOS())
|
||||
{
|
||||
// The hint-only attempt above didn't land at the requested
|
||||
// address. This is routinely the case for the PS5's fixed
|
||||
// 32 GiB image base under Rosetta 2 on Apple Silicon, where
|
||||
// the kernel never honors that hint. Retry with true
|
||||
// MAP_FIXED: this can clobber an untracked host mapping
|
||||
// (dyld, the runtime's JIT heap, Rosetta) if one already
|
||||
// sits exactly there, but without it guest images that
|
||||
// require this base never load at all on macOS.
|
||||
Trace($"exact mmap hint failed, retrying with MAP_FIXED: addr=0x{(ulong)address:X16}");
|
||||
result = mmap((nint)address, (nuint)alignedSize, posixProtect, flags | MAP_FIXED, -1, 0);
|
||||
// address. mach_vm_allocate with fixed placement and no
|
||||
// overwrite flag atomically maps the requested range or
|
||||
// fails if any host mapping already owns it. Unlike
|
||||
// MAP_FIXED, it cannot clobber CLR, dyld, JIT, or Rosetta
|
||||
// memory that is absent from our shadow table.
|
||||
Trace($"exact mmap hint failed, retrying with fixed Mach allocation: addr=0x{(ulong)address:X16}");
|
||||
result = AllocateDarwinFixed(
|
||||
(nint)address,
|
||||
alignedSize,
|
||||
posixProtect);
|
||||
usesMachAllocation = result != MAP_FAILED;
|
||||
}
|
||||
|
||||
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||
@@ -316,7 +325,8 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
{
|
||||
Base = (ulong)result,
|
||||
Size = alignedSize,
|
||||
DefaultProtect = protect
|
||||
DefaultProtect = protect,
|
||||
UsesMachAllocation = usesMachAllocation,
|
||||
};
|
||||
|
||||
return (void*)result;
|
||||
@@ -335,8 +345,15 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
return false;
|
||||
}
|
||||
|
||||
Regions.Remove((ulong)address);
|
||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||
var released = region.UsesMachAllocation
|
||||
? mach_vm_deallocate(mach_task_self(), (ulong)address, region.Size) == KERN_SUCCESS
|
||||
: munmap((nint)address, (nuint)region.Size) == 0;
|
||||
if (released)
|
||||
{
|
||||
Regions.Remove((ulong)address);
|
||||
}
|
||||
|
||||
return released;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +521,40 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
};
|
||||
}
|
||||
|
||||
private static nint AllocateDarwinFixed(nint requestedAddress, ulong size, int posixProtect)
|
||||
{
|
||||
var address = (ulong)requestedAddress;
|
||||
var result = mach_vm_allocate(
|
||||
mach_task_self(),
|
||||
ref address,
|
||||
size,
|
||||
VM_FLAGS_FIXED);
|
||||
if (result != KERN_SUCCESS || address != (ulong)requestedAddress)
|
||||
{
|
||||
if (result == KERN_SUCCESS)
|
||||
{
|
||||
_ = mach_vm_deallocate(mach_task_self(), address, size);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"fixed Mach allocation failed: addr=0x{(ulong)requestedAddress:X16} " +
|
||||
$"size=0x{size:X} kern_return={result}");
|
||||
return MAP_FAILED;
|
||||
}
|
||||
|
||||
if (mprotect((nint)address, (nuint)size, posixProtect) == 0)
|
||||
{
|
||||
return (nint)address;
|
||||
}
|
||||
|
||||
var error = Marshal.GetLastPInvokeError();
|
||||
_ = mach_vm_deallocate(mach_task_self(), address, size);
|
||||
Trace(
|
||||
$"fixed Mach allocation protection failed: addr=0x{address:X16} " +
|
||||
$"size=0x{size:X} errno={error}");
|
||||
return MAP_FAILED;
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||
@@ -524,5 +575,14 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int mprotect(nint addr, nuint length, int prot);
|
||||
|
||||
[DllImport("libSystem.B.dylib")]
|
||||
private static extern uint mach_task_self();
|
||||
|
||||
[DllImport("libSystem.B.dylib")]
|
||||
private static extern int mach_vm_allocate(uint target, ref ulong address, ulong size, int flags);
|
||||
|
||||
[DllImport("libSystem.B.dylib")]
|
||||
private static extern int mach_vm_deallocate(uint target, ulong address, ulong size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Core" />
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -121,6 +121,7 @@ public static partial class AgcExports
|
||||
private const uint CbBlend0Control = 0x1E0;
|
||||
private const uint PaScModeCntl0 = 0x292;
|
||||
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
||||
private const uint DbRenderControl = 0x000;
|
||||
private const uint DbDepthView = 0x002;
|
||||
private const uint DbDepthSizeXy = 0x007;
|
||||
private const uint DbDepthClear = 0x00B;
|
||||
@@ -5291,7 +5292,7 @@ public static partial class AgcExports
|
||||
var hasDepthOnlyCandidate = hasExportShader &&
|
||||
!hasPixelShader &&
|
||||
depthTarget is not null &&
|
||||
(depthState.TestEnable || depthState.WriteEnable);
|
||||
(depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
|
||||
if (hasDepthOnlyCandidate &&
|
||||
TryCreateTranslatedDepthOnlyGuestDraw(
|
||||
ctx,
|
||||
@@ -5937,16 +5938,30 @@ public static partial class AgcExports
|
||||
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
||||
// Fall back to slot 0 if we cannot match any export to a bound target.
|
||||
var allBoundTargets = GetRenderTargets(state.CxRegisters);
|
||||
var renderTargets = allBoundTargets
|
||||
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||
.OrderBy(target => target.Slot)
|
||||
.ToArray();
|
||||
if (renderTargets.Length == 0)
|
||||
// At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
|
||||
// closure allocations. Slots are distinct, so sorting by slot is stable.
|
||||
var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
|
||||
foreach (var target in allBoundTargets)
|
||||
{
|
||||
renderTargets = allBoundTargets
|
||||
.Where(target => target.Slot == 0)
|
||||
.ToArray();
|
||||
if (HasPixelColorExport(pixelState, target.Slot))
|
||||
{
|
||||
selectedTargets.Add(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTargets.Count == 0)
|
||||
{
|
||||
foreach (var target in allBoundTargets)
|
||||
{
|
||||
if (target.Slot == 0)
|
||||
{
|
||||
selectedTargets.Add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedTargets.Sort(static (left, right) => left.Slot.CompareTo(right.Slot));
|
||||
var renderTargets = selectedTargets.ToArray();
|
||||
if (_traceAgcShader && allBoundTargets.Count > 1)
|
||||
{
|
||||
TraceAgcShader(
|
||||
@@ -6398,12 +6413,23 @@ public static partial class AgcExports
|
||||
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||
GetPixelColorExportMask(state, target) != 0;
|
||||
|
||||
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
|
||||
state.Program.Instructions
|
||||
.Select(instruction => instruction.Control)
|
||||
.OfType<Gen5ExportControl>()
|
||||
.Where(export => export.Target == target)
|
||||
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
|
||||
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target)
|
||||
{
|
||||
// Called per render target (twice per draw via CreateRenderState +
|
||||
// HasPixelColorExport); a manual scan avoids the per-call LINQ iterator
|
||||
// and closure allocations. Same result as the previous
|
||||
// Select/OfType/Where/Aggregate chain.
|
||||
var mask = 0u;
|
||||
foreach (var instruction in state.Program.Instructions)
|
||||
{
|
||||
if (instruction.Control is Gen5ExportControl export && export.Target == target)
|
||||
{
|
||||
mask |= export.EnableMask & 0xFu;
|
||||
}
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||
{
|
||||
@@ -6627,27 +6653,30 @@ public static partial class AgcExports
|
||||
|
||||
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
|
||||
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering).
|
||||
// DB_RENDER_CONTROL (context register 0x000): DEPTH_CLEAR_ENABLE bit0.
|
||||
private const uint DbDepthControl = 0x200;
|
||||
|
||||
private static GuestDepthState DecodeDepthState(
|
||||
internal static GuestDepthState DecodeDepthState(
|
||||
IReadOnlyDictionary<uint, uint> registers)
|
||||
{
|
||||
if (!registers.TryGetValue(DbDepthControl, out var control))
|
||||
{
|
||||
return GuestDepthState.Default;
|
||||
}
|
||||
|
||||
var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
|
||||
registers.TryGetValue(DbRenderControl, out var renderControl);
|
||||
var testEnable = (control & 0x2u) != 0;
|
||||
var writeEnable = (control & 0x4u) != 0;
|
||||
var compareOp = (control >> 4) & 0x7u;
|
||||
return new GuestDepthState(testEnable, writeEnable, compareOp);
|
||||
var compareOp = hasDepthControl
|
||||
? (control >> 4) & 0x7u
|
||||
: GuestDepthState.Default.CompareOp;
|
||||
var clearEnable = (renderControl & 0x1u) != 0;
|
||||
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
|
||||
}
|
||||
|
||||
private static GuestDepthTarget? DecodeDepthTarget(
|
||||
IReadOnlyDictionary<uint, uint> registers)
|
||||
{
|
||||
var depthState = DecodeDepthState(registers);
|
||||
if (!depthState.TestEnable && !depthState.WriteEnable)
|
||||
if (!depthState.TestEnable &&
|
||||
!depthState.WriteEnable &&
|
||||
!depthState.ClearEnable)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
@@ -13,25 +12,7 @@ namespace SharpEmu.Libs.Gpu;
|
||||
/// </summary>
|
||||
internal static class GuestGpu
|
||||
{
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(CreateBackend);
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||
|
||||
public static IGuestGpuBackend Current => Instance.Value;
|
||||
|
||||
private static IGuestGpuBackend CreateBackend()
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND"),
|
||||
"native",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new VulkanGuestGpuBackend();
|
||||
}
|
||||
|
||||
if (!NativeVulkanApi.IsAvailable(out var error))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] {error}");
|
||||
}
|
||||
|
||||
return new NativeVulkanGuestGpuBackend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +91,10 @@ internal readonly record struct GuestRasterState(
|
||||
internal readonly record struct GuestDepthState(
|
||||
bool TestEnable,
|
||||
bool WriteEnable,
|
||||
uint CompareOp)
|
||||
uint CompareOp,
|
||||
bool ClearEnable = false)
|
||||
{
|
||||
public static GuestDepthState Default { get; } = new(false, false, 7);
|
||||
public static GuestDepthState Default { get; } = new(false, false, 7, false);
|
||||
}
|
||||
|
||||
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal sealed unsafe class NativeGpuInputSource : IPosixWindowInputSource
|
||||
{
|
||||
internal static NativeGpuInputSource Instance { get; } = new();
|
||||
private readonly object _gate = new();
|
||||
private readonly uint[] _keys = new uint[8];
|
||||
private bool _focused;
|
||||
private bool _gamepadConnected;
|
||||
private HostGamepadState _gamepad;
|
||||
private string? _gamepadName;
|
||||
|
||||
private NativeGpuInputSource() { }
|
||||
|
||||
internal void Attach() => PosixHostInput.SetSource(this);
|
||||
|
||||
internal void Update(NativeVulkanApi.Input* state)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_focused = state->KeyboardFocused != 0;
|
||||
for (var index = 0; index < _keys.Length; ++index) _keys[index] = state->VirtualKeys[index];
|
||||
_gamepadConnected = state->GamepadConnected != 0;
|
||||
_gamepad = new HostGamepadState(
|
||||
_gamepadConnected,
|
||||
(HostGamepadButtons)state->GamepadButtons,
|
||||
state->LeftX,
|
||||
state->LeftY,
|
||||
state->RightX,
|
||||
state->RightY,
|
||||
state->LeftTrigger,
|
||||
state->RightTrigger);
|
||||
_gamepadName = _gamepadConnected
|
||||
? Marshal.PtrToStringUTF8((nint)state->GamepadNameUtf8)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasKeyboardFocus
|
||||
{
|
||||
get { lock (_gate) return _focused; }
|
||||
}
|
||||
|
||||
public bool IsKeyDown(int virtualKey)
|
||||
{
|
||||
if ((uint)virtualKey >= 256) return false;
|
||||
lock (_gate) return (_keys[virtualKey / 32] & (1u << (virtualKey % 32))) != 0;
|
||||
}
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_gamepadConnected || destination.IsEmpty) return 0;
|
||||
destination[0] = _gamepad;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public string? DescribeConnectedGamepad()
|
||||
{
|
||||
lock (_gate) return _gamepadConnected ? _gamepadName ?? "SDL gamepad" : null;
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal static unsafe class NativeGpuPacket
|
||||
{
|
||||
internal static NativeGpuResult SubmitDraw(
|
||||
nint backend,
|
||||
VulkanCompiledGuestShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> memoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
VulkanCompiledGuestShader? vertexShader,
|
||||
uint vertexCount,
|
||||
uint instanceCount,
|
||||
uint primitiveType,
|
||||
GuestIndexBuffer? indexBuffer,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers,
|
||||
GuestRenderState? renderState,
|
||||
IReadOnlyList<GuestRenderTarget>? targets,
|
||||
bool publishTargets)
|
||||
{
|
||||
using var storage = new Storage();
|
||||
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||
for (var index = 0; index < textures.Count; ++index)
|
||||
nativeTextures[index] = Texture(textures[index], storage);
|
||||
var nativeMemory = storage.Allocate<NativeVulkanApi.MemoryBuffer>(memoryBuffers.Count);
|
||||
for (var index = 0; index < memoryBuffers.Count; ++index)
|
||||
nativeMemory[index] = new() { Address = memoryBuffers[index].BaseAddress, Data = storage.Pin(memoryBuffers[index].Data) };
|
||||
var vertices = vertexBuffers ?? [];
|
||||
var nativeVertices = storage.Allocate<NativeVulkanApi.VertexBuffer>(vertices.Count);
|
||||
for (var index = 0; index < vertices.Count; ++index)
|
||||
{
|
||||
var source = vertices[index];
|
||||
nativeVertices[index] = new()
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.VertexBuffer), Location = source.Location,
|
||||
ComponentCount = source.ComponentCount, DataFormat = source.DataFormat,
|
||||
NumberFormat = source.NumberFormat, Address = source.BaseAddress, Stride = source.Stride,
|
||||
OffsetBytes = source.OffsetBytes, Data = storage.Pin(source.Data),
|
||||
};
|
||||
}
|
||||
var targetList = targets ?? [];
|
||||
var nativeTargets = storage.Allocate<NativeVulkanApi.RenderTarget>(targetList.Count);
|
||||
for (var index = 0; index < targetList.Count; ++index)
|
||||
{
|
||||
var source = targetList[index];
|
||||
nativeTargets[index] = new()
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.RenderTarget), Address = source.Address,
|
||||
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||
NumberType = source.NumberType, MipLevels = source.MipLevels,
|
||||
};
|
||||
}
|
||||
var state = renderState ?? GuestRenderState.Default;
|
||||
var blends = storage.Allocate<NativeVulkanApi.Blend>(state.Blends.Count);
|
||||
for (var index = 0; index < state.Blends.Count; ++index)
|
||||
{
|
||||
var source = state.Blends[index];
|
||||
blends[index] = new()
|
||||
{
|
||||
Enable = source.Enable ? 1u : 0u, ColorSrc = source.ColorSrcFactor,
|
||||
ColorDst = source.ColorDstFactor, ColorFunc = source.ColorFunc,
|
||||
AlphaSrc = source.AlphaSrcFactor, AlphaDst = source.AlphaDstFactor,
|
||||
AlphaFunc = source.AlphaFunc, SeparateAlpha = source.SeparateAlphaBlend ? 1u : 0u,
|
||||
WriteMask = source.WriteMask,
|
||||
};
|
||||
}
|
||||
NativeVulkanApi.IndexBuffer nativeIndex = default;
|
||||
NativeVulkanApi.IndexBuffer* nativeIndexPointer = null;
|
||||
if (indexBuffer is not null)
|
||||
{
|
||||
nativeIndex = new() { Data = storage.Pin(indexBuffer.Data), Is32Bit = indexBuffer.Is32Bit ? 1u : 0u };
|
||||
nativeIndexPointer = &nativeIndex;
|
||||
}
|
||||
NativeVulkanApi.Rect nativeScissor = default; NativeVulkanApi.Rect* scissorPointer = null;
|
||||
if (state.Scissor is { } scissor)
|
||||
{
|
||||
nativeScissor = new() { X = scissor.X, Y = scissor.Y, Width = scissor.Width, Height = scissor.Height };
|
||||
scissorPointer = &nativeScissor;
|
||||
}
|
||||
NativeVulkanApi.Viewport nativeViewport = default; NativeVulkanApi.Viewport* viewportPointer = null;
|
||||
if (state.Viewport is { } viewport)
|
||||
{
|
||||
nativeViewport = new()
|
||||
{
|
||||
X = viewport.X, Y = viewport.Y, Width = viewport.Width, Height = viewport.Height,
|
||||
MinDepth = viewport.MinDepth, MaxDepth = viewport.MaxDepth,
|
||||
};
|
||||
viewportPointer = &nativeViewport;
|
||||
}
|
||||
var draw = new NativeVulkanApi.Draw
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Draw),
|
||||
Width = width,
|
||||
Height = height,
|
||||
VertexSpirv = storage.Pin(vertexShader?.Spirv ?? SpirvFixedShaders.CreateFullscreenVertex(attributeCount)),
|
||||
PixelSpirv = storage.Pin(pixelShader.Spirv), Textures = nativeTextures,
|
||||
TextureCount = (uint)textures.Count, MemoryBuffers = nativeMemory,
|
||||
MemoryBufferCount = (uint)memoryBuffers.Count, VertexBuffers = nativeVertices,
|
||||
VertexBufferCount = (uint)vertices.Count, Targets = nativeTargets,
|
||||
TargetCount = (uint)targetList.Count, Blends = blends, BlendCount = (uint)state.Blends.Count,
|
||||
IndexBuffer = nativeIndexPointer, Scissor = scissorPointer, ViewportState = viewportPointer,
|
||||
AttributeCount = attributeCount, VertexCount = vertexCount, InstanceCount = instanceCount,
|
||||
PrimitiveType = primitiveType, PublishTargets = publishTargets ? 1u : 0u,
|
||||
};
|
||||
return NativeVulkanApi.SubmitDraw(backend, &draw);
|
||||
}
|
||||
|
||||
internal static NativeGpuResult SubmitCompute(
|
||||
nint backend,
|
||||
ulong shaderAddress,
|
||||
VulkanCompiledGuestShader shader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> buffers,
|
||||
uint x, uint y, uint z)
|
||||
{
|
||||
using var storage = new Storage();
|
||||
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||
for (var index = 0; index < textures.Count; ++index) nativeTextures[index] = Texture(textures[index], storage);
|
||||
var nativeBuffers = storage.Allocate<NativeVulkanApi.MemoryBuffer>(buffers.Count);
|
||||
for (var index = 0; index < buffers.Count; ++index)
|
||||
nativeBuffers[index] = new() { Address = buffers[index].BaseAddress, Data = storage.Pin(buffers[index].Data) };
|
||||
var compute = new NativeVulkanApi.Compute
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Compute), ShaderAddress = shaderAddress,
|
||||
Spirv = storage.Pin(shader.Spirv), Textures = nativeTextures, TextureCount = (uint)textures.Count,
|
||||
MemoryBuffers = nativeBuffers, MemoryBufferCount = (uint)buffers.Count,
|
||||
GroupsX = x, GroupsY = y, GroupsZ = z,
|
||||
};
|
||||
return NativeVulkanApi.SubmitCompute(backend, &compute);
|
||||
}
|
||||
|
||||
private static NativeVulkanApi.Texture Texture(GuestDrawTexture source, Storage storage)
|
||||
{
|
||||
var result = new NativeVulkanApi.Texture
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Texture), Address = source.Address,
|
||||
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||
NumberType = source.NumberType, RgbaPixels = storage.Pin(source.RgbaPixels),
|
||||
IsFallback = source.IsFallback ? 1u : 0u, IsStorage = source.IsStorage ? 1u : 0u,
|
||||
MipLevels = source.MipLevels, MipLevel = source.MipLevel, Pitch = source.Pitch,
|
||||
TileMode = source.TileMode, DstSelect = source.DstSelect,
|
||||
};
|
||||
result.SamplerState.Words[0] = source.Sampler.Word0;
|
||||
result.SamplerState.Words[1] = source.Sampler.Word1;
|
||||
result.SamplerState.Words[2] = source.Sampler.Word2;
|
||||
result.SamplerState.Words[3] = source.Sampler.Word3;
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed class Storage : IDisposable
|
||||
{
|
||||
private readonly List<GCHandle> _pins = [];
|
||||
private readonly List<nint> _allocations = [];
|
||||
internal NativeVulkanApi.Bytes Pin(byte[] data)
|
||||
{
|
||||
if (data.Length == 0) return default;
|
||||
var pin = GCHandle.Alloc(data, GCHandleType.Pinned); _pins.Add(pin);
|
||||
return new() { Data = (void*)pin.AddrOfPinnedObject(), Size = (nuint)data.Length };
|
||||
}
|
||||
internal T* Allocate<T>(int count) where T : unmanaged
|
||||
{
|
||||
if (count == 0) return null;
|
||||
var pointer = NativeMemory.AllocZeroed((nuint)count, (nuint)sizeof(T));
|
||||
if (pointer is null) throw new OutOfMemoryException();
|
||||
_allocations.Add((nint)pointer); return (T*)pointer;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var pin in _pins) pin.Free();
|
||||
foreach (var allocation in _allocations) NativeMemory.Free((void*)allocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal enum NativeGpuResult
|
||||
{
|
||||
Success = 0,
|
||||
NotFound = 1,
|
||||
NotReady = 2,
|
||||
InvalidArgument = -1,
|
||||
IncompatibleAbi = -2,
|
||||
PlatformError = -3,
|
||||
VulkanError = -4,
|
||||
OutOfMemory = -5,
|
||||
InternalError = -6,
|
||||
}
|
||||
|
||||
internal static unsafe partial class NativeVulkanApi
|
||||
{
|
||||
internal const uint AbiVersion = 1;
|
||||
private const string Library = "sharpemu_gpu_vulkan";
|
||||
|
||||
internal static string LibraryFileName =>
|
||||
OperatingSystem.IsWindows() ? "sharpemu_gpu_vulkan.dll" :
|
||||
OperatingSystem.IsMacOS() ? "libsharpemu_gpu_vulkan.dylib" :
|
||||
"libsharpemu_gpu_vulkan.so";
|
||||
|
||||
internal static bool IsAvailable(out string error)
|
||||
{
|
||||
if (NativeLibrary.TryLoad(
|
||||
Library,
|
||||
typeof(NativeVulkanApi).Assembly,
|
||||
DllImportSearchPath.SafeDirectories | DllImportSearchPath.AssemblyDirectory,
|
||||
out var handle))
|
||||
{
|
||||
NativeLibrary.Free(handle);
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
error =
|
||||
$"The optional native Vulkan backend was selected, but {LibraryFileName} could not be loaded. " +
|
||||
"Place the library from https://github.com/sharpemu/sharpemu.vulkan next to the SharpEmu executable " +
|
||||
"and ensure its SDL3 and Vulkan runtime dependencies are installed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CreateInfo
|
||||
{
|
||||
internal uint StructSize;
|
||||
internal uint AbiVersion;
|
||||
internal uint Width;
|
||||
internal uint Height;
|
||||
internal uint EnableValidation;
|
||||
internal byte* TitleUtf8;
|
||||
internal delegate* unmanaged[Cdecl]<int, byte*, void*, void> Log;
|
||||
internal void* LogUser;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Bytes { internal void* Data; internal nuint Size; }
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Sampler { internal fixed uint Words[4]; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Texture
|
||||
{
|
||||
internal uint StructSize; internal ulong Address; internal uint Width, Height, Format, NumberType;
|
||||
internal Bytes RgbaPixels; internal uint IsFallback, IsStorage, MipLevels, MipLevel;
|
||||
internal uint Pitch, TileMode, DstSelect; internal Sampler SamplerState;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct MemoryBuffer { internal ulong Address; internal Bytes Data; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct VertexBuffer
|
||||
{
|
||||
internal uint StructSize, Location, ComponentCount, DataFormat, NumberFormat;
|
||||
internal ulong Address; internal uint Stride, OffsetBytes; internal Bytes Data;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct IndexBuffer { internal Bytes Data; internal uint Is32Bit; }
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Rect { internal int X, Y; internal uint Width, Height; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Viewport { internal float X, Y, Width, Height, MinDepth, MaxDepth; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Blend
|
||||
{
|
||||
internal uint Enable, ColorSrc, ColorDst, ColorFunc, AlphaSrc, AlphaDst, AlphaFunc;
|
||||
internal uint SeparateAlpha, WriteMask;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct RenderTarget
|
||||
{
|
||||
internal uint StructSize; internal ulong Address;
|
||||
internal uint Width, Height, Format, NumberType, MipLevels;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Draw
|
||||
{
|
||||
internal uint StructSize, Width, Height; internal Bytes VertexSpirv, PixelSpirv;
|
||||
internal Texture* Textures; internal uint TextureCount;
|
||||
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||
internal VertexBuffer* VertexBuffers; internal uint VertexBufferCount;
|
||||
internal RenderTarget* Targets; internal uint TargetCount;
|
||||
internal Blend* Blends; internal uint BlendCount;
|
||||
internal IndexBuffer* IndexBuffer; internal Rect* Scissor; internal Viewport* ViewportState;
|
||||
internal uint AttributeCount, VertexCount, InstanceCount, PrimitiveType, PublishTargets;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Compute
|
||||
{
|
||||
internal uint StructSize; internal ulong ShaderAddress; internal Bytes Spirv;
|
||||
internal Texture* Textures; internal uint TextureCount;
|
||||
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||
internal uint GroupsX, GroupsY, GroupsZ;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Input
|
||||
{
|
||||
internal uint StructSize, KeyboardFocused;
|
||||
internal fixed uint VirtualKeys[8];
|
||||
internal uint GamepadConnected, GamepadButtons;
|
||||
internal byte LeftX, LeftY, RightX, RightY, LeftTrigger, RightTrigger;
|
||||
internal fixed byte Reserved[2]; internal fixed byte GamepadNameUtf8[128];
|
||||
}
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_abi_version")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial uint GetAbiVersion();
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_last_error")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static partial byte* LastError(nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_create")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult Create(CreateInfo* info, out nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_destroy")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial void Destroy(nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_poll")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult Poll(nint backend, out uint shouldClose);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_input_snapshot")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult InputSnapshot(nint backend, Input* input);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_present_bgra")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult PresentBgra(
|
||||
nint backend,
|
||||
void* pixels,
|
||||
nuint size,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitch);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_submit_draw")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult SubmitDraw(nint backend, Draw* draw);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_submit_compute")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult SubmitCompute(nint backend, Compute* compute);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_register_display_buffer")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult RegisterDisplayBuffer(nint backend, ulong address, uint format);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_present_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult PresentGuestImage(
|
||||
nint backend, ulong address, uint width, uint height, uint pitch);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_has_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult HasGuestImage(nint backend, ulong address, uint format, uint numberType);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_blit_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult BlitGuestImage(
|
||||
nint backend, ulong sourceAddress, uint sourceWidth, uint sourceHeight, uint sourceFormat,
|
||||
ulong destinationAddress, uint destinationWidth, uint destinationHeight, uint destinationFormat);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_render_target_output_kind")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult RenderTargetOutputKind(uint format, uint numberType, out uint outputKind);
|
||||
|
||||
internal static string GetError(nint backend)
|
||||
{
|
||||
var pointer = LastError(backend);
|
||||
return pointer is null ? "Unknown native GPU error" : Marshal.PtrToStringUTF8((nint)pointer)!;
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
/// <summary>Native C++ Vulkan implementation of the guest-domain GPU seam.</summary>
|
||||
internal sealed unsafe class NativeVulkanGuestGpuBackend : IGuestGpuBackend
|
||||
{
|
||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||
|
||||
private readonly object _startGate = new();
|
||||
private readonly BlockingCollection<Action<nint>> _commands = new(new ConcurrentQueue<Action<nint>>(), 256);
|
||||
private readonly ManualResetEventSlim _ready = new(false);
|
||||
private Thread? _thread;
|
||||
private Exception? _startError;
|
||||
|
||||
public void EnsureStarted(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0) return;
|
||||
lock (_startGate)
|
||||
{
|
||||
if (_thread is null)
|
||||
{
|
||||
_thread = new Thread(() => Run(width, height))
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu native Vulkan",
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
}
|
||||
_ready.Wait();
|
||||
if (_startError is not null)
|
||||
{
|
||||
if (_startError is DllNotFoundException)
|
||||
{
|
||||
_ = NativeVulkanApi.IsAvailable(out var error);
|
||||
throw new InvalidOperationException(error, _startError);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Native Vulkan startup failed", _startError);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompileVertexShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader, out string error, int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1, int imageBindingBase = 0, int scalarRegisterBufferIndex = -1,
|
||||
int requiredVertexOutputCount = 0, ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var compiled, out error,
|
||||
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex,
|
||||
requiredVertexOutputCount, storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public bool TryCompilePixelShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs, out IGuestCompiledShader? shader, out string error,
|
||||
int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1, uint pixelInputEnable = 0, uint pixelInputAddress = 0,
|
||||
ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, outputs, out var compiled, out error,
|
||||
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex,
|
||||
pixelInputEnable, pixelInputAddress, storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public bool TryCompileComputeShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX, uint localSizeY, uint localSizeZ, out IGuestCompiledShader? shader, out string error,
|
||||
int totalGlobalBufferCount = -1, int initialScalarBufferIndex = -1, uint waveLaneCount = 32,
|
||||
ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, localSizeX, localSizeY, localSizeZ,
|
||||
out var compiled, out error, totalGlobalBufferCount, initialScalarBufferIndex, waveLaneCount,
|
||||
storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public IGuestCompiledShader GetDepthOnlyFragmentShader() => DepthOnlyFragmentShader;
|
||||
|
||||
public void HideSplashScreen() { }
|
||||
|
||||
public void Submit(byte[] bgraFrame, uint width, uint height)
|
||||
{
|
||||
if (bgraFrame.Length != checked((int)(width * height * 4))) return;
|
||||
EnsureStarted(width, height);
|
||||
Enqueue(handle =>
|
||||
{
|
||||
fixed (byte* pixels = bgraFrame)
|
||||
Check(handle, NativeVulkanApi.PresentBgra(handle, pixels, (nuint)bgraFrame.Length, width, height, width * 4));
|
||||
});
|
||||
}
|
||||
|
||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height)
|
||||
{
|
||||
if (drawKind != GuestDrawKind.FullscreenBarycentric || width == 0 || height == 0) return;
|
||||
EnsureStarted(width, height);
|
||||
var pixel = new VulkanCompiledGuestShader(SpirvFixedShaders.CreateBarycentricFragment());
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, pixel, [], [],
|
||||
width, height, 1, null, 3, 1, 4, null, null, null, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitTranslatedDraw(IGuestCompiledShader pixelShader, IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers, uint width, uint height, uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null, uint vertexCount = 3, uint instanceCount = 1,
|
||||
uint primitiveType = 4, GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null, GuestRenderState? renderState = null)
|
||||
{
|
||||
EnsureStarted(width, height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
width, height, attributeCount, vs, vertexCount, instanceCount, primitiveType, indexBuffer,
|
||||
vertexCopy, renderState, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitOffscreenTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, IReadOnlyList<GuestRenderTarget> targets, IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null, GuestDepthTarget? depthTarget = null, ulong shaderAddress = 0)
|
||||
{
|
||||
if (targets.Count == 0) return;
|
||||
EnsureStarted(targets[0].Width, targets[0].Height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var targetCopy = targets.ToArray(); var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
targetCopy[0].Width, targetCopy[0].Height, attributeCount, vs, vertexCount, instanceCount,
|
||||
primitiveType, indexBuffer, vertexCopy, renderState, targetCopy, true)));
|
||||
}
|
||||
|
||||
public void SubmitDepthOnlyTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, GuestDepthTarget depthTarget, IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null, ulong shaderAddress = 0)
|
||||
{
|
||||
EnsureStarted(depthTarget.Width, depthTarget.Height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
depthTarget.Width, depthTarget.Height, attributeCount, vs, vertexCount, instanceCount,
|
||||
primitiveType, indexBuffer, vertexCopy, renderState, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitStorageTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, uint width, uint height, ulong shaderAddress = 0)
|
||||
{
|
||||
EnsureStarted(width, height); var ps = Spirv(pixelShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
GuestRenderTarget[] targets = [new(0, width, height, 12, 7)];
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
width, height, attributeCount, null, 3, 1, 4, null, null, null, targets, false)));
|
||||
}
|
||||
|
||||
public long SubmitComputeDispatch(ulong shaderAddress, IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX, uint groupCountY, uint groupCountZ, uint baseGroupX, uint baseGroupY,
|
||||
uint baseGroupZ, uint localSizeX, uint localSizeY, uint localSizeZ, bool isIndirect,
|
||||
bool writesGlobalMemory, uint threadCountX = uint.MaxValue, uint threadCountY = uint.MaxValue,
|
||||
uint threadCountZ = uint.MaxValue)
|
||||
{
|
||||
EnsureStarted(1280, 720); var shader = Spirv(computeShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitCompute(handle, shaderAddress, shader,
|
||||
textureCopy, memoryCopy, groupCountX, groupCountY, groupCountZ)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool TrySubmitGuestImage(ulong address, uint width, uint height, uint pitchInPixel)
|
||||
{
|
||||
EnsureStarted(width, height);
|
||||
return Invoke(handle => NativeVulkanApi.PresentGuestImage(handle, address, width, height, pitchInPixel)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TrySubmitOrderedGuestImageFlip(int videoOutHandle, int displayBufferIndex, ulong address,
|
||||
uint width, uint height, uint pitchInPixel) =>
|
||||
TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
|
||||
{
|
||||
EnsureStarted(1280, 720);
|
||||
Enqueue(handle => Check(handle, NativeVulkanApi.RegisterDisplayBuffer(handle, address, guestFormat)));
|
||||
}
|
||||
|
||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType)
|
||||
{
|
||||
EnsureStarted(1280, 720);
|
||||
return Invoke(handle => NativeVulkanApi.HasGuestImage(handle, address, format, numberType)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TrySubmitGuestImageBlit(ulong sourceAddress, uint sourceWidth, uint sourceHeight,
|
||||
uint sourceFormat, uint sourceNumberType, ulong destinationAddress, uint destinationWidth,
|
||||
uint destinationHeight, uint destinationFormat, uint destinationNumberType)
|
||||
{
|
||||
EnsureStarted(destinationWidth, destinationHeight);
|
||||
return Invoke(handle => NativeVulkanApi.BlitGuestImage(handle, sourceAddress, sourceWidth, sourceHeight,
|
||||
sourceFormat, destinationAddress, destinationWidth, destinationHeight, destinationFormat)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType,
|
||||
out Gen5PixelOutputKind outputKind)
|
||||
{
|
||||
var result = NativeVulkanApi.RenderTargetOutputKind(dataFormat, numberType, out var nativeKind);
|
||||
outputKind = (Gen5PixelOutputKind)nativeKind; return result == NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
private void Run(uint width, uint height)
|
||||
{
|
||||
nint backend = 0;
|
||||
try
|
||||
{
|
||||
if (NativeVulkanApi.GetAbiVersion() != NativeVulkanApi.AbiVersion)
|
||||
throw new InvalidOperationException("Native Vulkan ABI version mismatch");
|
||||
var title = Marshal.StringToCoTaskMemUTF8("SharpEmu");
|
||||
try
|
||||
{
|
||||
var info = new NativeVulkanApi.CreateInfo
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.CreateInfo), AbiVersion = NativeVulkanApi.AbiVersion,
|
||||
Width = width, Height = height, TitleUtf8 = (byte*)title,
|
||||
EnableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1" ? 1u : 0u,
|
||||
};
|
||||
var result = NativeVulkanApi.Create(&info, out backend);
|
||||
if (result != NativeGpuResult.Success)
|
||||
throw new InvalidOperationException($"se_gpu_create failed with {result}: {NativeVulkanApi.GetError(0)}");
|
||||
}
|
||||
finally { Marshal.FreeCoTaskMem(title); }
|
||||
_ready.Set();
|
||||
NativeGpuInputSource.Instance.Attach();
|
||||
while (true)
|
||||
{
|
||||
if (_commands.TryTake(out var command, 8))
|
||||
{
|
||||
command(backend);
|
||||
for (var drained = 1; drained < 128 && _commands.TryTake(out command); ++drained)
|
||||
command(backend);
|
||||
}
|
||||
var result = NativeVulkanApi.Poll(backend, out var shouldClose);
|
||||
if (result != NativeGpuResult.Success || shouldClose != 0) break;
|
||||
var input = new NativeVulkanApi.Input { StructSize = (uint)sizeof(NativeVulkanApi.Input) };
|
||||
if (NativeVulkanApi.InputSnapshot(backend, &input) == NativeGpuResult.Success)
|
||||
NativeGpuInputSource.Instance.Update(&input);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_startError ??= exception;
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Native Vulkan backend failed: {exception}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ready.Set();
|
||||
if (backend != 0) NativeVulkanApi.Destroy(backend);
|
||||
}
|
||||
}
|
||||
|
||||
private void Enqueue(Action<nint> command)
|
||||
{
|
||||
if (!_commands.TryAdd(command)) Console.Error.WriteLine("[LOADER][WARN] Native GPU queue is full; dropping work");
|
||||
}
|
||||
|
||||
private NativeGpuResult Invoke(Func<nint, NativeGpuResult> operation)
|
||||
{
|
||||
var completion = new TaskCompletionSource<NativeGpuResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Enqueue(handle =>
|
||||
{
|
||||
try { completion.SetResult(operation(handle)); }
|
||||
catch (Exception exception) { completion.SetException(exception); }
|
||||
});
|
||||
return completion.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void Check(nint backend, NativeGpuResult result)
|
||||
{
|
||||
if (result is NativeGpuResult.Success or NativeGpuResult.NotReady) return;
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Native GPU operation failed: {result}: {NativeVulkanApi.GetError(backend)}");
|
||||
}
|
||||
|
||||
private static VulkanCompiledGuestShader Spirv(IGuestCompiledShader shader) =>
|
||||
shader as VulkanCompiledGuestShader ?? throw new InvalidOperationException(
|
||||
$"Shader type {shader.GetType().Name} was not compiled by the native Vulkan backend");
|
||||
}
|
||||
@@ -25,6 +25,11 @@ public static class KernelEventFlagCompatExports
|
||||
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
|
||||
private static long _nextEventFlagHandle = 1;
|
||||
|
||||
// Cached once: gating every call site avoids building the interpolated
|
||||
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
|
||||
private static readonly bool _traceEventFlag = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class EventFlagState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
@@ -79,7 +84,7 @@ public static class KernelEventFlagCompatExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -101,7 +106,7 @@ public static class KernelEventFlagCompatExports
|
||||
Monitor.PulseAll(state.Gate);
|
||||
}
|
||||
|
||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
if (_traceEventFlag) TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -124,7 +129,7 @@ public static class KernelEventFlagCompatExports
|
||||
{
|
||||
state.Bits |= pattern;
|
||||
Monitor.PulseAll(state.Gate);
|
||||
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||
@@ -148,7 +153,7 @@ public static class KernelEventFlagCompatExports
|
||||
lock (state.Gate)
|
||||
{
|
||||
state.Bits &= pattern;
|
||||
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
@@ -189,7 +194,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
@@ -287,8 +292,8 @@ public static class KernelEventFlagCompatExports
|
||||
return true;
|
||||
},
|
||||
deadline);
|
||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||
if (!requestedBlock)
|
||||
{
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
@@ -298,7 +303,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
var releaseWaiter = true;
|
||||
try
|
||||
{
|
||||
@@ -318,7 +323,7 @@ public static class KernelEventFlagCompatExports
|
||||
{
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, pumpedWaitResult);
|
||||
}
|
||||
|
||||
@@ -329,7 +334,7 @@ public static class KernelEventFlagCompatExports
|
||||
releaseWaiter = false;
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
@@ -346,7 +351,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
finally
|
||||
@@ -381,7 +386,7 @@ public static class KernelEventFlagCompatExports
|
||||
state.Bits = setPattern;
|
||||
state.WaitingThreads = 0;
|
||||
Monitor.PulseAll(state.Gate);
|
||||
TraceEventFlag(
|
||||
if (_traceEventFlag) TraceEventFlag(
|
||||
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
|
||||
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
|
||||
}
|
||||
@@ -476,7 +481,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
TraceEventFlag(
|
||||
if (_traceEventFlag) TraceEventFlag(
|
||||
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
|
||||
return true;
|
||||
}
|
||||
@@ -568,7 +573,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void TraceEventFlag(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||
if (_traceEventFlag)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
|
||||
}
|
||||
|
||||
@@ -102,7 +102,11 @@ public static partial class KernelMemoryCompatExports
|
||||
private static readonly object _guestMountGate = new();
|
||||
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
||||
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
|
||||
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
|
||||
// Keyed by (and kept sorted on) region base address so VirtualQuery can find a
|
||||
// containing/next region with a binary search instead of an O(n) scan. Every
|
||||
// write uses the region's own Address as the key (see AddMappedRegionSliceLocked
|
||||
// and the mmap sites), so Values enumerate in ascending address order.
|
||||
private static readonly SortedList<ulong, MappedRegion> _mappedRegions = new();
|
||||
private static readonly Dictionary<ulong, string> _mappedRegionNames = new();
|
||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||
@@ -1574,7 +1578,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",
|
||||
@@ -4013,7 +4037,7 @@ public static partial class KernelMemoryCompatExports
|
||||
_ => unchecked((int)argumentSource.NextGpArg())
|
||||
};
|
||||
|
||||
var formatted = value.ToString();
|
||||
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||
if (showSign && value >= 0)
|
||||
formatted = "+" + formatted;
|
||||
else if (spaceForSign && value >= 0)
|
||||
@@ -4033,7 +4057,7 @@ public static partial class KernelMemoryCompatExports
|
||||
_ => (uint)argumentSource.NextGpArg()
|
||||
};
|
||||
|
||||
var formatted = value.ToString();
|
||||
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
||||
}
|
||||
break;
|
||||
@@ -4050,8 +4074,8 @@ public static partial class KernelMemoryCompatExports
|
||||
};
|
||||
|
||||
var formatted = specifier == 'x'
|
||||
? value.ToString("x")
|
||||
: value.ToString("X");
|
||||
? value.ToString("x", CultureInfo.InvariantCulture)
|
||||
: value.ToString("X", CultureInfo.InvariantCulture);
|
||||
|
||||
if (alternateForm && value != 0)
|
||||
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
||||
@@ -4155,7 +4179,10 @@ public static partial class KernelMemoryCompatExports
|
||||
var formatStr = precision >= 0
|
||||
? $"{{0:{specifier}{precision}}}"
|
||||
: $"{{0:{specifier}}}";
|
||||
var formatted = string.Format(formatStr, value);
|
||||
var formatted = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
formatStr,
|
||||
value);
|
||||
|
||||
if (showSign && value >= 0)
|
||||
formatted = "+" + formatted;
|
||||
@@ -4219,31 +4246,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 +4852,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) ||
|
||||
@@ -5427,7 +5484,9 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
var affected = new List<MappedRegion>();
|
||||
var cursor = address;
|
||||
foreach (var region in _mappedRegions.Values.OrderBy(static region => region.Address))
|
||||
// _mappedRegions is a SortedList keyed by address, so Values already
|
||||
// enumerate in ascending address order.
|
||||
foreach (var region in _mappedRegions.Values)
|
||||
{
|
||||
if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor)
|
||||
{
|
||||
@@ -5588,9 +5647,33 @@ public static partial class KernelMemoryCompatExports
|
||||
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
||||
{
|
||||
region = default;
|
||||
var foundNext = false;
|
||||
foreach (var candidate in _mappedRegions.Values)
|
||||
var keys = _mappedRegions.Keys;
|
||||
var values = _mappedRegions.Values;
|
||||
var count = keys.Count;
|
||||
|
||||
// First index whose region address is >= queryAddress.
|
||||
var lo = 0;
|
||||
var hi = count;
|
||||
while (lo < hi)
|
||||
{
|
||||
var mid = (int)(((uint)lo + (uint)hi) >> 1);
|
||||
if (keys[mid] < queryAddress)
|
||||
{
|
||||
lo = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// Regions do not overlap, so only the one with the greatest base address
|
||||
// <= queryAddress can contain it — index lo when it starts exactly at
|
||||
// queryAddress, otherwise lo - 1.
|
||||
var floorIndex = (lo < count && keys[lo] == queryAddress) ? lo : lo - 1;
|
||||
if (floorIndex >= 0)
|
||||
{
|
||||
var candidate = values[floorIndex];
|
||||
if (TryAddU64(candidate.Address, candidate.Length, out var candidateEnd) &&
|
||||
queryAddress >= candidate.Address &&
|
||||
queryAddress < candidateEnd)
|
||||
@@ -5598,20 +5681,16 @@ public static partial class KernelMemoryCompatExports
|
||||
region = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!findNext || candidate.Address < queryAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundNext || candidate.Address < region.Address)
|
||||
{
|
||||
region = candidate;
|
||||
foundNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
return foundNext;
|
||||
// findNext: the region with the smallest base address >= queryAddress.
|
||||
if (findNext && lo < count)
|
||||
{
|
||||
region = values[lo];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void TraceDirectMemoryCall(
|
||||
@@ -5640,10 +5719,12 @@ public static partial class KernelMemoryCompatExports
|
||||
$"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} len=0x{length:X16} align=0x{alignment:X16} type=0x{memoryType:X8} out=0x{outAddress:X16} selected=0x{selectedAddress:X16} result={result?.ToString() ?? "<pending>"}");
|
||||
}
|
||||
|
||||
private static bool ShouldTraceDirectMemory()
|
||||
{
|
||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal);
|
||||
}
|
||||
// Cached once so the ~8 direct-memory call sites don't each do a
|
||||
// GetEnvironmentVariable P/Invoke per operation.
|
||||
private static readonly bool _traceDirectMemory = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static bool ShouldTraceDirectMemory() => _traceDirectMemory;
|
||||
|
||||
private static bool TryReleaseDirectMemoryRangeLocked(ulong start, ulong length)
|
||||
{
|
||||
|
||||
@@ -344,8 +344,10 @@ public static class NetExports
|
||||
public static int NetHtonl(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -356,8 +358,10 @@ public static class NetExports
|
||||
public static int NetHtons(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -368,8 +372,10 @@ public static class NetExports
|
||||
public static int NetNtohl(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -380,8 +386,10 @@ public static class NetExports
|
||||
public static int NetNtohs(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,9 +253,9 @@ public static class HostWindowInput
|
||||
};
|
||||
}
|
||||
|
||||
private static byte ToStickByte(float value)
|
||||
internal static byte ToStickByte(float value)
|
||||
{
|
||||
return (byte)Math.Clamp((int)(128.0f + value * 127.0f), 0, 255);
|
||||
return (byte)Math.Clamp((int)MathF.Round((value + 1.0f) * 127.5f), 0, 255);
|
||||
}
|
||||
|
||||
private static HostGamepadButtons MapButton(ButtonName name) => name switch
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace SharpEmu.Libs.PlayGo;
|
||||
|
||||
@@ -418,12 +419,21 @@ 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.
|
||||
loci[i] = PlayGoLocusNotDownloaded;
|
||||
return ctx.Memory.TryWrite(outLoci, loci)
|
||||
? OrbisPlayGoErrorBadChunkId
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
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 +673,8 @@ public static class PlayGoExports
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||
return _metadata.ChunkIdKnowledge == PlayGoChunkIdKnowledge.Unknown ||
|
||||
Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,7 +683,12 @@ 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],
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
@@ -682,13 +698,24 @@ 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");
|
||||
return new PlayGoMetadata(true, [(ushort)0]);
|
||||
// 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],
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||
return new PlayGoMetadata(true, chunkIds);
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
chunkIds,
|
||||
chunkIds.Length == 0
|
||||
? PlayGoChunkIdKnowledge.Unknown
|
||||
: PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||
@@ -701,6 +728,8 @@ public static class PlayGoExports
|
||||
try
|
||||
{
|
||||
var xml = File.ReadAllText(chunkDefsXml);
|
||||
_ = XDocument.Parse(xml, LoadOptions.None);
|
||||
|
||||
var chunkIds = new HashSet<ushort>();
|
||||
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
||||
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
||||
@@ -717,6 +746,10 @@ public static class PlayGoExports
|
||||
{
|
||||
return Array.Empty<ushort>();
|
||||
}
|
||||
catch (System.Xml.XmlException)
|
||||
{
|
||||
return Array.Empty<ushort>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
||||
@@ -738,7 +771,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,13 +781,42 @@ 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}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
|
||||
lock (_stateGate)
|
||||
{
|
||||
_initialized = false;
|
||||
_opened = false;
|
||||
_metadata = PlayGoMetadata.Empty;
|
||||
_installSpeed = PlayGoInstallSpeedTrickle;
|
||||
_languageMask = ulong.MaxValue;
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _unknownChunkDiagnostics, 0);
|
||||
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
|
||||
}
|
||||
|
||||
private enum PlayGoChunkIdKnowledge
|
||||
{
|
||||
Unknown,
|
||||
Authoritative,
|
||||
}
|
||||
|
||||
private sealed record PlayGoMetadata(
|
||||
bool Available,
|
||||
ushort[] ChunkIds,
|
||||
PlayGoChunkIdKnowledge ChunkIdKnowledge)
|
||||
{
|
||||
public static readonly PlayGoMetadata Empty = new(
|
||||
false,
|
||||
Array.Empty<ushort>(),
|
||||
PlayGoChunkIdKnowledge.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -259,6 +259,22 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Nv8c-Kb+DUM",
|
||||
ExportName = "sceVideoOutIsOutputSupported",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutIsOutputSupported(CpuContext ctx)
|
||||
{
|
||||
var busType = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
_ = ctx[CpuRegister.Rsi]; // pixelFormat
|
||||
_ = ctx[CpuRegister.Rdx]; // aspectRatio
|
||||
|
||||
// The emulator supports any output configuration on the main bus.
|
||||
// Return 1 (supported) for SceVideoOutBusTypeMain, 0 otherwise.
|
||||
return busType == SceVideoOutBusTypeMain ? 1 : 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uquVH4-Du78",
|
||||
ExportName = "sceVideoOutClose",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Silk.NET.Vulkan;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
internal readonly record struct VulkanHostBufferPoolKey(
|
||||
BufferUsageFlags Usage,
|
||||
ulong Capacity);
|
||||
|
||||
internal readonly record struct VulkanHostBufferAllocation(
|
||||
VkBuffer Buffer,
|
||||
DeviceMemory Memory,
|
||||
VulkanHostBufferPoolKey Key,
|
||||
nint Mapped);
|
||||
|
||||
internal sealed class VulkanHostBufferPool : IDisposable
|
||||
{
|
||||
private readonly Dictionary<VulkanHostBufferPoolKey, Stack<VulkanHostBufferAllocation>>
|
||||
_available = [];
|
||||
private readonly Dictionary<ulong, VulkanHostBufferAllocation> _allocations = [];
|
||||
private readonly HashSet<ulong> _cachedHandles = [];
|
||||
private readonly Action<VulkanHostBufferAllocation> _destroy;
|
||||
|
||||
public VulkanHostBufferPool(
|
||||
ulong maximumCachedBytes,
|
||||
Action<VulkanHostBufferAllocation> destroy)
|
||||
{
|
||||
MaximumCachedBytes = maximumCachedBytes;
|
||||
_destroy = destroy;
|
||||
}
|
||||
|
||||
public ulong MaximumCachedBytes { get; }
|
||||
|
||||
public ulong CachedBytes { get; private set; }
|
||||
|
||||
public bool TryRent(
|
||||
VulkanHostBufferPoolKey key,
|
||||
out VulkanHostBufferAllocation allocation)
|
||||
{
|
||||
if (!_available.TryGetValue(key, out var available) ||
|
||||
!available.TryPop(out allocation))
|
||||
{
|
||||
allocation = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
_cachedHandles.Remove(allocation.Buffer.Handle);
|
||||
CachedBytes -= allocation.Key.Capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Register(VulkanHostBufferAllocation allocation)
|
||||
{
|
||||
if (allocation.Buffer.Handle == 0)
|
||||
{
|
||||
throw new ArgumentException("A pooled buffer must have a valid handle.", nameof(allocation));
|
||||
}
|
||||
|
||||
_allocations.Add(allocation.Buffer.Handle, allocation);
|
||||
}
|
||||
|
||||
public bool Return(VkBuffer buffer, DeviceMemory memory)
|
||||
{
|
||||
if (!_allocations.TryGetValue(buffer.Handle, out var allocation) ||
|
||||
allocation.Memory.Handle != memory.Handle)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_cachedHandles.Add(buffer.Handle))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allocation.Key.Capacity > MaximumCachedBytes - CachedBytes)
|
||||
{
|
||||
_cachedHandles.Remove(buffer.Handle);
|
||||
_allocations.Remove(buffer.Handle);
|
||||
_destroy(allocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_available.TryGetValue(allocation.Key, out var available))
|
||||
{
|
||||
available = [];
|
||||
_available.Add(allocation.Key, available);
|
||||
}
|
||||
|
||||
available.Push(allocation);
|
||||
CachedBytes += allocation.Key.Capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var allocation in _allocations.Values)
|
||||
{
|
||||
_destroy(allocation);
|
||||
}
|
||||
|
||||
_allocations.Clear();
|
||||
_available.Clear();
|
||||
_cachedHandles.Clear();
|
||||
CachedBytes = 0;
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// stays tighter than the drain budget because queued draws pin their
|
||||
// pooled guest-data arrays until the render thread uploads them.
|
||||
private const int MaxPendingGuestWork = 64;
|
||||
private const ulong MaximumCachedHostBufferBytes = 128UL * 1024 * 1024;
|
||||
// A captured 4K flip can consume tens of MiB of device-local memory.
|
||||
// Retain only a short presentation queue while always preserving the
|
||||
// newest generation; older immutable versions are retired immediately.
|
||||
@@ -608,6 +609,42 @@ internal static unsafe class VulkanVideoPresenter
|
||||
shaderAddress);
|
||||
}
|
||||
|
||||
// Manual scans (targets are <= 8) so the per-draw validation does not
|
||||
// allocate LINQ iterators/closures or a Distinct HashSet.
|
||||
private static bool AnyRenderTargetInvalid(IReadOnlyList<GuestRenderTarget> targets)
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (target.Address == 0 || target.Width == 0 || target.Height == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool RenderTargetsMismatchedOrAliased(IReadOnlyList<GuestRenderTarget> targets, GuestRenderTarget first)
|
||||
{
|
||||
for (var i = 0; i < targets.Count; i++)
|
||||
{
|
||||
if (targets[i].Width != first.Width || targets[i].Height != first.Height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var j = i + 1; j < targets.Count; j++)
|
||||
{
|
||||
if (targets[i].Address == targets[j].Address)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void SubmitOffscreenTranslatedDraw(
|
||||
byte[] pixelSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
@@ -627,16 +664,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (pixelSpirv.Length == 0 ||
|
||||
targets.Count == 0 ||
|
||||
targets.Count > 8 ||
|
||||
targets.Any(target =>
|
||||
target.Address == 0 || target.Width == 0 || target.Height == 0))
|
||||
AnyRenderTargetInvalid(targets))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstTarget = targets[0];
|
||||
if (targets.Any(target =>
|
||||
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
|
||||
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
|
||||
if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
|
||||
@@ -654,9 +688,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var effectiveRenderState = renderState ?? GuestRenderState.Default;
|
||||
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
|
||||
{
|
||||
var broadcastBlends = new GuestBlendState[targets.Count];
|
||||
Array.Fill(broadcastBlends, effectiveRenderState.Blends[0]);
|
||||
effectiveRenderState = effectiveRenderState with
|
||||
{
|
||||
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(),
|
||||
Blends = broadcastBlends,
|
||||
};
|
||||
}
|
||||
lock (_gate)
|
||||
@@ -2177,6 +2213,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return keys;
|
||||
}
|
||||
|
||||
internal static bool ShouldAttachGuestDepth(
|
||||
GuestDepthTarget? target,
|
||||
GuestDepthState state) =>
|
||||
target is not null &&
|
||||
(state.TestEnable || state.WriteEnable || state.ClearEnable);
|
||||
|
||||
private readonly record struct Presentation(
|
||||
byte[]? Pixels,
|
||||
uint Width,
|
||||
@@ -2357,9 +2399,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<DescriptorLayoutKey, DescriptorLayoutBundle>
|
||||
_descriptorLayouts = new();
|
||||
private readonly Dictionary<HostBufferPoolKey, Stack<HostBufferAllocation>>
|
||||
_hostBufferPool = new();
|
||||
private readonly Dictionary<ulong, HostBufferAllocation> _hostBufferAllocations = new();
|
||||
private readonly VulkanHostBufferPool _hostBufferPool;
|
||||
private readonly List<GuestBufferAllocation> _guestBufferAllocations = [];
|
||||
private readonly Queue<PendingGuestSubmission> _pendingGuestSubmissions = new();
|
||||
private readonly Dictionary<string, ulong> _lastSubmittedTimelineByGuestQueue =
|
||||
@@ -2381,10 +2421,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
GuestRasterState Raster,
|
||||
GuestDepthState Depth);
|
||||
|
||||
private readonly record struct HostBufferPoolKey(
|
||||
BufferUsageFlags Usage,
|
||||
ulong Capacity);
|
||||
|
||||
private readonly record struct DescriptorLayoutKey(
|
||||
ShaderStageFlags Stages,
|
||||
string Resources);
|
||||
@@ -2397,12 +2433,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
DescriptorSetLayout DescriptorSetLayout,
|
||||
PipelineLayout PipelineLayout);
|
||||
|
||||
private sealed record HostBufferAllocation(
|
||||
VkBuffer Buffer,
|
||||
DeviceMemory Memory,
|
||||
HostBufferPoolKey Key,
|
||||
nint Mapped);
|
||||
|
||||
private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length);
|
||||
|
||||
private sealed class GuestBufferAllocation
|
||||
@@ -2592,6 +2622,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
public Presenter(uint width, uint height)
|
||||
{
|
||||
_hostBufferPool = new VulkanHostBufferPool(
|
||||
MaximumCachedHostBufferBytes,
|
||||
DestroyHostBufferAllocation);
|
||||
var options = WindowOptions.DefaultVulkan;
|
||||
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
|
||||
options.Title = VideoOutExports.GetWindowTitle();
|
||||
@@ -2646,6 +2679,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
if (PngSplashLoader.TryLoadIcon(out var iconPixels, out var iconWidth, out var iconHeight))
|
||||
{
|
||||
var icon = new RawImage((int)iconWidth, (int)iconHeight, iconPixels);
|
||||
_window.SetWindowIcon(ref icon);
|
||||
}
|
||||
|
||||
WaitForRenderDocAttachIfRequested();
|
||||
_vk = Vk.GetApi();
|
||||
CreateInstance();
|
||||
@@ -4154,6 +4193,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);
|
||||
@@ -4214,23 +4295,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)
|
||||
@@ -4304,11 +4405,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
|
||||
1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
||||
TraceVulkanShader(
|
||||
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
|
||||
$"waited_ms={waitedMs:F3}");
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
|
||||
$"waited_ms={waitedMs:F3}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
|
||||
@@ -4316,10 +4420,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||
work.Action();
|
||||
TraceVulkanShader(
|
||||
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
||||
@@ -4508,13 +4615,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)
|
||||
@@ -5241,7 +5355,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
resources.IndexBuffer = CreateHostBuffer(
|
||||
indexBuffer.Data.AsSpan(0, indexBuffer.Length),
|
||||
BufferUsageFlags.IndexBufferBit,
|
||||
out resources.IndexMemory);
|
||||
out resources.IndexMemory,
|
||||
out _);
|
||||
resources.Index32Bit = indexBuffer.Is32Bit;
|
||||
if (indexBuffer.Pooled)
|
||||
{
|
||||
@@ -6764,7 +6879,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
|
||||
|
||||
var expectedSize = GetTextureByteCount(texture.Format, rowLength, height);
|
||||
if (_tracedTextureUploads.Add((texture.Address, width, height, vkFormat)))
|
||||
if (ShouldTraceVulkanResources() &&
|
||||
_tracedTextureUploads.Add((texture.Address, width, height, vkFormat)))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " +
|
||||
@@ -7419,21 +7535,43 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
||||
var endAddress = checked(guestBuffer.BaseAddress + size);
|
||||
var allocation = _guestBufferAllocations
|
||||
.Where(candidate =>
|
||||
candidate.BaseAddress <= guestBuffer.BaseAddress &&
|
||||
candidate.BaseAddress + candidate.Size >= endAddress)
|
||||
.OrderByDescending(candidate =>
|
||||
string.Equals(
|
||||
GuestBufferAllocation? allocation = null;
|
||||
var allocationPriority = -1;
|
||||
// This runs for every bound global buffer. Preserve the previous
|
||||
// stable queue preference without allocating LINQ sort state.
|
||||
foreach (var candidate in _guestBufferAllocations)
|
||||
{
|
||||
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
|
||||
candidate.BaseAddress + candidate.Size < endAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidatePriority = string.Equals(
|
||||
candidate.QueueName,
|
||||
_activeGuestQueue.Name,
|
||||
StringComparison.Ordinal))
|
||||
.ThenByDescending(candidate =>
|
||||
string.Equals(
|
||||
StringComparison.Ordinal)
|
||||
? 2
|
||||
: string.Equals(
|
||||
candidate.QueueName,
|
||||
SharedReadOnlyGuestBufferQueue,
|
||||
StringComparison.Ordinal))
|
||||
.First();
|
||||
StringComparison.Ordinal)
|
||||
? 1
|
||||
: 0;
|
||||
if (candidatePriority > allocationPriority)
|
||||
{
|
||||
allocation = candidate;
|
||||
allocationPriority = candidatePriority;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocation is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"no Vulkan guest buffer allocation covers " +
|
||||
$"0x{guestBuffer.BaseAddress:X16}-0x{endAddress:X16}");
|
||||
}
|
||||
|
||||
var guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress;
|
||||
var descriptorOffset = guestOffset &
|
||||
~(GuestStorageBufferOffsetAlignment - 1);
|
||||
@@ -7482,16 +7620,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||
}
|
||||
var live = new byte[guestBuffer.Length];
|
||||
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, live) == true)
|
||||
var mapped = new Span<byte>(
|
||||
(void*)(allocation.Mapped + checked((nint)guestOffset)),
|
||||
guestBuffer.Length);
|
||||
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, mapped) != true)
|
||||
{
|
||||
source = live;
|
||||
source.CopyTo(mapped);
|
||||
}
|
||||
|
||||
source.CopyTo(new Span<byte>(
|
||||
(void*)(allocation.Mapped + checked((nint)guestOffset)),
|
||||
source.Length));
|
||||
source.CopyTo(shadow);
|
||||
mapped.CopyTo(shadow);
|
||||
}
|
||||
|
||||
if (ShouldTraceVulkanResources() &&
|
||||
@@ -7501,14 +7638,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
||||
$"bytes={guestBuffer.Length}");
|
||||
}
|
||||
if (_setDebugUtilsObjectName is not null)
|
||||
{
|
||||
SetDebugName(
|
||||
ObjectType.Buffer,
|
||||
allocation.Buffer.Handle,
|
||||
$"SharpEmu global 0x{guestBuffer.BaseAddress:X16} {guestBuffer.Length}b");
|
||||
}
|
||||
|
||||
if (guestBuffer.Pooled)
|
||||
{
|
||||
GuestDataPool.Return(guestBuffer.Data);
|
||||
@@ -7536,8 +7665,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var buffer = CreateHostBuffer(
|
||||
guestBuffer.Data.AsSpan(0, guestBuffer.Length),
|
||||
BufferUsageFlags.StorageBufferBit,
|
||||
out var memory);
|
||||
var allocation = _hostBufferAllocations[buffer.Handle];
|
||||
out var memory,
|
||||
out var mapped);
|
||||
if (guestBuffer.Pooled)
|
||||
{
|
||||
GuestDataPool.Return(guestBuffer.Data);
|
||||
@@ -7550,7 +7679,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
WriteBackToGuest = false,
|
||||
Buffer = buffer,
|
||||
Memory = memory,
|
||||
Mapped = allocation.Mapped,
|
||||
Mapped = mapped,
|
||||
Offset = 0,
|
||||
Size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)),
|
||||
GuestOffset = 0,
|
||||
@@ -7770,7 +7899,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var buffer = CreateHostBuffer(
|
||||
source,
|
||||
BufferUsageFlags.VertexBufferBit,
|
||||
out var memory);
|
||||
out var memory,
|
||||
out _);
|
||||
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
||||
if (_setDebugUtilsObjectName is not null)
|
||||
{
|
||||
@@ -7839,19 +7969,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private VkBuffer CreateHostBuffer(
|
||||
ReadOnlySpan<byte> data,
|
||||
BufferUsageFlags usage,
|
||||
out DeviceMemory memory)
|
||||
out DeviceMemory memory,
|
||||
out nint mapped)
|
||||
{
|
||||
var size = (ulong)Math.Max(data.Length, sizeof(uint));
|
||||
var capacity = BitOperations.RoundUpToPowerOf2(size);
|
||||
var key = new HostBufferPoolKey(usage, capacity);
|
||||
if (!_hostBufferPool.TryGetValue(key, out var available))
|
||||
{
|
||||
available = new Stack<HostBufferAllocation>();
|
||||
_hostBufferPool.Add(key, available);
|
||||
}
|
||||
var key = new VulkanHostBufferPoolKey(usage, capacity);
|
||||
|
||||
HostBufferAllocation allocation;
|
||||
if (available.TryPop(out var pooled))
|
||||
VulkanHostBufferAllocation allocation;
|
||||
if (_hostBufferPool.TryRent(key, out var pooled))
|
||||
{
|
||||
allocation = pooled;
|
||||
}
|
||||
@@ -7869,15 +7995,16 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(
|
||||
_vk.MapMemory(_device, allocatedMemory, 0, capacity, 0, &persistentMapping),
|
||||
"vkMapMemory(host persistent)");
|
||||
allocation = new HostBufferAllocation(
|
||||
allocation = new VulkanHostBufferAllocation(
|
||||
buffer,
|
||||
allocatedMemory,
|
||||
key,
|
||||
(nint)persistentMapping);
|
||||
_hostBufferAllocations.Add(buffer.Handle, allocation);
|
||||
_hostBufferPool.Register(allocation);
|
||||
}
|
||||
|
||||
memory = allocation.Memory;
|
||||
mapped = allocation.Mapped;
|
||||
fixed (byte* source = data)
|
||||
{
|
||||
System.Buffer.MemoryCopy(
|
||||
@@ -7897,10 +8024,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (_hostBufferAllocations.TryGetValue(buffer.Handle, out var allocation) &&
|
||||
allocation.Memory.Handle == memory.Handle)
|
||||
if (_hostBufferPool.Return(buffer, memory))
|
||||
{
|
||||
_hostBufferPool[allocation.Key].Push(allocation);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7911,6 +8036,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyHostBufferAllocation(VulkanHostBufferAllocation allocation)
|
||||
{
|
||||
_vk.UnmapMemory(_device, allocation.Memory);
|
||||
_vk.DestroyBuffer(_device, allocation.Buffer, null);
|
||||
_vk.FreeMemory(_device, allocation.Memory, null);
|
||||
}
|
||||
|
||||
private static PrimitiveTopology GetPrimitiveTopology(uint primitiveType) =>
|
||||
primitiveType switch
|
||||
{
|
||||
@@ -8294,7 +8426,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,
|
||||
@@ -9333,6 +9465,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
|
||||
var draw = work.Draw;
|
||||
var clearDepthForDraw = draw.RenderState.Depth.ClearEnable;
|
||||
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
|
||||
{
|
||||
draw = draw with
|
||||
@@ -9345,9 +9478,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
GuestDepthResource? depth = null;
|
||||
DepthFramebufferResource? depthFramebuffer = null;
|
||||
if (work.DepthTarget is { } depthTarget &&
|
||||
(draw.RenderState.Depth.TestEnable ||
|
||||
draw.RenderState.Depth.WriteEnable))
|
||||
if (ShouldAttachGuestDepth(
|
||||
work.DepthTarget,
|
||||
draw.RenderState.Depth) &&
|
||||
work.DepthTarget is { } depthTarget)
|
||||
{
|
||||
var effectiveDepthTarget = depthTarget;
|
||||
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
|
||||
@@ -9394,21 +9528,45 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
depth = GetOrCreateGuestDepth(effectiveDepthTarget);
|
||||
PrepareFirstUseDepth(depth, draw.RenderState.Depth);
|
||||
if (clearDepthForDraw)
|
||||
{
|
||||
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
|
||||
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
|
||||
}
|
||||
if (targets.Length == 1)
|
||||
{
|
||||
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
|
||||
}
|
||||
}
|
||||
|
||||
if (clearDepthForDraw)
|
||||
{
|
||||
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
|
||||
// clear operation. The draw still produces color, but its
|
||||
// interpolated vertex Z is not the guest clear value.
|
||||
draw = draw with
|
||||
{
|
||||
RenderState = draw.RenderState with
|
||||
{
|
||||
Depth = draw.RenderState.Depth with
|
||||
{
|
||||
TestEnable = false,
|
||||
WriteEnable = false,
|
||||
ClearEnable = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
var renderPass = depthFramebuffer is null
|
||||
? firstTarget.Initialized
|
||||
? firstTarget.RenderPass
|
||||
: firstTarget.InitialRenderPass
|
||||
: firstTarget.Initialized
|
||||
? depth!.Initialized
|
||||
? depth!.Initialized && !clearDepthForDraw
|
||||
? depthFramebuffer.LoadRenderPass
|
||||
: depthFramebuffer.DepthClearRenderPass
|
||||
: depth!.Initialized
|
||||
: depth!.Initialized && !clearDepthForDraw
|
||||
? depthFramebuffer.ColorClearRenderPass
|
||||
: depthFramebuffer.BothClearRenderPass;
|
||||
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
|
||||
@@ -9424,7 +9582,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
targets.Select(target =>
|
||||
target.Initialized || target.InitialUploadPending).ToArray(),
|
||||
depth,
|
||||
depth?.Initialized == true);
|
||||
depth?.Initialized == true && !clearDepthForDraw);
|
||||
transientRenderPass = renderPass;
|
||||
transientFramebuffer = framebuffer;
|
||||
}
|
||||
@@ -9612,7 +9770,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (depth is not null)
|
||||
{
|
||||
depth.Initialized = true;
|
||||
if (draw.RenderState.Depth.WriteEnable)
|
||||
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||
if (clearDepthForDraw)
|
||||
{
|
||||
depth.InitializationSource = "guest-depth-clear";
|
||||
}
|
||||
else if (draw.RenderState.Depth.WriteEnable)
|
||||
{
|
||||
depth.InitializationSource = "translated-depth-write";
|
||||
}
|
||||
@@ -9746,10 +9909,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
}
|
||||
TraceVulkanShader(
|
||||
$"vk.offscreen_draw mrt={targets.Length} " +
|
||||
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
||||
$"textures={work.Draw.Textures.Count}");
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.offscreen_draw mrt={targets.Length} " +
|
||||
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
||||
$"textures={work.Draw.Textures.Count}");
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -10164,6 +10330,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
|
||||
{
|
||||
@@ -10931,7 +11100,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)
|
||||
{
|
||||
@@ -10997,6 +11166,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))
|
||||
{
|
||||
@@ -11010,6 +11196,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;
|
||||
@@ -13592,13 +13790,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
DestroyGuestBufferAllocation(allocation);
|
||||
}
|
||||
_guestBufferAllocations.Clear();
|
||||
foreach (var allocation in _hostBufferAllocations.Values)
|
||||
{
|
||||
_vk.DestroyBuffer(_device, allocation.Buffer, null);
|
||||
_vk.FreeMemory(_device, allocation.Memory, null);
|
||||
}
|
||||
_hostBufferAllocations.Clear();
|
||||
_hostBufferPool.Clear();
|
||||
_hostBufferPool.Dispose();
|
||||
foreach (var guestImage in _guestImages.Values)
|
||||
{
|
||||
DestroyGuestImage(guestImage);
|
||||
@@ -13830,20 +14022,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;
|
||||
}
|
||||
|
||||
@@ -1802,24 +1802,6 @@ public static partial class Gen5SpirvTranslator
|
||||
|
||||
switch (instruction.Opcode)
|
||||
{
|
||||
case "DsAddU32":
|
||||
{
|
||||
if (instruction.Sources.Count < 2)
|
||||
{
|
||||
error = "missing LDS atomic-add source";
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = GetRawSource(instruction, 0);
|
||||
_module.AddInstruction(
|
||||
SpirvOp.AtomicIAdd,
|
||||
_uintType,
|
||||
LdsPointer(address, control.Offset0),
|
||||
UInt(2), // Workgroup scope.
|
||||
UInt(0x108), // AcquireRelease | WorkgroupMemory.
|
||||
GetRawSource(instruction, 1));
|
||||
return true;
|
||||
}
|
||||
case "DsWriteB32":
|
||||
{
|
||||
if (instruction.Sources.Count < 2)
|
||||
@@ -1964,6 +1946,11 @@ public static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
if (Gen5ShaderTranslator.IsDataShareAtomic(instruction.Opcode))
|
||||
{
|
||||
return TryEmitDataShareAtomic(instruction, control, out error);
|
||||
}
|
||||
|
||||
error = $"unsupported LDS opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
@@ -2004,6 +1991,126 @@ public static partial class Gen5SpirvTranslator
|
||||
Store(pointer, selected);
|
||||
}
|
||||
|
||||
private bool TryEmitDataShareAtomic(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5DataShareControl control,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var atomicOp = instruction.Opcode switch
|
||||
{
|
||||
"DsAddU32" or "DsAddRtnU32" => SpirvOp.AtomicIAdd,
|
||||
"DsSubU32" or "DsSubRtnU32" => SpirvOp.AtomicISub,
|
||||
"DsIncU32" or "DsIncRtnU32" => SpirvOp.AtomicIIncrement,
|
||||
"DsDecU32" or "DsDecRtnU32" => SpirvOp.AtomicIDecrement,
|
||||
"DsMinI32" or "DsMinRtnI32" => SpirvOp.AtomicSMin,
|
||||
"DsMaxI32" or "DsMaxRtnI32" => SpirvOp.AtomicSMax,
|
||||
"DsMinU32" or "DsMinRtnU32" => SpirvOp.AtomicUMin,
|
||||
"DsMaxU32" or "DsMaxRtnU32" => SpirvOp.AtomicUMax,
|
||||
"DsAndB32" or "DsAndRtnB32" => SpirvOp.AtomicAnd,
|
||||
"DsOrB32" or "DsOrRtnB32" => SpirvOp.AtomicOr,
|
||||
"DsXorB32" or "DsXorRtnB32" => SpirvOp.AtomicXor,
|
||||
"DsWrxchgRtnB32" => SpirvOp.AtomicExchange,
|
||||
"DsCmpstB32" or "DsCmpstRtnB32" => SpirvOp.AtomicCompareExchange,
|
||||
_ => SpirvOp.Nop,
|
||||
};
|
||||
if (atomicOp == SpirvOp.Nop)
|
||||
{
|
||||
error = $"unsupported LDS opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = GetRawSource(instruction, 0);
|
||||
var pointer = LdsPointer(address, control.Offset0);
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var original = EmitAtomic(
|
||||
atomicOp,
|
||||
_uintType,
|
||||
pointer,
|
||||
scope: 2,
|
||||
semantics: 0x108,
|
||||
// DS_CMPST sources: DATA0 is the comparator, DATA1 the new value.
|
||||
value: () => GetRawSource(
|
||||
instruction,
|
||||
atomicOp == SpirvOp.AtomicCompareExchange ? 2 : 1),
|
||||
comparator: () => GetRawSource(instruction, 1));
|
||||
if (instruction.Destinations.Count > 0)
|
||||
{
|
||||
StoreV(instruction.Destinations[0].Value, original);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maps the AMD atomic-op name suffix shared by buffer/image atomics to a SPIR-V opcode.
|
||||
// Inc/Dec approximate the AMD wrap-clamp semantics (MEM = tmp >= DATA ? 0 : tmp + 1),
|
||||
// which is exact for the common 0xFFFFFFFF clamp operand.
|
||||
private static bool TryGetAtomicOp(string name, out SpirvOp op)
|
||||
{
|
||||
op = name switch
|
||||
{
|
||||
"Swap" => SpirvOp.AtomicExchange,
|
||||
"Cmpswap" => SpirvOp.AtomicCompareExchange,
|
||||
"Add" => SpirvOp.AtomicIAdd,
|
||||
"Sub" => SpirvOp.AtomicISub,
|
||||
"Smin" => SpirvOp.AtomicSMin,
|
||||
"Umin" => SpirvOp.AtomicUMin,
|
||||
"Smax" => SpirvOp.AtomicSMax,
|
||||
"Umax" => SpirvOp.AtomicUMax,
|
||||
"And" => SpirvOp.AtomicAnd,
|
||||
"Or" => SpirvOp.AtomicOr,
|
||||
"Xor" => SpirvOp.AtomicXor,
|
||||
"Inc" => SpirvOp.AtomicIIncrement,
|
||||
"Dec" => SpirvOp.AtomicIDecrement,
|
||||
_ => SpirvOp.Nop,
|
||||
};
|
||||
return op != SpirvOp.Nop;
|
||||
}
|
||||
|
||||
private uint EmitAtomic(
|
||||
SpirvOp op,
|
||||
uint type,
|
||||
uint pointer,
|
||||
uint scope,
|
||||
uint semantics,
|
||||
Func<uint> value,
|
||||
Func<uint> comparator)
|
||||
{
|
||||
if (op is SpirvOp.AtomicIIncrement or SpirvOp.AtomicIDecrement)
|
||||
{
|
||||
return _module.AddInstruction(
|
||||
op,
|
||||
type,
|
||||
pointer,
|
||||
UInt(scope),
|
||||
UInt(semantics));
|
||||
}
|
||||
|
||||
if (op == SpirvOp.AtomicCompareExchange)
|
||||
{
|
||||
// The unequal semantics must not contain Release; downgrade it to Acquire.
|
||||
return _module.AddInstruction(
|
||||
op,
|
||||
type,
|
||||
pointer,
|
||||
UInt(scope),
|
||||
UInt(semantics),
|
||||
UInt((semantics & ~0x8u) | 0x2u),
|
||||
value(),
|
||||
comparator());
|
||||
}
|
||||
|
||||
return _module.AddInstruction(
|
||||
op,
|
||||
type,
|
||||
pointer,
|
||||
UInt(scope),
|
||||
UInt(semantics),
|
||||
value());
|
||||
}
|
||||
|
||||
private bool TryEmitInterpolation(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5InterpolationControl interpolation,
|
||||
@@ -2239,22 +2346,27 @@ public static partial class Gen5SpirvTranslator
|
||||
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
|
||||
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
|
||||
|
||||
if (instruction.Opcode is "BufferAtomicAdd" or "BufferAtomicUMax")
|
||||
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
|
||||
{
|
||||
if (!TryGetAtomicOp(instruction.Opcode["BufferAtomic".Length..], out var atomicOp))
|
||||
{
|
||||
error = $"unsupported buffer opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var inRange = IsBufferWordInRange(bindingIndex, dwordAddress);
|
||||
EmitConditional(inRange, () =>
|
||||
{
|
||||
var original = _module.AddInstruction(
|
||||
instruction.Opcode == "BufferAtomicAdd"
|
||||
? SpirvOp.AtomicIAdd
|
||||
: SpirvOp.AtomicUMax,
|
||||
var original = EmitAtomic(
|
||||
atomicOp,
|
||||
_uintType,
|
||||
BufferWordPointer(bindingIndex, dwordAddress),
|
||||
UInt(1),
|
||||
UInt(0x48),
|
||||
LoadV(control.VectorData));
|
||||
scope: 1,
|
||||
semantics: 0x48,
|
||||
value: () => LoadV(control.VectorData),
|
||||
comparator: () => LoadV(control.VectorData + 1));
|
||||
if (control.Glc)
|
||||
{
|
||||
StoreV(control.VectorData, original);
|
||||
@@ -3196,6 +3308,60 @@ public static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("ImageAtomic", StringComparison.Ordinal))
|
||||
{
|
||||
if (!resource.IsStorage)
|
||||
{
|
||||
error = "image atomic is not bound as storage";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.ComponentKind == ImageComponentKind.Float ||
|
||||
!TryGetAtomicOp(instruction.Opcode["ImageAtomic".Length..], out var atomicOp))
|
||||
{
|
||||
error = $"unsupported storage image opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var signed = resource.ComponentKind == ImageComponentKind.Sint;
|
||||
var atomicImageSize = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySize,
|
||||
_module.TypeVector(_intType, 2),
|
||||
imageObject);
|
||||
var coordinates = BuildClampedIntegerCoordinates(
|
||||
image,
|
||||
0,
|
||||
atomicImageSize);
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var pointer = _module.AddInstruction(
|
||||
SpirvOp.ImageTexelPointer,
|
||||
_module.TypePointer(SpirvStorageClass.Image, resource.ComponentType),
|
||||
resource.Variable,
|
||||
coordinates,
|
||||
UInt(0));
|
||||
uint LoadData(uint register) => signed
|
||||
? Bitcast(_intType, LoadV(register))
|
||||
: LoadV(register);
|
||||
var original = EmitAtomic(
|
||||
atomicOp,
|
||||
resource.ComponentType,
|
||||
pointer,
|
||||
scope: 1,
|
||||
semantics: 0x808,
|
||||
value: () => LoadData(image.VectorData),
|
||||
comparator: () => LoadData(image.VectorData + 1));
|
||||
if (image.Glc)
|
||||
{
|
||||
StoreV(
|
||||
image.VectorData,
|
||||
signed ? Bitcast(_uintType, original) : original);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resource.IsStorage &&
|
||||
instruction.Opcode is not ("ImageLoad" or "ImageLoadMip"))
|
||||
{
|
||||
|
||||
@@ -5,36 +5,6 @@ namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
public static class SpirvFixedShaders
|
||||
{
|
||||
/// <summary>Fragment half of the fixed fullscreen barycentric diagnostic draw.</summary>
|
||||
public static byte[] CreateBarycentricFragment()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
var voidType = module.TypeVoid();
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputPointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||
var outputPointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
var barycentric = module.AddGlobalVariable(inputPointer, SpirvStorageClass.Input);
|
||||
module.AddName(barycentric, "barycentric");
|
||||
module.AddDecoration(barycentric, SpirvDecoration.Location, 0);
|
||||
module.AddDecoration(barycentric, SpirvDecoration.NoPerspective);
|
||||
var output = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
|
||||
module.AddName(output, "outColor");
|
||||
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
var value = module.AddInstruction(SpirvOp.Load, vec4Type, barycentric);
|
||||
module.AddStatement(SpirvOp.Store, output, value);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
module.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", [barycentric, output]);
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
|
||||
@@ -40,6 +40,7 @@ public enum SpirvOp : ushort
|
||||
FunctionEnd = 56,
|
||||
FunctionCall = 57,
|
||||
Variable = 59,
|
||||
ImageTexelPointer = 60,
|
||||
Load = 61,
|
||||
Store = 62,
|
||||
AccessChain = 65,
|
||||
@@ -142,8 +143,19 @@ public enum SpirvOp : ushort
|
||||
BitCount = 205,
|
||||
ControlBarrier = 224,
|
||||
MemoryBarrier = 225,
|
||||
AtomicExchange = 229,
|
||||
AtomicCompareExchange = 230,
|
||||
AtomicIIncrement = 232,
|
||||
AtomicIDecrement = 233,
|
||||
AtomicIAdd = 234,
|
||||
AtomicISub = 235,
|
||||
AtomicSMin = 236,
|
||||
AtomicUMin = 237,
|
||||
AtomicSMax = 238,
|
||||
AtomicUMax = 239,
|
||||
AtomicAnd = 240,
|
||||
AtomicOr = 241,
|
||||
AtomicXor = 242,
|
||||
Phi = 245,
|
||||
LoopMerge = 246,
|
||||
SelectionMerge = 247,
|
||||
|
||||
@@ -1172,9 +1172,33 @@ public static class Gen5ShaderTranslator
|
||||
name = opcode switch
|
||||
{
|
||||
0x00 => "DsAddU32",
|
||||
0x01 => "DsSubU32",
|
||||
0x03 => "DsIncU32",
|
||||
0x04 => "DsDecU32",
|
||||
0x05 => "DsMinI32",
|
||||
0x06 => "DsMaxI32",
|
||||
0x07 => "DsMinU32",
|
||||
0x08 => "DsMaxU32",
|
||||
0x09 => "DsAndB32",
|
||||
0x0A => "DsOrB32",
|
||||
0x0B => "DsXorB32",
|
||||
0x0D => "DsWriteB32",
|
||||
0x0E => "DsWrite2B32",
|
||||
0x0F => "DsWrite2St64B32",
|
||||
0x10 => "DsCmpstB32",
|
||||
0x20 => "DsAddRtnU32",
|
||||
0x21 => "DsSubRtnU32",
|
||||
0x23 => "DsIncRtnU32",
|
||||
0x24 => "DsDecRtnU32",
|
||||
0x25 => "DsMinRtnI32",
|
||||
0x26 => "DsMaxRtnI32",
|
||||
0x27 => "DsMinRtnU32",
|
||||
0x28 => "DsMaxRtnU32",
|
||||
0x29 => "DsAndRtnB32",
|
||||
0x2A => "DsOrRtnB32",
|
||||
0x2B => "DsXorRtnB32",
|
||||
0x2D => "DsWrxchgRtnB32",
|
||||
0x30 => "DsCmpstRtnB32",
|
||||
0x35 => "DsSwizzleB32",
|
||||
0x36 => "DsReadB32",
|
||||
0x37 => "DsRead2B32",
|
||||
@@ -1272,8 +1296,19 @@ public static class Gen5ShaderTranslator
|
||||
0x23 => "BufferLoadSbyteD16Hi",
|
||||
0x24 => "BufferLoadShortD16",
|
||||
0x25 => "BufferLoadShortD16Hi",
|
||||
0x30 => "BufferAtomicSwap",
|
||||
0x31 => "BufferAtomicCmpswap",
|
||||
0x32 => "BufferAtomicAdd",
|
||||
0x38 => "BufferAtomicUMax",
|
||||
0x33 => "BufferAtomicSub",
|
||||
0x35 => "BufferAtomicSmin",
|
||||
0x36 => "BufferAtomicUmin",
|
||||
0x37 => "BufferAtomicSmax",
|
||||
0x38 => "BufferAtomicUmax",
|
||||
0x39 => "BufferAtomicAnd",
|
||||
0x3A => "BufferAtomicOr",
|
||||
0x3B => "BufferAtomicXor",
|
||||
0x3C => "BufferAtomicInc",
|
||||
0x3D => "BufferAtomicDec",
|
||||
_ => $"MubufRaw{opcode:X2}",
|
||||
};
|
||||
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
|
||||
@@ -1388,7 +1423,18 @@ public static class Gen5ShaderTranslator
|
||||
0x08 => "ImageStore",
|
||||
0x09 => "ImageStoreMip",
|
||||
0x0E => "ImageGetResinfo",
|
||||
0x0F => "ImageAtomicSwap",
|
||||
0x10 => "ImageAtomicCmpswap",
|
||||
0x11 => "ImageAtomicAdd",
|
||||
0x12 => "ImageAtomicSub",
|
||||
0x14 => "ImageAtomicSmin",
|
||||
0x15 => "ImageAtomicUmin",
|
||||
0x16 => "ImageAtomicSmax",
|
||||
0x17 => "ImageAtomicUmax",
|
||||
0x18 => "ImageAtomicAnd",
|
||||
0x19 => "ImageAtomicOr",
|
||||
0x1A => "ImageAtomicXor",
|
||||
0x1B => "ImageAtomicInc",
|
||||
0x1C => "ImageAtomicDec",
|
||||
0x20 => "ImageSample",
|
||||
0x22 => "ImageSampleD",
|
||||
@@ -1488,6 +1534,18 @@ public static class Gen5ShaderTranslator
|
||||
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
|
||||
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
|
||||
|
||||
public static bool IsDataShareAtomic(string name) => name switch
|
||||
{
|
||||
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
|
||||
"DsMinI32" or "DsMaxI32" or "DsMinU32" or "DsMaxU32" or
|
||||
"DsAndB32" or "DsOrB32" or "DsXorB32" or "DsCmpstB32" or
|
||||
"DsAddRtnU32" or "DsSubRtnU32" or "DsIncRtnU32" or "DsDecRtnU32" or
|
||||
"DsMinRtnI32" or "DsMaxRtnI32" or "DsMinRtnU32" or "DsMaxRtnU32" or
|
||||
"DsAndRtnB32" or "DsOrRtnB32" or "DsXorRtnB32" or
|
||||
"DsWrxchgRtnB32" or "DsCmpstRtnB32" => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static Gen5ShaderInstruction CreateInstruction(
|
||||
uint pc,
|
||||
Gen5ShaderEncoding encoding,
|
||||
@@ -1767,9 +1825,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(
|
||||
@@ -1794,10 +1852,6 @@ public static class Gen5ShaderTranslator
|
||||
((word >> 17) & 1) != 0);
|
||||
sources = opcode switch
|
||||
{
|
||||
"DsAddU32" => [
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorData0),
|
||||
],
|
||||
"DsWriteB32" => [
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorData0),
|
||||
@@ -1826,6 +1880,17 @@ public static class Gen5ShaderTranslator
|
||||
Gen5Operand.Vector(vectorData1),
|
||||
],
|
||||
"DsSwizzleB32" => [Gen5Operand.Vector(vectorData0)],
|
||||
// DS_CMPST operand order is reversed vs buffer/image cmpswap:
|
||||
// DATA0 holds the comparator, DATA1 holds the new value.
|
||||
"DsCmpstB32" or "DsCmpstRtnB32" => [
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorData0),
|
||||
Gen5Operand.Vector(vectorData1),
|
||||
],
|
||||
_ when IsDataShareAtomic(opcode) => [
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorData0),
|
||||
],
|
||||
_ => [Gen5Operand.Vector(vectorAddress)],
|
||||
};
|
||||
destinations = opcode switch
|
||||
@@ -1848,6 +1913,10 @@ public static class Gen5ShaderTranslator
|
||||
Gen5Operand.Vector(vectorDestination + 2),
|
||||
Gen5Operand.Vector(vectorDestination + 3),
|
||||
],
|
||||
_ when IsDataShareAtomic(opcode) &&
|
||||
opcode.Contains("Rtn", StringComparison.Ordinal) => [
|
||||
Gen5Operand.Vector(vectorDestination),
|
||||
],
|
||||
_ => [],
|
||||
};
|
||||
break;
|
||||
@@ -1953,8 +2022,8 @@ public static class Gen5ShaderTranslator
|
||||
"BufferStoreDwordx2" => 2u,
|
||||
"BufferStoreDwordx3" => 3u,
|
||||
"BufferStoreDwordx4" => 4u,
|
||||
"BufferAtomicAdd" => 1u,
|
||||
"BufferAtomicUMax" => 1u,
|
||||
"BufferAtomicCmpswap" => 2u,
|
||||
_ when opcode.StartsWith("BufferAtomic", StringComparison.Ordinal) => 1u,
|
||||
_ => 0u,
|
||||
};
|
||||
sources =
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Decodes synthetic GFX10 atomic instructions and checks the opcode names and operand wiring
|
||||
// that the SPIR-V translator relies on. Word layouts follow the RDNA2 ISA manual:
|
||||
// MUBUF op=word0[24:18], MIMG op=word0[24:18], DS op=word0[25:18].
|
||||
public sealed class Gen5ShaderAtomicDecodeTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const uint EndPgm = 0xBF810000;
|
||||
|
||||
// Compute-stage register block: COMPUTE_USER_DATA_0 and COMPUTE_PGM_RSRC2,
|
||||
// required since TryCreateState validates the USER_SGPR count.
|
||||
internal const uint ComputeUserDataRegister = 0x240;
|
||||
internal const uint ComputePgmRsrc2Register = 0x213;
|
||||
|
||||
[Fact]
|
||||
public void BufferAtomicUmax_DecodesControlAndDestination()
|
||||
{
|
||||
// BUFFER_ATOMIC_UMAX v1, off, s[0:3], 128 offset:8 glc
|
||||
var instruction = DecodeSingle(0xE0E04008, 0x80000100);
|
||||
|
||||
Assert.Equal("BufferAtomicUmax", instruction.Opcode);
|
||||
var control = Assert.IsType<Gen5BufferMemoryControl>(instruction.Control);
|
||||
Assert.Equal(1u, control.DwordCount);
|
||||
Assert.Equal(1u, control.VectorData);
|
||||
Assert.Equal(0u, control.ScalarResource);
|
||||
Assert.Equal(8, control.OffsetBytes);
|
||||
Assert.True(control.Glc);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(1) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BufferAtomicCmpswap_UsesTwoDataRegisters()
|
||||
{
|
||||
// BUFFER_ATOMIC_CMPSWAP v[1:2], off, s[0:3], 128 glc
|
||||
var instruction = DecodeSingle(0xE0C44000, 0x80000100);
|
||||
|
||||
Assert.Equal("BufferAtomicCmpswap", instruction.Opcode);
|
||||
var control = Assert.IsType<Gen5BufferMemoryControl>(instruction.Control);
|
||||
Assert.Equal(2u, control.DwordCount);
|
||||
Assert.Equal(1u, control.VectorData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageAtomicAdd_KeepsDataRegisterAsDestination()
|
||||
{
|
||||
// IMAGE_ATOMIC_ADD v2, v[0:1], s[4:11] dmask:0x1 dim:2D glc
|
||||
var instruction = DecodeSingle(0xF0442100, 0x00010200);
|
||||
|
||||
Assert.Equal("ImageAtomicAdd", instruction.Opcode);
|
||||
var control = Assert.IsType<Gen5ImageControl>(instruction.Control);
|
||||
Assert.Equal(2u, control.VectorData);
|
||||
Assert.Equal(4u, control.ScalarResource);
|
||||
Assert.True(control.Glc);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(2) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DsAddU32_HasAddressAndDataSourcesButNoDestination()
|
||||
{
|
||||
// DS_ADD_U32 v0, v1
|
||||
var instruction = DecodeSingle(0xD8000000, 0x00000100);
|
||||
|
||||
Assert.Equal("DsAddU32", instruction.Opcode);
|
||||
Assert.Equal(
|
||||
new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(1) },
|
||||
instruction.Sources);
|
||||
Assert.Empty(instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DsAddRtnU32_WritesReturnRegister()
|
||||
{
|
||||
// DS_ADD_RTN_U32 v3, v0, v1
|
||||
var instruction = DecodeSingle(0xD8800000, 0x03000100);
|
||||
|
||||
Assert.Equal("DsAddRtnU32", instruction.Opcode);
|
||||
Assert.Equal(
|
||||
new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(1) },
|
||||
instruction.Sources);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(3) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DsCmpstRtnB32_OrdersComparatorBeforeNewValue()
|
||||
{
|
||||
// DS_CMPST_RTN_B32 v3, v0, v1, v2 - DATA0 (v1) is the comparator, DATA1 (v2) the
|
||||
// new value, reversed relative to buffer/image cmpswap.
|
||||
var instruction = DecodeSingle(0xD8C00000, 0x03020100);
|
||||
|
||||
Assert.Equal("DsCmpstRtnB32", instruction.Opcode);
|
||||
Assert.Equal(
|
||||
new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(1), Gen5Operand.Vector(2) },
|
||||
instruction.Sources);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(3) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
private static Gen5ShaderInstruction DecodeSingle(params uint[] words)
|
||||
{
|
||||
var memory = new FakeCpuMemory(ShaderAddress, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
WriteProgram(memory, ShaderAddress, words);
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryCreateState(
|
||||
ctx,
|
||||
ShaderAddress,
|
||||
0,
|
||||
new Dictionary<uint, uint> { [ComputePgmRsrc2Register] = 0 },
|
||||
ComputeUserDataRegister,
|
||||
out var state,
|
||||
out var error),
|
||||
error);
|
||||
return state.Program.Instructions[0];
|
||||
}
|
||||
|
||||
internal static void WriteProgram(FakeCpuMemory memory, ulong address, uint[] words)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
foreach (var word in words.Append(EndPgm))
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, word);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
address += sizeof(uint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// End-to-end pipeline tests: synthetic GFX10 program -> decode -> scalar evaluation -> SPIR-V.
|
||||
// Each test asserts the expected OpAtomic* instructions land in the emitted module.
|
||||
public sealed class Gen5SpirvAtomicTranslationTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const ulong BufferAddress = 0x1_0000_1000;
|
||||
|
||||
[Fact]
|
||||
public void BufferAtomics_EmitAtomicOpcodes()
|
||||
{
|
||||
// BUFFER_ATOMIC_UMAX v1, BUFFER_ATOMIC_CMPSWAP v[1:2], BUFFER_ATOMIC_INC v1,
|
||||
// all against the V# in s[0:3].
|
||||
var opcodes = CompileCompute(
|
||||
[
|
||||
0xE0E04008, 0x80000100,
|
||||
0xE0C44000, 0x80000100,
|
||||
0xE0F00000, 0x80000100,
|
||||
],
|
||||
BufferDescriptorRegisters());
|
||||
|
||||
Assert.Contains((ushort)SpirvOp.AtomicUMax, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.AtomicCompareExchange, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.AtomicIIncrement, opcodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataShareAtomics_EmitAtomicOpcodes()
|
||||
{
|
||||
// DS_ADD_RTN_U32 v3, v0, v1; DS_CMPST_RTN_B32 v3, v0, v1, v2; DS_MAX_U32 v0, v1.
|
||||
var opcodes = CompileCompute(
|
||||
[
|
||||
0xD8800000, 0x03000100,
|
||||
0xD8C00000, 0x03020100,
|
||||
0xD8200000, 0x00000100,
|
||||
],
|
||||
new Dictionary<uint, uint>());
|
||||
|
||||
Assert.Contains((ushort)SpirvOp.AtomicIAdd, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.AtomicCompareExchange, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.AtomicUMax, opcodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageAtomicAdd_EmitsTexelPointerAndAtomicAdd()
|
||||
{
|
||||
// IMAGE_ATOMIC_ADD v2, v[0:1], s[4:11] dmask:0x1 dim:2D glc against an R32ui T#.
|
||||
var opcodes = CompileCompute(
|
||||
[0xF0442100, 0x00010200],
|
||||
new Dictionary<uint, uint>
|
||||
{
|
||||
// Descriptor word1 dataFormat (bits 28:20) = 20 selects R32ui/Uint.
|
||||
[5] = 20u << 20,
|
||||
});
|
||||
|
||||
Assert.Contains((ushort)SpirvOp.ImageTexelPointer, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.AtomicIAdd, opcodes);
|
||||
}
|
||||
|
||||
private static Dictionary<uint, uint> BufferDescriptorRegisters() => new()
|
||||
{
|
||||
// V# in s[0:3]: base=BufferAddress, stride=0, numRecords=64 bytes, type=0.
|
||||
[0] = unchecked((uint)BufferAddress),
|
||||
[1] = (uint)(BufferAddress >> 32),
|
||||
[2] = 64,
|
||||
[3] = 0,
|
||||
};
|
||||
|
||||
private static HashSet<ushort> CompileCompute(
|
||||
uint[] programWords,
|
||||
Dictionary<uint, uint> userDataSgprs)
|
||||
{
|
||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
||||
// COMPUTE_PGM_RSRC2 advertises 16 user SGPRs; the user data words at
|
||||
// COMPUTE_USER_DATA_0 + index seed s[0..15] for the scalar evaluator.
|
||||
var shaderRegisters = new Dictionary<uint, uint>
|
||||
{
|
||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
||||
};
|
||||
foreach (var (sgpr, value) in userDataSgprs)
|
||||
{
|
||||
shaderRegisters[Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister + sgpr] = value;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryCreateState(
|
||||
ctx,
|
||||
ShaderAddress,
|
||||
0,
|
||||
shaderRegisters,
|
||||
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
|
||||
out var state,
|
||||
out var error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
out var shader,
|
||||
out error),
|
||||
error);
|
||||
return CollectOpcodes(shader.Spirv);
|
||||
}
|
||||
|
||||
private static HashSet<ushort> CollectOpcodes(byte[] spirv)
|
||||
{
|
||||
var opcodes = new HashSet<ushort>();
|
||||
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
||||
{
|
||||
var word = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
spirv.AsSpan(offset, sizeof(uint)));
|
||||
opcodes.Add((ushort)word);
|
||||
offset += Math.Max((int)(word >> 16), 1) * sizeof(uint);
|
||||
}
|
||||
|
||||
return opcodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
public sealed class AprStreamingContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveStatAndReadFile_UsesSharedAprFileId()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong pathListAddress = memoryBase + 0x100;
|
||||
const ulong pathAddress = memoryBase + 0x200;
|
||||
const ulong idsAddress = memoryBase + 0x800;
|
||||
const ulong statAddress = memoryBase + 0x900;
|
||||
const ulong commandBufferAddress = memoryBase + 0x1000;
|
||||
const ulong recordBufferAddress = memoryBase + 0x1100;
|
||||
const ulong destinationAddress = memoryBase + 0x2000;
|
||||
const ulong stackAddress = memoryBase + 0x3000;
|
||||
byte[] fileContents = [10, 11, 12, 13, 14, 15, 16, 17];
|
||||
var hostPath = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(hostPath, fileContents);
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(pathAddress, hostPath);
|
||||
WriteUInt64(memory, pathListAddress, pathAddress);
|
||||
|
||||
context[CpuRegister.Rdi] = pathListAddress;
|
||||
context[CpuRegister.Rsi] = 1;
|
||||
context[CpuRegister.Rdx] = idsAddress;
|
||||
|
||||
Assert.Equal(0, KernelMemoryCompatExports.KernelAprResolveFilepathsToIds(context));
|
||||
|
||||
Span<byte> idBytes = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(idsAddress, idBytes));
|
||||
var fileId = BinaryPrimitives.ReadUInt32LittleEndian(idBytes);
|
||||
Assert.NotEqual(uint.MaxValue, fileId);
|
||||
|
||||
context[CpuRegister.Rdi] = fileId;
|
||||
context[CpuRegister.Rsi] = statAddress;
|
||||
|
||||
Assert.Equal(0, KernelMemoryCompatExports.KernelAprGetFileStat(context));
|
||||
|
||||
Span<byte> stat = stackalloc byte[120];
|
||||
Assert.True(memory.TryRead(statAddress, stat));
|
||||
Assert.Equal(fileContents.Length, BinaryPrimitives.ReadInt64LittleEndian(stat[72..]));
|
||||
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = recordBufferAddress;
|
||||
context[CpuRegister.Rdx] = 0x100;
|
||||
|
||||
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
|
||||
|
||||
const ulong readOffset = 2;
|
||||
const ulong readSize = 4;
|
||||
WriteUInt64(memory, stackAddress + sizeof(ulong), readOffset);
|
||||
context[CpuRegister.Rsp] = stackAddress;
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rcx] = fileId;
|
||||
context[CpuRegister.R8] = destinationAddress;
|
||||
context[CpuRegister.R9] = readSize;
|
||||
|
||||
Assert.Equal(0, AmprExports.AprCommandBufferReadFile(context));
|
||||
|
||||
Span<byte> destination = stackalloc byte[(int)readSize];
|
||||
Assert.True(memory.TryRead(destinationAddress, destination));
|
||||
Assert.Equal(fileContents.AsSpan((int)readOffset, (int)readSize), destination);
|
||||
|
||||
Span<byte> record = stackalloc byte[0x30];
|
||||
Assert.True(memory.TryRead(recordBufferAddress, record));
|
||||
Assert.Equal(1U, BinaryPrimitives.ReadUInt32LittleEndian(record));
|
||||
Assert.Equal(fileId, BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
|
||||
Assert.Equal(destinationAddress, BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]));
|
||||
Assert.Equal(readSize, BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]));
|
||||
Assert.Equal(readOffset, BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]));
|
||||
Assert.Equal(readSize, BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(hostPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
// These exercise the pure BMI1/BMI2/ABM semantics used by the illegal-instruction software
|
||||
// fallback. The expected values were computed by hand from the Intel/AMD definitions so a
|
||||
// regression in the bit math or the flag handling fails here without needing a live guest.
|
||||
public sealed class BmiInstructionEmulatorTests
|
||||
{
|
||||
private const uint Carry = 1u << 0;
|
||||
private const uint Zero = 1u << 6;
|
||||
private const uint Sign = 1u << 7;
|
||||
private const uint Overflow = 1u << 11;
|
||||
private const uint DirectionFlag = 1u << 10; // Unrelated flag; must be preserved.
|
||||
|
||||
[Fact]
|
||||
public void Andn_ClearsSourceBitsAndFlags()
|
||||
{
|
||||
uint flags = Overflow | Carry | DirectionFlag;
|
||||
var result = BmiInstructionEmulator.Andn(0x0000_00F0, 0x0000_00FF, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0x0000_000FUL, result);
|
||||
Assert.Equal(0u, flags & Carry);
|
||||
Assert.Equal(0u, flags & Overflow);
|
||||
Assert.Equal(0u, flags & Zero);
|
||||
Assert.Equal(0u, flags & Sign);
|
||||
Assert.Equal(DirectionFlag, flags & DirectionFlag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Andn_SetsZeroFlagWhenResultIsZero()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Andn(0xFF, 0xFF, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0UL, result);
|
||||
Assert.Equal(Zero, flags & Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Andn_SetsSignFlagOnHighBit32()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Andn(0, 0x8000_0000, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0x8000_0000UL, result);
|
||||
Assert.Equal(Sign, flags & Sign);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Andn_MasksTo64Bits()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Andn(0xFFFF_FFFF_FFFF_FF00, 0xFF, GprOperandSize.Bits64, ref flags);
|
||||
|
||||
Assert.Equal(0xFFUL, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0000_000CUL, 0x0000_0004UL, false)] // isolate lowest set bit
|
||||
[InlineData(0x0000_0000UL, 0x0000_0000UL, true)] // src == 0 -> CF clear, ZF set
|
||||
public void Blsi_IsolatesLowestBit(ulong src, ulong expected, bool zero)
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Blsi(src, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
Assert.Equal(src != 0 ? Carry : 0u, flags & Carry);
|
||||
Assert.Equal(zero ? Zero : 0u, flags & Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blsmsk_MasksThroughLowestBit()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Blsmsk(0x0000_000C, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0x0000_0007UL, result);
|
||||
Assert.Equal(0u, flags & Carry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blsmsk_SetsCarryWhenSourceZero32()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Blsmsk(0, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0xFFFF_FFFFUL, result);
|
||||
Assert.Equal(Carry, flags & Carry);
|
||||
Assert.Equal(Sign, flags & Sign);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blsr_ResetsLowestBit()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Blsr(0x0000_000C, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0x0000_0008UL, result);
|
||||
Assert.Equal(0u, flags & Carry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blsr_SetsCarryAndZeroWhenSourceZero()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Blsr(0, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0UL, result);
|
||||
Assert.Equal(Carry, flags & Carry);
|
||||
Assert.Equal(Zero, flags & Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bextr_ExtractsBitField()
|
||||
{
|
||||
uint flags = 0;
|
||||
// start = 4, len = 8 -> bits [11:4] of 0x12345678 == 0x67
|
||||
var control = (8UL << 8) | 4UL;
|
||||
var result = BmiInstructionEmulator.Bextr(0x1234_5678, control, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0x0000_0067UL, result);
|
||||
Assert.Equal(0u, flags & Zero);
|
||||
Assert.Equal(0u, flags & Carry);
|
||||
Assert.Equal(0u, flags & Overflow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bextr_StartBeyondWidthYieldsZero()
|
||||
{
|
||||
uint flags = 0;
|
||||
var control = (8UL << 8) | 40UL; // start = 40 >= 32
|
||||
var result = BmiInstructionEmulator.Bextr(0xFFFF_FFFF, control, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0UL, result);
|
||||
Assert.Equal(Zero, flags & Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bzhi_ZeroesHighBits()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Bzhi(0xFFFF_FFFF, 8, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0x0000_00FFUL, result);
|
||||
Assert.Equal(0u, flags & Carry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bzhi_SetsCarryWhenIndexAtOrBeyondWidth()
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Bzhi(0xFFFF_FFFF, 32, GprOperandSize.Bits32, ref flags);
|
||||
|
||||
Assert.Equal(0xFFFF_FFFFUL, result);
|
||||
Assert.Equal(Carry, flags & Carry);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0000_0008UL, GprOperandSize.Bits32, 3UL, false)]
|
||||
[InlineData(0x0000_0001UL, GprOperandSize.Bits32, 0UL, false)]
|
||||
[InlineData(0x0000_0000UL, GprOperandSize.Bits32, 32UL, true)]
|
||||
[InlineData(0x1_0000_0000UL, GprOperandSize.Bits64, 32UL, false)]
|
||||
public void Tzcnt_CountsTrailingZeros(ulong src, GprOperandSize size, ulong expected, bool carry)
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Tzcnt(src, size, ref flags);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
Assert.Equal(carry ? Carry : 0u, flags & Carry);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0000_0001UL, GprOperandSize.Bits32, 31UL, false)]
|
||||
[InlineData(0x8000_0000UL, GprOperandSize.Bits32, 0UL, false)]
|
||||
[InlineData(0x0000_0000UL, GprOperandSize.Bits32, 32UL, true)]
|
||||
[InlineData(0x0000_0001UL, GprOperandSize.Bits64, 63UL, false)]
|
||||
[InlineData(0x0000_0000UL, GprOperandSize.Bits64, 64UL, true)]
|
||||
public void Lzcnt_CountsLeadingZeros(ulong src, GprOperandSize size, ulong expected, bool carry)
|
||||
{
|
||||
uint flags = 0;
|
||||
var result = BmiInstructionEmulator.Lzcnt(src, size, ref flags);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
Assert.Equal(carry ? Carry : 0u, flags & Carry);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x1234_5678UL, 8, GprOperandSize.Bits32, 0x7812_3456UL)]
|
||||
[InlineData(0x1234_5678UL, 0, GprOperandSize.Bits32, 0x1234_5678UL)]
|
||||
[InlineData(0x0123_4567_89AB_CDEFUL, 4, GprOperandSize.Bits64, 0xF012_3456_789A_BCDEUL)]
|
||||
public void Rorx_RotatesRight(ulong src, int count, GprOperandSize size, ulong expected)
|
||||
{
|
||||
Assert.Equal(expected, BmiInstructionEmulator.Rorx(src, count, size));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x8000_0000UL, 4, GprOperandSize.Bits32, 0xF800_0000UL)]
|
||||
[InlineData(0x8000_0000UL, 36, GprOperandSize.Bits32, 0xF800_0000UL)] // count masked to 4
|
||||
[InlineData(0x8000_0000_0000_0000UL, 4, GprOperandSize.Bits64, 0xF800_0000_0000_0000UL)]
|
||||
public void Sarx_ArithmeticShiftRight(ulong src, int count, GprOperandSize size, ulong expected)
|
||||
{
|
||||
Assert.Equal(expected, BmiInstructionEmulator.Sarx(src, count, size));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0000_0001UL, 4, GprOperandSize.Bits32, 0x0000_0010UL)]
|
||||
[InlineData(0x0000_0001UL, 33, GprOperandSize.Bits32, 0x0000_0002UL)] // count masked to 1
|
||||
public void Shlx_LogicalShiftLeft(ulong src, int count, GprOperandSize size, ulong expected)
|
||||
{
|
||||
Assert.Equal(expected, BmiInstructionEmulator.Shlx(src, count, size));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x8000_0000UL, 4, GprOperandSize.Bits32, 0x0800_0000UL)]
|
||||
[InlineData(0x8000_0000_0000_0000UL, 4, GprOperandSize.Bits64, 0x0800_0000_0000_0000UL)]
|
||||
public void Shrx_LogicalShiftRight(ulong src, int count, GprOperandSize size, ulong expected)
|
||||
{
|
||||
Assert.Equal(expected, BmiInstructionEmulator.Shrx(src, count, size));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PdepAndPext_AreInverseForContiguousSource()
|
||||
{
|
||||
var deposited = BmiInstructionEmulator.Pdep(0x0000_000F, 0x0000_00AA, GprOperandSize.Bits32);
|
||||
Assert.Equal(0x0000_00AAUL, deposited);
|
||||
|
||||
var extracted = BmiInstructionEmulator.Pext(0x0000_00AA, 0x0000_00AA, GprOperandSize.Bits32);
|
||||
Assert.Equal(0x0000_000FUL, extracted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pext_GathersSelectedBits()
|
||||
{
|
||||
// mask selects bit positions 1,3,5,7; source has 1s only at 5 and 7 -> packed 0b1100
|
||||
var extracted = BmiInstructionEmulator.Pext(0xF0, 0xAA, GprOperandSize.Bits32);
|
||||
Assert.Equal(0x0000_000CUL, extracted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class GuestContextTransferValidationTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
|
||||
[Fact]
|
||||
public void MappedGuestInstructionPointer_IsAccepted()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var continuation = CreateContinuation(MemoryBase + 0x100, MemoryBase + 0x800);
|
||||
|
||||
Assert.True(DirectExecutionBackend.TryValidateGuestContextTransferTarget(
|
||||
memory,
|
||||
continuation,
|
||||
out var error));
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnmappedGuestInstructionPointer_IsRejected()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var continuation = CreateContinuation(MemoryBase + 0x2000, MemoryBase + 0x800);
|
||||
|
||||
Assert.False(DirectExecutionBackend.TryValidateGuestContextTransferTarget(
|
||||
memory,
|
||||
continuation,
|
||||
out var error));
|
||||
Assert.Contains("not mapped guest memory", error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0UL, MemoryBase + 0x800)]
|
||||
[InlineData(MemoryBase + 0x100, 0UL)]
|
||||
public void InvalidInstructionOrStackPointer_IsRejected(ulong rip, ulong rsp)
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var continuation = CreateContinuation(rip, rsp);
|
||||
|
||||
Assert.False(DirectExecutionBackend.TryValidateGuestContextTransferTarget(
|
||||
memory,
|
||||
continuation,
|
||||
out var error));
|
||||
Assert.Contains("invalid guest context transfer target", error);
|
||||
}
|
||||
|
||||
private static GuestCpuContinuation CreateContinuation(ulong rip, ulong rsp) =>
|
||||
new()
|
||||
{
|
||||
Rip = rip,
|
||||
Rsp = rsp,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class ImportTrampolineAbiTests
|
||||
{
|
||||
[Fact]
|
||||
public unsafe void GeneratedTrampoline_PreservesVolatileGuestState()
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
// The production backend emits and initializes x86-64 callback
|
||||
// thunks, so inspect it only in an x64 test process. Apple Silicon
|
||||
// runs this path through the same Rosetta environment as SharpEmu.
|
||||
return;
|
||||
}
|
||||
|
||||
var code = CreateTrampolineBytes();
|
||||
|
||||
AssertContains(code, [0x48, 0x81, 0xEC, 0xB0, 0x00, 0x00, 0x00]); // sub rsp,0xB0
|
||||
AssertContains(code, [0x48, 0x89, 0x04, 0x24]); // mov [rsp],rax
|
||||
AssertContains(code, [0x4C, 0x89, 0x54, 0x24, 0x08]); // mov [rsp+8],r10
|
||||
AssertContains(code, [0x4C, 0x89, 0x5C, 0x24, 0x10]); // mov [rsp+16],r11
|
||||
AssertContains(code, [0x0F, 0xAE, 0x5C, 0x24, 0x18]); // stmxcsr [rsp+24]
|
||||
AssertContains(code, [0xD9, 0x7C, 0x24, 0x1C]); // fnstcw [rsp+28]
|
||||
AssertContains(code, [0x4C, 0x8D, 0xA4, 0x24, 0xB0, 0, 0, 0]); // lea r12,[rsp+0xB0]
|
||||
|
||||
for (var xmm = 0; xmm < 8; xmm++)
|
||||
{
|
||||
Span<byte> save = new byte[9];
|
||||
save[0] = 0xF3;
|
||||
save[1] = 0x0F;
|
||||
save[2] = 0x7F;
|
||||
save[3] = (byte)(0x84 | (xmm << 3));
|
||||
save[4] = 0x24;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(save[5..], 0x30 + (xmm * 0x10));
|
||||
AssertContains(code, save);
|
||||
}
|
||||
|
||||
for (var xmm = 0; xmm < 2; xmm++)
|
||||
{
|
||||
Span<byte> restore = new byte[10];
|
||||
restore[0] = 0xF3;
|
||||
restore[1] = 0x41;
|
||||
restore[2] = 0x0F;
|
||||
restore[3] = 0x6F;
|
||||
restore[4] = (byte)(0x84 | (xmm << 3));
|
||||
restore[5] = 0x24;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(restore[6..], -0x80 + (xmm * 0x10));
|
||||
AssertContains(code, restore);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe byte[] CreateTrampolineBytes()
|
||||
{
|
||||
var backend = (DirectExecutionBackend)RuntimeHelpers.GetUninitializedObject(
|
||||
typeof(DirectExecutionBackend));
|
||||
var trampolineList = typeof(DirectExecutionBackend).GetField(
|
||||
"_importHandlerTrampolines",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.NotNull(trampolineList);
|
||||
trampolineList.SetValue(backend, new List<nint>());
|
||||
|
||||
var createTrampoline = typeof(DirectExecutionBackend).GetMethod(
|
||||
"CreateImportHandlerTrampoline",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.NotNull(createTrampoline);
|
||||
var trampoline = (nint)createTrampoline.Invoke(backend, [0])!;
|
||||
Assert.NotEqual(0, trampoline);
|
||||
|
||||
try
|
||||
{
|
||||
return new ReadOnlySpan<byte>((void*)trampoline, 512).ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Assert.True(HostMemory.Free((void*)trampoline, 0, HostMemory.MEM_RELEASE));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertContains(ReadOnlySpan<byte> code, ReadOnlySpan<byte> expected)
|
||||
{
|
||||
Assert.True(
|
||||
code.IndexOf(expected) >= 0,
|
||||
$"Generated trampoline did not contain {Convert.ToHexString(expected)}.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Globalization;
|
||||
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 previousCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("es-ES");
|
||||
|
||||
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));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = previousCulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Memory;
|
||||
|
||||
public sealed unsafe class PosixHostMemoryTests
|
||||
{
|
||||
private const int ProtRead = 0x1;
|
||||
private const int ProtWrite = 0x2;
|
||||
private const int MapPrivate = 0x02;
|
||||
private const int MapAnonymousDarwin = 0x1000;
|
||||
private static readonly nint MapFailed = -1;
|
||||
|
||||
[Fact]
|
||||
public void ExactAllocationDoesNotReplaceUntrackedDarwinMapping()
|
||||
{
|
||||
if (!OperatingSystem.IsMacOS())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var size = checked((nuint)Environment.SystemPageSize);
|
||||
var externalMapping = mmap(
|
||||
0,
|
||||
size,
|
||||
ProtRead | ProtWrite,
|
||||
MapPrivate | MapAnonymousDarwin,
|
||||
-1,
|
||||
0);
|
||||
Assert.NotEqual(MapFailed, externalMapping);
|
||||
|
||||
try
|
||||
{
|
||||
var sentinel = new Span<byte>((void*)externalMapping, checked((int)size));
|
||||
sentinel.Fill(0xA5);
|
||||
|
||||
var hostMemory = new PosixHostMemory();
|
||||
var result = hostMemory.Allocate(
|
||||
(ulong)externalMapping,
|
||||
(ulong)size,
|
||||
HostPageProtection.ReadWrite);
|
||||
|
||||
Assert.Equal(0UL, result);
|
||||
Assert.All(sentinel.ToArray(), value => Assert.Equal(0xA5, value));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Assert.Equal(0, munmap(externalMapping, size));
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern nint mmap(nint addr, nuint length, int prot, int flags, int fd, long offset);
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int munmap(nint addr, nuint length);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Network;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Network;
|
||||
|
||||
// The libSceNet byte-order helpers take their operand in Rdi and return the converted value in
|
||||
// Rax. They swap endianness unconditionally, which is correct on the little-endian hosts (and
|
||||
// little-endian guest) the emulator targets, so network (big-endian) order is always a byte swap.
|
||||
public sealed class NetExportsTests
|
||||
{
|
||||
private readonly CpuContext _ctx = new(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
|
||||
[Fact]
|
||||
public void Htonl_SwapsAllFourBytes()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0x01020304;
|
||||
|
||||
Assert.Equal(0, NetExports.NetHtonl(_ctx));
|
||||
Assert.Equal(0x04030201UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ntohl_SwapsAllFourBytes()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0x01020304;
|
||||
|
||||
Assert.Equal(0, NetExports.NetNtohl(_ctx));
|
||||
Assert.Equal(0x04030201UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Htons_SwapsLowTwoBytesOnly()
|
||||
{
|
||||
// High bits above the 16-bit short must be ignored, not folded into the result.
|
||||
_ctx[CpuRegister.Rdi] = 0xFFFF_0102;
|
||||
|
||||
Assert.Equal(0, NetExports.NetHtons(_ctx));
|
||||
Assert.Equal(0x0201UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ntohs_SwapsLowTwoBytesOnly()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0xFFFF_0102;
|
||||
|
||||
Assert.Equal(0, NetExports.NetNtohs(_ctx));
|
||||
Assert.Equal(0x0201UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Htonl_IgnoresBitsAboveThe32BitWord()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0xDEADBEEF_01020304;
|
||||
|
||||
Assert.Equal(0, NetExports.NetHtonl(_ctx));
|
||||
Assert.Equal(0x04030201UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0xDEADBEEFUL)]
|
||||
[InlineData(0x00000000UL)]
|
||||
[InlineData(0xFFFFFFFFUL)]
|
||||
[InlineData(0x00000001UL)]
|
||||
public void HtonlThenNtohl_RoundTripsToOriginal(ulong value)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = value;
|
||||
NetExports.NetHtonl(_ctx);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = _ctx[CpuRegister.Rax];
|
||||
NetExports.NetNtohl(_ctx);
|
||||
|
||||
Assert.Equal(value, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
// Regression guard: a non-palindromic value must not come back as 0. The functions previously
|
||||
// computed the swap into Rax and then called SetReturn(0), which overwrote Rax, so every
|
||||
// sceNetHtonl/Htons/Ntohl/Ntohs call returned 0 regardless of input.
|
||||
[Fact]
|
||||
public void ByteOrderConversions_DoNotReturnZeroForNonZeroInput()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0x01020304;
|
||||
NetExports.NetHtonl(_ctx);
|
||||
Assert.NotEqual(0UL, _ctx[CpuRegister.Rax]);
|
||||
|
||||
_ctx[CpuRegister.Rax] = 0;
|
||||
_ctx[CpuRegister.Rdi] = 0x0102;
|
||||
NetExports.NetHtons(_ctx);
|
||||
Assert.NotEqual(0UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Pad;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Pad;
|
||||
|
||||
public sealed class HostWindowInputTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(-1.0f, 0)]
|
||||
[InlineData(0.0f, 128)]
|
||||
[InlineData(1.0f, 255)]
|
||||
public void ToStickByteMapsFullSilkRange(float value, int expected)
|
||||
{
|
||||
Assert.Equal((byte)expected, HostWindowInput.ToStickByte(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.PlayGo;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.PlayGo;
|
||||
|
||||
[CollectionDefinition("PlayGoState", DisableParallelization = true)]
|
||||
public sealed class PlayGoStateCollection
|
||||
{
|
||||
public const string Name = "PlayGoState";
|
||||
}
|
||||
|
||||
[Collection(PlayGoStateCollection.Name)]
|
||||
public sealed class PlayGoExportsTests : IDisposable
|
||||
{
|
||||
private const int BadChunkId = unchecked((int)0x80B2000C);
|
||||
private const byte LocusNotDownloaded = 0;
|
||||
private const byte LocusLocalFast = 3;
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const ulong InitParamsAddress = MemoryBase + 0x100;
|
||||
private const ulong InitBufferAddress = MemoryBase + 0x1000;
|
||||
private const ulong HandleAddress = MemoryBase + 0x200;
|
||||
private const ulong ChunkIdsAddress = MemoryBase + 0x300;
|
||||
private const ulong LociAddress = MemoryBase + 0x400;
|
||||
|
||||
private readonly string? _originalApp0Root;
|
||||
private readonly string _app0Root;
|
||||
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x10000);
|
||||
private readonly CpuContext _ctx;
|
||||
|
||||
public PlayGoExportsTests()
|
||||
{
|
||||
_originalApp0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
_app0Root = Path.Combine(Path.GetTempPath(), $"sharpemu-playgo-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_app0Root);
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", _app0Root);
|
||||
PlayGoExports.ResetForTests();
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocus_MetadataFreeApp0_RejectsPastAuthoritativeDefaultChunk()
|
||||
{
|
||||
var handle = InitializeAndOpen();
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, GetLocus(handle, [0], 0x7F));
|
||||
Assert.Equal(new byte[] { LocusLocalFast }, ReadLoci(1));
|
||||
|
||||
Assert.Equal(BadChunkId, GetLocus(handle, [1]));
|
||||
Assert.Equal(new byte[] { LocusNotDownloaded }, ReadLoci(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocus_ParsedChunkDefinitions_WritesPrefixAndRejectsFirstUnknownChunk()
|
||||
{
|
||||
File.WriteAllText(
|
||||
Path.Combine(_app0Root, "playgo-chunkdefs.xml"),
|
||||
"<playgo default_chunk=\"2\"><chunk id=\"2\"/><chunk id=\"7\"/></playgo>");
|
||||
var handle = InitializeAndOpen();
|
||||
|
||||
Assert.Equal(BadChunkId, GetLocus(handle, [2, 3], 0xCC));
|
||||
Assert.Equal(new byte[] { LocusLocalFast, LocusNotDownloaded }, ReadLoci(2));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(UnusableMetadataKind.DatOnly)]
|
||||
[InlineData(UnusableMetadataKind.MalformedChunkDefinitions)]
|
||||
[InlineData(UnusableMetadataKind.UnrecognizedChunkDefinitions)]
|
||||
public void GetLocus_UnparseableMetadata_RemainsPermissive(UnusableMetadataKind metadataKind)
|
||||
{
|
||||
switch (metadataKind)
|
||||
{
|
||||
case UnusableMetadataKind.DatOnly:
|
||||
var sceSys = Directory.CreateDirectory(Path.Combine(_app0Root, "sce_sys"));
|
||||
File.WriteAllBytes(Path.Combine(sceSys.FullName, "playgo-chunk.dat"), [0x70, 0x6C, 0x67, 0x6F]);
|
||||
break;
|
||||
case UnusableMetadataKind.MalformedChunkDefinitions:
|
||||
File.WriteAllText(
|
||||
Path.Combine(_app0Root, "playgo-chunkdefs.xml"),
|
||||
"<playgo><chunk id=\"2\"></playgo>");
|
||||
break;
|
||||
case UnusableMetadataKind.UnrecognizedChunkDefinitions:
|
||||
File.WriteAllText(Path.Combine(_app0Root, "playgo-chunkdefs.xml"), "<not-playgo/>");
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(metadataKind), metadataKind, null);
|
||||
}
|
||||
|
||||
var handle = InitializeAndOpen();
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, GetLocus(handle, [42]));
|
||||
Assert.Equal(new byte[] { LocusLocalFast }, ReadLoci(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocus_FaultingChunkAndOutputPointers_ReturnMemoryFault()
|
||||
{
|
||||
var handle = InitializeAndOpen();
|
||||
const ulong faultingAddress = 0xDEAD_0000_0000;
|
||||
Assert.True(_memory.TryWrite(LociAddress, new byte[] { 0xA5 }));
|
||||
|
||||
SetGetLocusArguments(handle, faultingAddress, 1, LociAddress);
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
PlayGoExports.PlayGoGetLocus(_ctx));
|
||||
Assert.Equal(new byte[] { 0xA5 }, ReadLoci(1));
|
||||
|
||||
WriteChunkIds([1]);
|
||||
SetGetLocusArguments(handle, ChunkIdsAddress, 1, faultingAddress);
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
PlayGoExports.PlayGoGetLocus(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocus_AllEntriesSentinel_DoesNotReadChunksOrWriteLoci()
|
||||
{
|
||||
var handle = InitializeAndOpen();
|
||||
Assert.True(_memory.TryWrite(LociAddress, [0xA5]));
|
||||
|
||||
SetGetLocusArguments(handle, 0xDEAD_0000_0000, uint.MaxValue, LociAddress);
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
PlayGoExports.PlayGoGetLocus(_ctx));
|
||||
Assert.Equal(new byte[] { 0xA5 }, ReadLoci(1));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PlayGoExports.ResetForTests();
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", _originalApp0Root);
|
||||
Directory.Delete(_app0Root, recursive: true);
|
||||
}
|
||||
|
||||
private uint InitializeAndOpen()
|
||||
{
|
||||
Span<byte> initParams = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(initParams, InitBufferAddress);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(initParams[8..], 0x200000);
|
||||
Assert.True(_memory.TryWrite(InitParamsAddress, initParams));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = InitParamsAddress;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
PlayGoExports.PlayGoInitialize(_ctx));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = HandleAddress;
|
||||
_ctx[CpuRegister.Rsi] = 0;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
PlayGoExports.PlayGoOpen(_ctx));
|
||||
|
||||
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(_memory.TryRead(HandleAddress, handleBytes));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(handleBytes);
|
||||
}
|
||||
|
||||
private int GetLocus(uint handle, ushort[] chunkIds, byte? fill = null)
|
||||
{
|
||||
WriteChunkIds(chunkIds);
|
||||
if (fill is { } fillValue)
|
||||
{
|
||||
var initialLoci = new byte[chunkIds.Length];
|
||||
Array.Fill(initialLoci, fillValue);
|
||||
Assert.True(_memory.TryWrite(LociAddress, initialLoci));
|
||||
}
|
||||
|
||||
SetGetLocusArguments(handle, ChunkIdsAddress, (uint)chunkIds.Length, LociAddress);
|
||||
return PlayGoExports.PlayGoGetLocus(_ctx);
|
||||
}
|
||||
|
||||
private void WriteChunkIds(ushort[] chunkIds)
|
||||
{
|
||||
var bytes = new byte[chunkIds.Length * sizeof(ushort)];
|
||||
for (var i = 0; i < chunkIds.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes.AsSpan(i * sizeof(ushort)), chunkIds[i]);
|
||||
}
|
||||
|
||||
Assert.True(_memory.TryWrite(ChunkIdsAddress, bytes));
|
||||
}
|
||||
|
||||
private byte[] ReadLoci(int count)
|
||||
{
|
||||
var loci = new byte[count];
|
||||
Assert.True(_memory.TryRead(LociAddress, loci));
|
||||
return loci;
|
||||
}
|
||||
|
||||
private void SetGetLocusArguments(uint handle, ulong chunkIds, uint count, ulong outLoci)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = handle;
|
||||
_ctx[CpuRegister.Rsi] = chunkIds;
|
||||
_ctx[CpuRegister.Rdx] = count;
|
||||
_ctx[CpuRegister.Rcx] = outLoci;
|
||||
}
|
||||
|
||||
public enum UnusableMetadataKind
|
||||
{
|
||||
DatOnly,
|
||||
MalformedChunkDefinitions,
|
||||
UnrecognizedChunkDefinitions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class VulkanDepthAttachmentTests
|
||||
{
|
||||
private static readonly GuestDepthTarget Target = new(
|
||||
ReadAddress: 0x1000,
|
||||
WriteAddress: 0x1000,
|
||||
Width: 1920,
|
||||
Height: 1080,
|
||||
GuestFormat: 1,
|
||||
SwizzleMode: 0,
|
||||
ClearDepth: 1f,
|
||||
ReadOnly: false);
|
||||
|
||||
[Fact]
|
||||
public void GuestDepthTarget_AttachesForDepthWork()
|
||||
{
|
||||
var state = new GuestDepthState(
|
||||
TestEnable: true,
|
||||
WriteEnable: true,
|
||||
CompareOp: 3);
|
||||
|
||||
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
public void GuestDepthTarget_AttachesForEitherDepthOperation(
|
||||
bool testEnable,
|
||||
bool writeEnable)
|
||||
{
|
||||
var state = new GuestDepthState(testEnable, writeEnable, CompareOp: 3);
|
||||
|
||||
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuestDepthTarget_RequiresTargetAndDepthWork()
|
||||
{
|
||||
var state = GuestDepthState.Default;
|
||||
|
||||
Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
||||
Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(
|
||||
target: null,
|
||||
new GuestDepthState(true, false, CompareOp: 3)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuestDepthTarget_AttachesForDepthClear()
|
||||
{
|
||||
var state = new GuestDepthState(
|
||||
TestEnable: false,
|
||||
WriteEnable: false,
|
||||
CompareOp: 7,
|
||||
ClearEnable: true);
|
||||
|
||||
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x41u, true)]
|
||||
[InlineData(0x40u, false)]
|
||||
public void DepthState_DecodesRenderControlClearBit(
|
||||
uint renderControl,
|
||||
bool clearEnable)
|
||||
{
|
||||
var registers = new Dictionary<uint, uint>
|
||||
{
|
||||
[0x000] = renderControl,
|
||||
[0x200] = 0x776,
|
||||
};
|
||||
|
||||
var state = AgcExports.DecodeDepthState(registers);
|
||||
|
||||
Assert.True(state.TestEnable);
|
||||
Assert.True(state.WriteEnable);
|
||||
Assert.Equal(7u, state.CompareOp);
|
||||
Assert.Equal(clearEnable, state.ClearEnable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Silk.NET.Vulkan;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class VulkanHostBufferPoolTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReturnedAllocationCanBeRentedAgain()
|
||||
{
|
||||
var destroyed = new List<VulkanHostBufferAllocation>();
|
||||
using var pool = new VulkanHostBufferPool(1024, destroyed.Add);
|
||||
var key = new VulkanHostBufferPoolKey(BufferUsageFlags.StorageBufferBit, 256);
|
||||
var allocation = Allocation(1, 2, key);
|
||||
|
||||
pool.Register(allocation);
|
||||
Assert.True(pool.Return(allocation.Buffer, allocation.Memory));
|
||||
Assert.Equal(256UL, pool.CachedBytes);
|
||||
|
||||
Assert.True(pool.TryRent(key, out var rented));
|
||||
Assert.Equal(allocation, rented);
|
||||
Assert.Equal(0UL, pool.CachedBytes);
|
||||
Assert.Empty(destroyed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnDestroysAllocationThatWouldExceedBudget()
|
||||
{
|
||||
var destroyed = new List<VulkanHostBufferAllocation>();
|
||||
using var pool = new VulkanHostBufferPool(256, destroyed.Add);
|
||||
var key = new VulkanHostBufferPoolKey(BufferUsageFlags.VertexBufferBit, 512);
|
||||
var allocation = Allocation(3, 4, key);
|
||||
|
||||
pool.Register(allocation);
|
||||
|
||||
Assert.True(pool.Return(allocation.Buffer, allocation.Memory));
|
||||
Assert.Equal(0UL, pool.CachedBytes);
|
||||
Assert.Equal([allocation], destroyed);
|
||||
Assert.False(pool.TryRent(key, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownAllocationIsNotClaimedByPool()
|
||||
{
|
||||
using var pool = new VulkanHostBufferPool(1024, _ => { });
|
||||
|
||||
Assert.False(pool.Return(new VkBuffer(9), new DeviceMemory(10)));
|
||||
}
|
||||
|
||||
private static VulkanHostBufferAllocation Allocation(
|
||||
ulong buffer,
|
||||
ulong memory,
|
||||
VulkanHostBufferPoolKey key) =>
|
||||
new(new VkBuffer(buffer), new DeviceMemory(memory), key, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user