Compare commits

..

18 Commits

Author SHA1 Message Date
Berk 459ae7e3f7 Docs/readme images (#302)
* [readme] update showcase images

* [readme] update readme images and change version to 0.0.2
2026-07-17 02:44:28 +03:00
kuba 8f405caebe Vulkan: reduce guest buffer upload allocations (#301) 2026-07-17 02:36:39 +03:00
kuba dabf723b3e [Vulkan] Honor guest depth clear state (#290) 2026-07-17 02:29:53 +03:00
AlexC 2129a12684 [GUI] Last commit SHA (id) displayed on about section (#279)
* Latest commit info axamal structure

* Link latest commit to text

* Added localization for About>Last commit info

* Made the commit hash a button that redirects you to the commit in github

* Reorder about section

* Commit and update icon in about section to have consistency in section
2026-07-17 02:25:01 +03:00
kuba 2db1fae282 [Tests/HLE] Cover APR resolve, stat, and streaming flow (#272) 2026-07-16 19:57:45 +03:00
kuba 33f96252da [CPU] Reject context transfers to unmapped guest addresses (#273) 2026-07-16 19:57:34 +03:00
kuba f7981a7ed7 [Tests/CPU] Verify import trampoline volatile-state ABI (#274) 2026-07-16 19:57:23 +03:00
kuba e10efa3ae1 [HLE] Make guest printf formatting locale-invariant (#271) 2026-07-16 18:39:48 +02:00
Peter Bonanni 16a2131b67 [PlayGo] Reject authoritative unknown chunk loci (#242) 2026-07-16 19:22:33 +03:00
Mees van den Kieboom 9883a9445d [AGC] Implement RDNA2 buffer/image/DS 32-bit atomic instructions (#222)
Adds decode and SPIR-V translation for the missing MUBUF, MIMG and DS
atomic instructions in the Gen5 shader translator, generalizing the
existing BufferAtomicAdd path. Covers swap, cmpswap, add, sub,
smin/smax, umin/umax, and/or/xor, inc and dec, plus the DS RTN
variants. Image atomics go through OpImageTexelPointer on the storage
image binding.

Notable: DS_CMPST operand order (DATA0 = comparator, DATA1 = new
value) is reversed relative to buffer/image cmpswap, which a dedicated
test locks in. ATOMIC_INC/DEC are approximated with
OpAtomicIIncrement/IDecrement, exact for the common 0xFFFFFFFF clamp.

Verified with 9 new synthetic decoder and end-to-end SPIR-V tests
(part of the #36 test corpus effort); full suite passes 35/35.

Signed-off-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
Co-authored-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
2026-07-16 17:28:39 +03:00
Peter Bonanni f2d9051358 Fix macOS fixed-address allocation collisions (#246) 2026-07-16 16:45:08 +03:00
Dafenx 53d414e096 Bound Vulkan host buffer pool memory (#201)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-16 16:44:29 +03:00
MikeyLITE69 52d2874fa8 cpu: emulate BMI1/BMI2/ABM instructions in software when the host lacks them (#249)
## What

The native backend runs guest code directly on the host CPU. When the host doesn't
implement a BMI1/BMI2/ABM instruction that the PS5's Zen 2 cores do, it raises #UD
(STATUS_ILLEGAL_INSTRUCTION). Today the vectored handler just logs the faulting bytes
and gives up, so the title dies.

This adds a software fallback. On an illegal-instruction fault we decode the opcode
with Iced (the decoder already used elsewhere in the backend), evaluate it against the
trapped register/memory state, write the result and flags back into the CONTEXT record,
step RIP past the instruction, and resume.

Instructions covered (32- and 64-bit): ANDN, BLSI, BLSMSK, BLSR, BEXTR, BZHI, TZCNT,
LZCNT, RORX, SARX, SHLX, SHRX, PDEP, PEXT.

## Why

Users on CPUs without these extensions currently can't get past code that uses them.
This is a generic fix (no game-specific hacks) that improves compatibility on older
hosts. MULX is intentionally left out for now — its dest_hi/dest_lo operand ordering
is easy to get subtly wrong, so I'd rather add it separately with its own tests.

## How it's structured

- `BmiInstructionEmulator` holds the pure bit/flag semantics with no dependency on the
  unsafe CONTEXT plumbing, so it can be unit-tested directly.
- `DirectExecutionBackend.IllegalInstruction.cs` is the thin unsafe adapter (decode →
  read operands → emulate → write back → advance RIP). Anything it doesn't fully model
  returns false and falls through to the existing diagnostics unchanged, so it can never
  mis-handle an opcode it doesn't recognize.
- One hook in `DirectExecutionBackend.Exceptions.cs`, next to the other TryRecover* calls.
- Emits a single one-time "emulating in software" log line, not per-instruction spam.

## How I verified

- Added xUnit tests covering every instruction in both widths plus the CF/ZF/SF/OF
  edge cases (src == 0, shift-count masking, index beyond operand width, etc.).
- Cross-checked all the expected values against an independent reference implementation
  written from the Intel/AMD definitions; results match.
- `dotnet build` + `dotnet test` pass locally.

## Notes

New files follow .editorconfig (4-space, SPDX headers, REUSE-compliant).
2026-07-16 16:38:27 +03:00
Gutemberg Ribeiro c5a82c1065 [Perf] Behavior-preserving hot-path allocation/LINQ wins (#264)
* [Perf] Gate event-flag tracing so it allocates nothing when disabled

TraceEventFlag built its interpolated argument (and, on the wait path,
FormatFrameChain + a new StringBuilder(256) in FormatGuestWaitObject plus
~12 guest-memory reads) on every call, then checked the env var inside the
method — so every sceKernelSetEventFlag/Clear/Poll/Wait paid a string
allocation and an Environment.GetEnvironmentVariable P/Invoke even with
tracing off. Cache the flag once in a static readonly bool and gate every
call site, matching the semaphore/event-queue pattern. Behavior is
unchanged when SHARPEMU_LOG_EVENT_FLAG=1.

* [Perf] Hoist IsNoBlockLeaf classification to import-stub setup

The leaf-dispatch path called IsNoBlockLeafImport(nid) — a ~30-literal
string pattern match — on every leaf import. IsLeaf/NidHash were already
precomputed on ImportStubEntry at stub setup; add IsNoBlockLeaf alongside
them and read the field in the hot path. Behavior unchanged.

* [Perf] Cache trace env-var flags read on hot paths

Two SHARPEMU_LOG_* env vars were read via Environment.GetEnvironmentVariable
(a P/Invoke + transient string) on hot paths: SHARPEMU_LOG_FIBER on every
fiber context transfer, and SHARPEMU_LOG_DIRECT_MEMORY on every direct-memory
op (~8 sites). Cache both once — _logFiber alongside the other backend _log*
flags, and _traceDirectMemory behind the existing ShouldTraceDirectMemory
helper. Behavior unchanged.

* [Perf] Avoid per-iteration thread snapshot in the idle pump loop

PumpUntilGuestThreadsIdle allocated a full GuestThreadState[] snapshot
(via LINQ Values.ToArray()) on every spin just to tally three run-state
booleans. Tally them under the lock with an allocation-free helper, and
only materialize the snapshot inside the gated (default-off) diagnostic
dump. SnapshotGuestThreads now uses Values.CopyTo instead of LINQ.
Behavior unchanged.

* [Perf] De-LINQ GetPixelColorExportMask on the per-draw path

GetPixelColorExportMask ran a Select/OfType/Where/Aggregate chain over all
shader instructions, allocating iterators + closures, and is called per
render target (twice per draw via CreateRenderState and HasPixelColorExport).
Replace with a manual scan producing the identical mask — no allocation, and
it removes an authored-LINQ use the repo bans.

* [Perf] De-LINQ per-draw render-target selection in AgcExports

The bound-render-target selection used Where(...).OrderBy(...).ToArray()
(plus a second Where/ToArray fallback) on every translated draw, allocating
LINQ iterators + closures. Replace with an explicit filter into a pre-sized
list + List.Sort by slot; slots are distinct so this matches the stable
OrderBy. Same result, no per-draw LINQ allocations.

* [Perf] De-LINQ per-draw render-target validation in the Vulkan presenter

SubmitOffscreenTranslatedDraw validated its render targets with
targets.Any(...) twice plus targets.Select(a=>a.Address).Distinct().Count()
(a per-draw HashSet), and broadcast a single blend with
Enumerable.Repeat(...).ToArray(). Targets are <= 8, so replace with manual
scans (invalid-target check; combined dimension-mismatch + pairwise
aliasing check) and Array.Fill. Same results, no per-draw LINQ allocations.

* [Perf] Binary-search VirtualQuery region lookup (SortedList)

_mappedRegions was an unordered Dictionary, so TryFindVirtualQueryRegionLocked
scanned every region for containment/next — O(n) per sceKernelVirtualQuery and
O(n^2) when an allocator walks the address space with the findNext flag. Store
regions in a SortedList keyed by base address (every write already uses the
region's own address as the key) and find the containing/next region with a
binary search over the sorted keys. Also drops a now-redundant Values.OrderBy.

Non-overlapping regions assumed (mmap semantics), so only the floor region can
contain the query. Behavior-preserving; worth spot-checking VirtualQuery-heavy
titles.

* [Perf] Remove stray CLI packages.lock.json committed by mistake

git add -A in an earlier commit swept in a regenerated
src/SharpEmu.CLI/packages.lock.json. main tracks no lock files (central
package management, no RestorePackagesWithLockFile), and REUSE.toml no
longer covers packages.lock.json, so the committed file failed the REUSE
Compliance check. Remove it.
2026-07-16 16:37:28 +03:00
can a8be1daca4 Fix sceNetHtonl/Htons/Ntohl/Ntohs discarding their converted result (#211)
Each of the four byte-order helpers computed the swap into Rax and then
returned via ctx.SetReturn(0). SetReturn writes its argument into Rax, so it
immediately overwrote the converted value with 0 and the guest saw every
sceNetHtonl / sceNetHtons / sceNetNtohl / sceNetNtohs call return 0.

Leave the swapped value in Rax and return ORBIS_GEN2_OK as the dispatch status
instead, matching how the value-returning exports in this file (e.g.
sceNetPoolCreate) already work.

Add a NetExports test suite covering the swaps, the 16-bit width masking, an
htonl/ntohl round-trip, and a regression guard that a non-zero input never
converts to 0.
2026-07-16 16:24:34 +03:00
999sian 26dbad8ac8 fix(input): map POSIX stick endpoints exactly (#244) 2026-07-16 16:19:33 +03:00
Dafenx 2196a1f786 Gate Vulkan texture upload traces (#220)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-16 16:19:11 +03:00
kostyaff 8db2c0622d [VideoOut] Implement sceVideoOutIsOutputSupported stub (fixes #257) (#270)
Quake (PPSA01880, Kex Engine) crashes at startup because
sceVideoOutIsOutputSupported (NID Nv8c-Kb+DUM) is unimplemented.
The game calls it to check video output capabilities before
initializing rendering.

Add HLE export stub: returns 1 (supported) for SceVideoOutBusTypeMain,
0 otherwise. The emulator renders via Vulkan and supports any pixel
format or aspect ratio on the main bus.

NID verified via Ps5Nid.Compute SHA1 algorithm.
Export name confirmed in scripts/ps5_names.txt.
2026-07-16 16:07:21 +03:00
48 changed files with 3104 additions and 327 deletions
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

+1 -1
View File
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.1</SharpEmuVersion> <SharpEmuVersion>0.0.2</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version> <Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot> <RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
+6 -14
View File
@@ -90,21 +90,13 @@ release includes the MoltenVK Vulkan implementation.
## Games Tested ## Games Tested
* **Demon's Souls Remake** | Demons Souls Remake | Dreaming Sarah |
* [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. | ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
![DeS videoOut submit first frame](./.github/images/des-videoout-shaders.jpg)
* **Poppy Playtime Chapter 1** | Void Terrarium | Dead Cells |
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/sharpemu/sharpemu/issues/3) | :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
* **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;
![Splash texture](./.github/images/dreaming-sarah.jpg)
> [!IMPORTANT] > [!IMPORTANT]
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -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) &amp; 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) &amp; 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) &amp; 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; return -1;
} }
if (exceptionCode == StatusIllegalInstruction &&
TryRecoverIllegalInstruction(contextRecord, rip))
{
return -1;
}
if (IsBenignHostDebugException(exceptionCode)) if (IsBenignHostDebugException(exceptionCode))
{ {
return -1; 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); *(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub);
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal)) if (_logFiber)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " + $"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " +
@@ -1292,7 +1292,7 @@ public sealed partial class DirectExecutionBackend
} }
int returnValue; int returnValue;
if (IsNoBlockLeafImport(importStubEntry.Nid)) if (importStubEntry.IsNoBlockLeaf)
{ {
cpuContext.ClearRaxWriteFlag(); cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext); returnValue = export.Function(cpuContext);
@@ -48,6 +48,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// hashing are hoisted to stub-setup time. // hashing are hoisted to stub-setup time.
public bool IsLeaf { get; } public bool IsLeaf { get; }
public bool IsNoBlockLeaf { get; }
public bool SuppressStrlenTrace { get; } public bool SuppressStrlenTrace { get; }
public bool IsLoopGuardBoundary { get; } public bool IsLoopGuardBoundary { get; }
@@ -59,6 +61,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
string nid, string nid,
ExportedFunction? export, ExportedFunction? export,
bool isLeaf, bool isLeaf,
bool isNoBlockLeaf,
bool suppressStrlenTrace, bool suppressStrlenTrace,
bool isLoopGuardBoundary, bool isLoopGuardBoundary,
ulong nidHash) ulong nidHash)
@@ -67,6 +70,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Nid = nid; Nid = nid;
Export = export; Export = export;
IsLeaf = isLeaf; IsLeaf = isLeaf;
IsNoBlockLeaf = isNoBlockLeaf;
SuppressStrlenTrace = suppressStrlenTrace; SuppressStrlenTrace = suppressStrlenTrace;
IsLoopGuardBoundary = isLoopGuardBoundary; IsLoopGuardBoundary = isLoopGuardBoundary;
NidHash = nidHash; NidHash = nidHash;
@@ -318,6 +322,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private bool _logUsleep; private bool _logUsleep;
private bool _logFiber;
private bool _logBootstrap; private bool _logBootstrap;
private bool _logAllImports; private bool _logAllImports;
@@ -1067,6 +1073,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_ignoredGuestInt41Count = 0; _ignoredGuestInt41Count = 0;
_logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal); _logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
_logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "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); _logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
_logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "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); _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, text2,
resolvedExport, resolvedExport,
IsLeafImport(text2), IsLeafImport(text2),
IsNoBlockLeafImport(text2),
ShouldSuppressStrlenTrace(text2), ShouldSuppressStrlenTrace(text2),
IsImportLoopGuardBoundary(text2), IsImportLoopGuardBoundary(text2),
StableHash64(text2)); StableHash64(text2));
@@ -1760,14 +1768,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
frameAddress = 0; frameAddress = 0;
transferStub = 0; transferStub = 0;
error = null; 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; return false;
} }
if (target.Rsp < sizeof(ulong) || if (!TryValidateGuestContextTransferTarget(activeContext.Memory, target, out error))
ActiveCpuContext is not { } activeContext || {
!activeContext.TryWriteUInt64(target.Rsp - sizeof(ulong), target.Rip)) 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}"; error = $"guest context transfer slot is not writable at 0x{target.Rsp - sizeof(ulong):X16}";
return false; return false;
@@ -1811,6 +1821,30 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true; 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() private unsafe nint GetOrCreateGuestContextTransferStub()
{ {
if (Volatile.Read(ref _guestContextTransferStub) != 0) if (Volatile.Read(ref _guestContextTransferStub) != 0)
@@ -3264,31 +3298,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{ {
Pump(callerContext, reason); Pump(callerContext, reason);
var threads = SnapshotGuestThreads(); // Tally run states under the lock without allocating a snapshot every
if (threads.Length == 0) // 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; 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) if (hasReadyThread)
{ {
continue; continue;
@@ -3301,7 +3319,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (_logGuestThreads && Stopwatch.GetTimestamp() >= nextSnapshotTimestamp) if (_logGuestThreads && Stopwatch.GetTimestamp() >= nextSnapshotTimestamp)
{ {
foreach (var thread in threads) foreach (var thread in SnapshotGuestThreads())
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] guest_thread.idle_wait reason={reason} handle=0x{thread.ThreadHandle:X16} " + $"[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")) 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;
}
}
} }
} }
+4
View File
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Iced" /> <PackageReference Include="Iced" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup> </PropertyGroup>
+2
View File
@@ -141,6 +141,8 @@
"Options.About" : "About", "Options.About" : "About",
"About.Github.Label": "GitHub", "About.Github.Label": "GitHub",
"About.Github.Desc": "Source code, issues and project development.", "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.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.", "About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!", "About.GithubButton": "Contribute in GitHub!",
+2
View File
@@ -131,6 +131,8 @@
"About.Github.Label": "GitHub", "About.Github.Label": "GitHub",
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.", "About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
"About.Discord.Label": "Discord", "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.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!", "About.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "Únete a nuestro Discord!" "About.DiscordButton": "Únete a nuestro Discord!"
+25 -1
View File
@@ -333,12 +333,36 @@ SPDX-License-Identifier: GPL-2.0-or-later
Classes="sectionTitle" Classes="sectionTitle"
Text="ABOUT" /> Text="ABOUT" />
<!--Latest commit info-->
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center"> <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="UpdateLabel" Text="Updates" FontSize="13" />
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11" <TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" /> Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</StackPanel>
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost" <Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
Content="Check for updates" VerticalAlignment="Center" /> Content="Check for updates" VerticalAlignment="Center" />
</Grid> </Grid>
+109
View File
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Net.Http.Headers;
namespace SharpEmu.GUI; namespace SharpEmu.GUI;
@@ -78,6 +79,10 @@ public partial class MainWindow : Window
private long _navUpNextAt; private long _navUpNextAt;
private long _navDownNextAt; private long _navDownNextAt;
//Github http client for latest commit
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
private string? _latestCommitSha;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
@@ -195,6 +200,21 @@ public partial class MainWindow : Window
UseShellExecute = true UseShellExecute = true
}); });
}; };
LatestCommitHashText.Click += (_, _) =>
{
if (string.IsNullOrWhiteSpace(_latestCommitSha))
{
return;
}
Process.Start(new ProcessStartInfo
{
FileName =
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
UseShellExecute = true
});
};
} }
/// <summary> /// <summary>
@@ -236,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 ---- // ---- Controller navigation ----
private void PollGamepad() private void PollGamepad()
@@ -381,6 +486,8 @@ public partial class MainWindow : Window
ApplySettingsToControls(); ApplySettingsToControls();
LocateEmulator(); LocateEmulator();
UpdateDiscordPresence(); UpdateDiscordPresence();
_ = LoadLatestCommitAsync();
if (_settings.CheckForUpdatesOnStartup) if (_settings.CheckForUpdatesOnStartup)
{ {
_ = CheckForUpdatesAsync(); _ = CheckForUpdatesAsync();
@@ -516,6 +623,8 @@ public partial class MainWindow : Window
GithubButton.Content = loc.Get("About.GithubButton"); GithubButton.Content = loc.Get("About.GithubButton");
DiscordButton.Content = loc.Get("About.DiscordButton"); DiscordButton.Content = loc.Get("About.DiscordButton");
UpdateLabel.Text = loc.Get("Updater.Label"); UpdateLabel.Text = loc.Get("Updater.Label");
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
RefreshUpdateText(); RefreshUpdateText();
UpdateEmptyStateTexts(); UpdateEmptyStateTexts();
+2
View File
@@ -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\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" /> <AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.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>
<ItemGroup> <ItemGroup>
+72 -12
View File
@@ -163,13 +163,18 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
private const int PROT_EXEC = 0x4; private const int PROT_EXEC = 0x4;
private const int MAP_PRIVATE = 0x02; 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_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000; private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
// Linux-only: fail instead of clobbering an existing mapping. // Linux-only: fail instead of clobbering an existing mapping.
private const int MAP_FIXED_NOREPLACE = 0x100000; 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 nint MAP_FAILED = -1;
private static readonly object Gate = new(); private static readonly object Gate = new();
@@ -181,6 +186,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
public ulong Size; public ulong Size;
public uint DefaultProtect; public uint DefaultProtect;
public Dictionary<ulong, uint>? PageProtects; public Dictionary<ulong, uint>? PageProtects;
public bool UsesMachAllocation;
public ulong End => Base + Size; public ulong End => Base + Size;
@@ -251,6 +257,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
} }
nint result; nint result;
var usesMachAllocation = false;
if (address != null) if (address != null)
{ {
// Win32 maps at exactly the requested address or fails // 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()) if (result == MAP_FAILED && OperatingSystem.IsMacOS())
{ {
// The hint-only attempt above didn't land at the requested // The hint-only attempt above didn't land at the requested
// address. This is routinely the case for the PS5's fixed // address. mach_vm_allocate with fixed placement and no
// 32 GiB image base under Rosetta 2 on Apple Silicon, where // overwrite flag atomically maps the requested range or
// the kernel never honors that hint. Retry with true // fails if any host mapping already owns it. Unlike
// MAP_FIXED: this can clobber an untracked host mapping // MAP_FIXED, it cannot clobber CLR, dyld, JIT, or Rosetta
// (dyld, the runtime's JIT heap, Rosetta) if one already // memory that is absent from our shadow table.
// sits exactly there, but without it guest images that Trace($"exact mmap hint failed, retrying with fixed Mach allocation: addr=0x{(ulong)address:X16}");
// require this base never load at all on macOS. result = AllocateDarwinFixed(
Trace($"exact mmap hint failed, retrying with MAP_FIXED: addr=0x{(ulong)address:X16}"); (nint)address,
result = mmap((nint)address, (nuint)alignedSize, posixProtect, flags | MAP_FIXED, -1, 0); alignedSize,
posixProtect);
usesMachAllocation = result != MAP_FAILED;
} }
if (result == MAP_FAILED || (ulong)result != (ulong)address) if (result == MAP_FAILED || (ulong)result != (ulong)address)
@@ -316,7 +325,8 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
{ {
Base = (ulong)result, Base = (ulong)result,
Size = alignedSize, Size = alignedSize,
DefaultProtect = protect DefaultProtect = protect,
UsesMachAllocation = usesMachAllocation,
}; };
return (void*)result; return (void*)result;
@@ -335,8 +345,15 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
return false; return false;
} }
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); Regions.Remove((ulong)address);
return munmap((nint)address, (nuint)region.Size) == 0; }
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) private static void Trace(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal)) 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)] [DllImport("libc", SetLastError = true)]
private static extern int mprotect(nint addr, nuint length, int prot); 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);
} }
} }
+1
View File
@@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="SharpEmu.Core" /> <InternalsVisibleTo Include="SharpEmu.Core" />
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+53 -24
View File
@@ -121,6 +121,7 @@ public static partial class AgcExports
private const uint CbBlend0Control = 0x1E0; private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292; private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4). // GFX10 DB context registers (register byte address minus 0x28000, / 4).
private const uint DbRenderControl = 0x000;
private const uint DbDepthView = 0x002; private const uint DbDepthView = 0x002;
private const uint DbDepthSizeXy = 0x007; private const uint DbDepthSizeXy = 0x007;
private const uint DbDepthClear = 0x00B; private const uint DbDepthClear = 0x00B;
@@ -5291,7 +5292,7 @@ public static partial class AgcExports
var hasDepthOnlyCandidate = hasExportShader && var hasDepthOnlyCandidate = hasExportShader &&
!hasPixelShader && !hasPixelShader &&
depthTarget is not null && depthTarget is not null &&
(depthState.TestEnable || depthState.WriteEnable); (depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
if (hasDepthOnlyCandidate && if (hasDepthOnlyCandidate &&
TryCreateTranslatedDepthOnlyGuestDraw( TryCreateTranslatedDepthOnlyGuestDraw(
ctx, ctx,
@@ -5937,16 +5938,30 @@ public static partial class AgcExports
// draw a multi-render-target G-buffer (up to eight slots) in one pass. // 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. // Fall back to slot 0 if we cannot match any export to a bound target.
var allBoundTargets = GetRenderTargets(state.CxRegisters); var allBoundTargets = GetRenderTargets(state.CxRegisters);
var renderTargets = allBoundTargets // At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
.Where(target => HasPixelColorExport(pixelState, target.Slot)) // closure allocations. Slots are distinct, so sorting by slot is stable.
.OrderBy(target => target.Slot) var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
.ToArray(); foreach (var target in allBoundTargets)
if (renderTargets.Length == 0)
{ {
renderTargets = allBoundTargets if (HasPixelColorExport(pixelState, target.Slot))
.Where(target => target.Slot == 0) {
.ToArray(); 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) if (_traceAgcShader && allBoundTargets.Count > 1)
{ {
TraceAgcShader( TraceAgcShader(
@@ -6398,12 +6413,23 @@ public static partial class AgcExports
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) => private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
GetPixelColorExportMask(state, target) != 0; GetPixelColorExportMask(state, target) != 0;
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) => private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target)
state.Program.Instructions {
.Select(instruction => instruction.Control) // Called per render target (twice per draw via CreateRenderState +
.OfType<Gen5ExportControl>() // HasPixelColorExport); a manual scan avoids the per-call LINQ iterator
.Where(export => export.Target == target) // and closure allocations. Same result as the previous
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu)); // 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) 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 // DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering). // 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 const uint DbDepthControl = 0x200;
private static GuestDepthState DecodeDepthState( internal static GuestDepthState DecodeDepthState(
IReadOnlyDictionary<uint, uint> registers) IReadOnlyDictionary<uint, uint> registers)
{ {
if (!registers.TryGetValue(DbDepthControl, out var control)) var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
{ registers.TryGetValue(DbRenderControl, out var renderControl);
return GuestDepthState.Default;
}
var testEnable = (control & 0x2u) != 0; var testEnable = (control & 0x2u) != 0;
var writeEnable = (control & 0x4u) != 0; var writeEnable = (control & 0x4u) != 0;
var compareOp = (control >> 4) & 0x7u; var compareOp = hasDepthControl
return new GuestDepthState(testEnable, writeEnable, compareOp); ? (control >> 4) & 0x7u
: GuestDepthState.Default.CompareOp;
var clearEnable = (renderControl & 0x1u) != 0;
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
} }
private static GuestDepthTarget? DecodeDepthTarget( private static GuestDepthTarget? DecodeDepthTarget(
IReadOnlyDictionary<uint, uint> registers) IReadOnlyDictionary<uint, uint> registers)
{ {
var depthState = DecodeDepthState(registers); var depthState = DecodeDepthState(registers);
if (!depthState.TestEnable && !depthState.WriteEnable) if (!depthState.TestEnable &&
!depthState.WriteEnable &&
!depthState.ClearEnable)
{ {
return null; return null;
} }
+3 -2
View File
@@ -91,9 +91,10 @@ internal readonly record struct GuestRasterState(
internal readonly record struct GuestDepthState( internal readonly record struct GuestDepthState(
bool TestEnable, bool TestEnable,
bool WriteEnable, 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 /// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
@@ -25,6 +25,11 @@ public static class KernelEventFlagCompatExports
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new(); private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
private static long _nextEventFlagHandle = 1; 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 private sealed class EventFlagState
{ {
public required string Name { get; init; } public required string Name { get; init; }
@@ -79,7 +84,7 @@ public static class KernelEventFlagCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -101,7 +106,7 @@ public static class KernelEventFlagCompatExports
Monitor.PulseAll(state.Gate); 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -124,7 +129,7 @@ public static class KernelEventFlagCompatExports
{ {
state.Bits |= pattern; state.Bits |= pattern;
Monitor.PulseAll(state.Gate); 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)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
@@ -148,7 +153,7 @@ public static class KernelEventFlagCompatExports
lock (state.Gate) lock (state.Gate)
{ {
state.Bits &= pattern; 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
@@ -189,7 +194,7 @@ public static class KernelEventFlagCompatExports
} }
ApplyClearMode(state, pattern, waitMode); 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -287,8 +292,8 @@ public static class KernelEventFlagCompatExports
return true; return true;
}, },
deadline); 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)}"); 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)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock) if (!requestedBlock)
{ {
var scheduler = GuestThreadExecution.Scheduler; var scheduler = GuestThreadExecution.Scheduler;
@@ -298,7 +303,7 @@ public static class KernelEventFlagCompatExports
} }
state.WaitingThreads++; 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; var releaseWaiter = true;
try try
{ {
@@ -318,7 +323,7 @@ public static class KernelEventFlagCompatExports
{ {
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false; 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); return SetReturn(ctx, pumpedWaitResult);
} }
@@ -329,7 +334,7 @@ public static class KernelEventFlagCompatExports
releaseWaiter = false; releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0); _ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits); _ = 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
} }
@@ -346,7 +351,7 @@ public static class KernelEventFlagCompatExports
} }
state.WaitingThreads++; 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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
finally finally
@@ -381,7 +386,7 @@ public static class KernelEventFlagCompatExports
state.Bits = setPattern; state.Bits = setPattern;
state.WaitingThreads = 0; state.WaitingThreads = 0;
Monitor.PulseAll(state.Gate); Monitor.PulseAll(state.Gate);
TraceEventFlag( if (_traceEventFlag) TraceEventFlag(
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " + $"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():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); 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}"); $"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true; return true;
} }
@@ -568,7 +573,7 @@ public static class KernelEventFlagCompatExports
private static void TraceEventFlag(string message) 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}"); 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 object _guestMountGate = new();
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new(); private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = 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<ulong, string> _mappedRegionNames = new();
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal); private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
@@ -5480,7 +5484,9 @@ public static partial class KernelMemoryCompatExports
var affected = new List<MappedRegion>(); var affected = new List<MappedRegion>();
var cursor = address; 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) if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor)
{ {
@@ -5641,9 +5647,33 @@ public static partial class KernelMemoryCompatExports
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region) private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
{ {
region = default; region = default;
var foundNext = false; var keys = _mappedRegions.Keys;
foreach (var candidate in _mappedRegions.Values) 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) && if (TryAddU64(candidate.Address, candidate.Length, out var candidateEnd) &&
queryAddress >= candidate.Address && queryAddress >= candidate.Address &&
queryAddress < candidateEnd) queryAddress < candidateEnd)
@@ -5651,20 +5681,16 @@ public static partial class KernelMemoryCompatExports
region = candidate; region = candidate;
return true; return true;
} }
}
if (!findNext || candidate.Address < queryAddress) // findNext: the region with the smallest base address >= queryAddress.
if (findNext && lo < count)
{ {
continue; region = values[lo];
return true;
} }
if (!foundNext || candidate.Address < region.Address) return false;
{
region = candidate;
foundNext = true;
}
}
return foundNext;
} }
private static void TraceDirectMemoryCall( private static void TraceDirectMemoryCall(
@@ -5693,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>"}"); $"[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() // Cached once so the ~8 direct-memory call sites don't each do a
{ // GetEnvironmentVariable P/Invoke per operation.
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal); 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) private static bool TryReleaseDirectMemoryRangeLocked(ulong start, ulong length)
{ {
+12 -4
View File
@@ -344,8 +344,10 @@ public static class NetExports
public static int NetHtonl(CpuContext ctx) public static int NetHtonl(CpuContext ctx)
{ {
var value = unchecked((uint)ctx[CpuRegister.Rdi]); 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); ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0); return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport( [SysAbiExport(
@@ -356,8 +358,10 @@ public static class NetExports
public static int NetHtons(CpuContext ctx) public static int NetHtons(CpuContext ctx)
{ {
var value = unchecked((ushort)ctx[CpuRegister.Rdi]); 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); ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0); return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport( [SysAbiExport(
@@ -368,8 +372,10 @@ public static class NetExports
public static int NetNtohl(CpuContext ctx) public static int NetNtohl(CpuContext ctx)
{ {
var value = unchecked((uint)ctx[CpuRegister.Rdi]); 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); ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0); return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport( [SysAbiExport(
@@ -380,8 +386,10 @@ public static class NetExports
public static int NetNtohs(CpuContext ctx) public static int NetNtohs(CpuContext ctx)
{ {
var value = unchecked((ushort)ctx[CpuRegister.Rdi]); 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); ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0); return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport( [SysAbiExport(
+2 -2
View File
@@ -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 private static HostGamepadButtons MapButton(ButtonName name) => name switch
+56 -15
View File
@@ -4,6 +4,7 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace SharpEmu.Libs.PlayGo; namespace SharpEmu.Libs.PlayGo;
@@ -423,7 +424,10 @@ public static class PlayGoExports
// Titles rely on this as an enumeration terminator: Monster Truck // Titles rely on this as an enumeration terminator: Monster Truck
// scans ids 0,1,2,... until BAD_CHUNK_ID, and answering OK for every // 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. // id makes that scan wrap the ushort range and spin forever.
return OrbisPlayGoErrorBadChunkId; loci[i] = PlayGoLocusNotDownloaded;
return ctx.Memory.TryWrite(outLoci, loci)
? OrbisPlayGoErrorBadChunkId
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
loci[i] = PlayGoLocusLocalFast; loci[i] = PlayGoLocusLocalFast;
@@ -669,7 +673,8 @@ public static class PlayGoExports
{ {
lock (_stateGate) lock (_stateGate)
{ {
return Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0; return _metadata.ChunkIdKnowledge == PlayGoChunkIdKnowledge.Unknown ||
Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
} }
} }
@@ -680,7 +685,10 @@ public static class PlayGoExports
{ {
// No app0 override to probe for sidecar files: same fully-installed // No app0 override to probe for sidecar files: same fully-installed
// single-chunk fallback as below, or scePlayGoOpen fails fatally. // single-chunk fallback as below, or scePlayGoOpen fails fatally.
return new PlayGoMetadata(true, [(ushort)0]); return new PlayGoMetadata(
true,
[(ushort)0],
PlayGoChunkIdKnowledge.Authoritative);
} }
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat"); var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
@@ -695,19 +703,19 @@ public static class PlayGoExports
// init failure for UE titles); chunk 0 reports LocalFast and every other id // init failure for UE titles); chunk 0 reports LocalFast and every other id
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration. // returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
TracePlayGo("metadata_missing; fully-installed single chunk"); TracePlayGo("metadata_missing; fully-installed single chunk");
return new PlayGoMetadata(true, [(ushort)0]); return new PlayGoMetadata(
true,
[(ushort)0],
PlayGoChunkIdKnowledge.Authoritative);
} }
var chunkIds = LoadChunkIds(chunkDefsXml); var chunkIds = LoadChunkIds(chunkDefsXml);
if (chunkIds.Length == 0) return new PlayGoMetadata(
{ true,
// Unreadable/empty sidecar: fall back to chunk 0, not an empty set chunkIds,
// (which IsKnownChunkId would treat as "every id valid"). chunkIds.Length == 0
TracePlayGo("metadata_unreadable; fully-installed single chunk"); ? PlayGoChunkIdKnowledge.Unknown
return new PlayGoMetadata(true, [(ushort)0]); : PlayGoChunkIdKnowledge.Authoritative);
}
return new PlayGoMetadata(true, chunkIds);
} }
private static ushort[] LoadChunkIds(string chunkDefsXml) private static ushort[] LoadChunkIds(string chunkDefsXml)
@@ -720,6 +728,8 @@ public static class PlayGoExports
try try
{ {
var xml = File.ReadAllText(chunkDefsXml); var xml = File.ReadAllText(chunkDefsXml);
_ = XDocument.Parse(xml, LoadOptions.None);
var chunkIds = new HashSet<ushort>(); var chunkIds = new HashSet<ushort>();
AddChunkIds(xml, DefaultChunkPattern, chunkIds); AddChunkIds(xml, DefaultChunkPattern, chunkIds);
AddChunkIds(xml, ChunkIdPattern, chunkIds); AddChunkIds(xml, ChunkIdPattern, chunkIds);
@@ -736,6 +746,10 @@ public static class PlayGoExports
{ {
return Array.Empty<ushort>(); return Array.Empty<ushort>();
} }
catch (System.Xml.XmlException)
{
return Array.Empty<ushort>();
}
} }
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds) private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
@@ -774,8 +788,35 @@ public static class PlayGoExports
} }
} }
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);
} }
} }
@@ -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( [SysAbiExport(
Nid = "uquVH4-Du78", Nid = "uquVH4-Du78",
ExportName = "sceVideoOutClose", 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 // stays tighter than the drain budget because queued draws pin their
// pooled guest-data arrays until the render thread uploads them. // pooled guest-data arrays until the render thread uploads them.
private const int MaxPendingGuestWork = 64; 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. // A captured 4K flip can consume tens of MiB of device-local memory.
// Retain only a short presentation queue while always preserving the // Retain only a short presentation queue while always preserving the
// newest generation; older immutable versions are retired immediately. // newest generation; older immutable versions are retired immediately.
@@ -608,6 +609,42 @@ internal static unsafe class VulkanVideoPresenter
shaderAddress); 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( public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv, byte[] pixelSpirv,
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestDrawTexture> textures,
@@ -627,16 +664,13 @@ internal static unsafe class VulkanVideoPresenter
if (pixelSpirv.Length == 0 || if (pixelSpirv.Length == 0 ||
targets.Count == 0 || targets.Count == 0 ||
targets.Count > 8 || targets.Count > 8 ||
targets.Any(target => AnyRenderTargetInvalid(targets))
target.Address == 0 || target.Width == 0 || target.Height == 0))
{ {
return; return;
} }
var firstTarget = targets[0]; var firstTarget = targets[0];
if (targets.Any(target => if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets."); "[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; var effectiveRenderState = renderState ?? GuestRenderState.Default;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1) if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{ {
var broadcastBlends = new GuestBlendState[targets.Count];
Array.Fill(broadcastBlends, effectiveRenderState.Blends[0]);
effectiveRenderState = effectiveRenderState with effectiveRenderState = effectiveRenderState with
{ {
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(), Blends = broadcastBlends,
}; };
} }
lock (_gate) lock (_gate)
@@ -2177,6 +2213,12 @@ internal static unsafe class VulkanVideoPresenter
return keys; 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( private readonly record struct Presentation(
byte[]? Pixels, byte[]? Pixels,
uint Width, uint Width,
@@ -2357,9 +2399,7 @@ internal static unsafe class VulkanVideoPresenter
new(ReferenceEqualityComparer.Instance); new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<DescriptorLayoutKey, DescriptorLayoutBundle> private readonly Dictionary<DescriptorLayoutKey, DescriptorLayoutBundle>
_descriptorLayouts = new(); _descriptorLayouts = new();
private readonly Dictionary<HostBufferPoolKey, Stack<HostBufferAllocation>> private readonly VulkanHostBufferPool _hostBufferPool;
_hostBufferPool = new();
private readonly Dictionary<ulong, HostBufferAllocation> _hostBufferAllocations = new();
private readonly List<GuestBufferAllocation> _guestBufferAllocations = []; private readonly List<GuestBufferAllocation> _guestBufferAllocations = [];
private readonly Queue<PendingGuestSubmission> _pendingGuestSubmissions = new(); private readonly Queue<PendingGuestSubmission> _pendingGuestSubmissions = new();
private readonly Dictionary<string, ulong> _lastSubmittedTimelineByGuestQueue = private readonly Dictionary<string, ulong> _lastSubmittedTimelineByGuestQueue =
@@ -2381,10 +2421,6 @@ internal static unsafe class VulkanVideoPresenter
GuestRasterState Raster, GuestRasterState Raster,
GuestDepthState Depth); GuestDepthState Depth);
private readonly record struct HostBufferPoolKey(
BufferUsageFlags Usage,
ulong Capacity);
private readonly record struct DescriptorLayoutKey( private readonly record struct DescriptorLayoutKey(
ShaderStageFlags Stages, ShaderStageFlags Stages,
string Resources); string Resources);
@@ -2397,12 +2433,6 @@ internal static unsafe class VulkanVideoPresenter
DescriptorSetLayout DescriptorSetLayout, DescriptorSetLayout DescriptorSetLayout,
PipelineLayout PipelineLayout); PipelineLayout PipelineLayout);
private sealed record HostBufferAllocation(
VkBuffer Buffer,
DeviceMemory Memory,
HostBufferPoolKey Key,
nint Mapped);
private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length); private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length);
private sealed class GuestBufferAllocation private sealed class GuestBufferAllocation
@@ -2592,6 +2622,9 @@ internal static unsafe class VulkanVideoPresenter
public Presenter(uint width, uint height) public Presenter(uint width, uint height)
{ {
_hostBufferPool = new VulkanHostBufferPool(
MaximumCachedHostBufferBytes,
DestroyHostBufferAllocation);
var options = WindowOptions.DefaultVulkan; var options = WindowOptions.DefaultVulkan;
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight); options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
options.Title = VideoOutExports.GetWindowTitle(); options.Title = VideoOutExports.GetWindowTitle();
@@ -4372,23 +4405,29 @@ internal static unsafe class VulkanVideoPresenter
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) * var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
1000.0 / System.Diagnostics.Stopwatch.Frequency; 1000.0 / System.Diagnostics.Stopwatch.Frequency;
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader( TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " + $"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " + $"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " + $"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
$"waited_ms={waitedMs:F3}"); $"waited_ms={waitedMs:F3}");
} }
}
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work) private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{ {
WaitForActiveGuestQueueSubmissionsForCpuVisibility(); WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
work.Action(); work.Action();
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader( TraceVulkanShader(
$"vk.ordered_action queue={_activeGuestQueue.Name} " + $"vk.ordered_action queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " + $"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'"); $"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
} }
}
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work) private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
{ {
@@ -5316,7 +5355,8 @@ internal static unsafe class VulkanVideoPresenter
resources.IndexBuffer = CreateHostBuffer( resources.IndexBuffer = CreateHostBuffer(
indexBuffer.Data.AsSpan(0, indexBuffer.Length), indexBuffer.Data.AsSpan(0, indexBuffer.Length),
BufferUsageFlags.IndexBufferBit, BufferUsageFlags.IndexBufferBit,
out resources.IndexMemory); out resources.IndexMemory,
out _);
resources.Index32Bit = indexBuffer.Is32Bit; resources.Index32Bit = indexBuffer.Is32Bit;
if (indexBuffer.Pooled) if (indexBuffer.Pooled)
{ {
@@ -6839,7 +6879,8 @@ internal static unsafe class VulkanVideoPresenter
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
var expectedSize = GetTextureByteCount(texture.Format, rowLength, height); 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( Console.Error.WriteLine(
$"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " + $"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " +
@@ -7494,21 +7535,43 @@ internal static unsafe class VulkanVideoPresenter
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)); var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
var endAddress = checked(guestBuffer.BaseAddress + size); var endAddress = checked(guestBuffer.BaseAddress + size);
var allocation = _guestBufferAllocations GuestBufferAllocation? allocation = null;
.Where(candidate => var allocationPriority = -1;
candidate.BaseAddress <= guestBuffer.BaseAddress && // This runs for every bound global buffer. Preserve the previous
candidate.BaseAddress + candidate.Size >= endAddress) // stable queue preference without allocating LINQ sort state.
.OrderByDescending(candidate => foreach (var candidate in _guestBufferAllocations)
string.Equals( {
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
candidate.BaseAddress + candidate.Size < endAddress)
{
continue;
}
var candidatePriority = string.Equals(
candidate.QueueName, candidate.QueueName,
_activeGuestQueue.Name, _activeGuestQueue.Name,
StringComparison.Ordinal)) StringComparison.Ordinal)
.ThenByDescending(candidate => ? 2
string.Equals( : string.Equals(
candidate.QueueName, candidate.QueueName,
SharedReadOnlyGuestBufferQueue, SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal)) StringComparison.Ordinal)
.First(); ? 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 guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress;
var descriptorOffset = guestOffset & var descriptorOffset = guestOffset &
~(GuestStorageBufferOffsetAlignment - 1); ~(GuestStorageBufferOffsetAlignment - 1);
@@ -7557,16 +7620,15 @@ internal static unsafe class VulkanVideoPresenter
WaitForActiveGuestQueueSubmissionsForCpuVisibility(); WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
} }
var live = new byte[guestBuffer.Length]; var mapped = new Span<byte>(
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, live) == true) (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>( mapped.CopyTo(shadow);
(void*)(allocation.Mapped + checked((nint)guestOffset)),
source.Length));
source.CopyTo(shadow);
} }
if (ShouldTraceVulkanResources() && if (ShouldTraceVulkanResources() &&
@@ -7576,14 +7638,6 @@ internal static unsafe class VulkanVideoPresenter
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " + $"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
$"bytes={guestBuffer.Length}"); $"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) if (guestBuffer.Pooled)
{ {
GuestDataPool.Return(guestBuffer.Data); GuestDataPool.Return(guestBuffer.Data);
@@ -7611,8 +7665,8 @@ internal static unsafe class VulkanVideoPresenter
var buffer = CreateHostBuffer( var buffer = CreateHostBuffer(
guestBuffer.Data.AsSpan(0, guestBuffer.Length), guestBuffer.Data.AsSpan(0, guestBuffer.Length),
BufferUsageFlags.StorageBufferBit, BufferUsageFlags.StorageBufferBit,
out var memory); out var memory,
var allocation = _hostBufferAllocations[buffer.Handle]; out var mapped);
if (guestBuffer.Pooled) if (guestBuffer.Pooled)
{ {
GuestDataPool.Return(guestBuffer.Data); GuestDataPool.Return(guestBuffer.Data);
@@ -7625,7 +7679,7 @@ internal static unsafe class VulkanVideoPresenter
WriteBackToGuest = false, WriteBackToGuest = false,
Buffer = buffer, Buffer = buffer,
Memory = memory, Memory = memory,
Mapped = allocation.Mapped, Mapped = mapped,
Offset = 0, Offset = 0,
Size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)), Size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)),
GuestOffset = 0, GuestOffset = 0,
@@ -7845,7 +7899,8 @@ internal static unsafe class VulkanVideoPresenter
var buffer = CreateHostBuffer( var buffer = CreateHostBuffer(
source, source,
BufferUsageFlags.VertexBufferBit, BufferUsageFlags.VertexBufferBit,
out var memory); out var memory,
out _);
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)); var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
if (_setDebugUtilsObjectName is not null) if (_setDebugUtilsObjectName is not null)
{ {
@@ -7914,19 +7969,15 @@ internal static unsafe class VulkanVideoPresenter
private VkBuffer CreateHostBuffer( private VkBuffer CreateHostBuffer(
ReadOnlySpan<byte> data, ReadOnlySpan<byte> data,
BufferUsageFlags usage, BufferUsageFlags usage,
out DeviceMemory memory) out DeviceMemory memory,
out nint mapped)
{ {
var size = (ulong)Math.Max(data.Length, sizeof(uint)); var size = (ulong)Math.Max(data.Length, sizeof(uint));
var capacity = BitOperations.RoundUpToPowerOf2(size); var capacity = BitOperations.RoundUpToPowerOf2(size);
var key = new HostBufferPoolKey(usage, capacity); var key = new VulkanHostBufferPoolKey(usage, capacity);
if (!_hostBufferPool.TryGetValue(key, out var available))
{
available = new Stack<HostBufferAllocation>();
_hostBufferPool.Add(key, available);
}
HostBufferAllocation allocation; VulkanHostBufferAllocation allocation;
if (available.TryPop(out var pooled)) if (_hostBufferPool.TryRent(key, out var pooled))
{ {
allocation = pooled; allocation = pooled;
} }
@@ -7944,15 +7995,16 @@ internal static unsafe class VulkanVideoPresenter
Check( Check(
_vk.MapMemory(_device, allocatedMemory, 0, capacity, 0, &persistentMapping), _vk.MapMemory(_device, allocatedMemory, 0, capacity, 0, &persistentMapping),
"vkMapMemory(host persistent)"); "vkMapMemory(host persistent)");
allocation = new HostBufferAllocation( allocation = new VulkanHostBufferAllocation(
buffer, buffer,
allocatedMemory, allocatedMemory,
key, key,
(nint)persistentMapping); (nint)persistentMapping);
_hostBufferAllocations.Add(buffer.Handle, allocation); _hostBufferPool.Register(allocation);
} }
memory = allocation.Memory; memory = allocation.Memory;
mapped = allocation.Mapped;
fixed (byte* source = data) fixed (byte* source = data)
{ {
System.Buffer.MemoryCopy( System.Buffer.MemoryCopy(
@@ -7972,10 +8024,8 @@ internal static unsafe class VulkanVideoPresenter
return; return;
} }
if (_hostBufferAllocations.TryGetValue(buffer.Handle, out var allocation) && if (_hostBufferPool.Return(buffer, memory))
allocation.Memory.Handle == memory.Handle)
{ {
_hostBufferPool[allocation.Key].Push(allocation);
return; return;
} }
@@ -7986,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) => private static PrimitiveTopology GetPrimitiveTopology(uint primitiveType) =>
primitiveType switch primitiveType switch
{ {
@@ -9408,6 +9465,7 @@ internal static unsafe class VulkanVideoPresenter
{ {
var extent = new Extent2D(firstTarget.Width, firstTarget.Height); var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
var draw = work.Draw; var draw = work.Draw;
var clearDepthForDraw = draw.RenderState.Depth.ClearEnable;
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable) if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
{ {
draw = draw with draw = draw with
@@ -9420,9 +9478,10 @@ internal static unsafe class VulkanVideoPresenter
} }
GuestDepthResource? depth = null; GuestDepthResource? depth = null;
DepthFramebufferResource? depthFramebuffer = null; DepthFramebufferResource? depthFramebuffer = null;
if (work.DepthTarget is { } depthTarget && if (ShouldAttachGuestDepth(
(draw.RenderState.Depth.TestEnable || work.DepthTarget,
draw.RenderState.Depth.WriteEnable)) draw.RenderState.Depth) &&
work.DepthTarget is { } depthTarget)
{ {
var effectiveDepthTarget = depthTarget; var effectiveDepthTarget = depthTarget;
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height) if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
@@ -9469,21 +9528,45 @@ internal static unsafe class VulkanVideoPresenter
depth = GetOrCreateGuestDepth(effectiveDepthTarget); depth = GetOrCreateGuestDepth(effectiveDepthTarget);
PrepareFirstUseDepth(depth, draw.RenderState.Depth); PrepareFirstUseDepth(depth, draw.RenderState.Depth);
if (clearDepthForDraw)
{
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
}
if (targets.Length == 1) if (targets.Length == 1)
{ {
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth); 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 var renderPass = depthFramebuffer is null
? firstTarget.Initialized ? firstTarget.Initialized
? firstTarget.RenderPass ? firstTarget.RenderPass
: firstTarget.InitialRenderPass : firstTarget.InitialRenderPass
: firstTarget.Initialized : firstTarget.Initialized
? depth!.Initialized ? depth!.Initialized && !clearDepthForDraw
? depthFramebuffer.LoadRenderPass ? depthFramebuffer.LoadRenderPass
: depthFramebuffer.DepthClearRenderPass : depthFramebuffer.DepthClearRenderPass
: depth!.Initialized : depth!.Initialized && !clearDepthForDraw
? depthFramebuffer.ColorClearRenderPass ? depthFramebuffer.ColorClearRenderPass
: depthFramebuffer.BothClearRenderPass; : depthFramebuffer.BothClearRenderPass;
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer; var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
@@ -9499,7 +9582,7 @@ internal static unsafe class VulkanVideoPresenter
targets.Select(target => targets.Select(target =>
target.Initialized || target.InitialUploadPending).ToArray(), target.Initialized || target.InitialUploadPending).ToArray(),
depth, depth,
depth?.Initialized == true); depth?.Initialized == true && !clearDepthForDraw);
transientRenderPass = renderPass; transientRenderPass = renderPass;
transientFramebuffer = framebuffer; transientFramebuffer = framebuffer;
} }
@@ -9687,7 +9770,12 @@ internal static unsafe class VulkanVideoPresenter
if (depth is not null) if (depth is not null)
{ {
depth.Initialized = true; 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"; depth.InitializationSource = "translated-depth-write";
} }
@@ -9821,11 +9909,14 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
} }
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader( TraceVulkanShader(
$"vk.offscreen_draw mrt={targets.Length} " + $"vk.offscreen_draw mrt={targets.Length} " +
$"size={firstTarget.Width}x{firstTarget.Height} " + $"size={firstTarget.Width}x{firstTarget.Height} " +
$"textures={work.Draw.Textures.Count}"); $"textures={work.Draw.Textures.Count}");
} }
}
catch (Exception exception) catch (Exception exception)
{ {
if (TryMarkDeviceLost(exception)) if (TryMarkDeviceLost(exception))
@@ -13699,13 +13790,7 @@ internal static unsafe class VulkanVideoPresenter
DestroyGuestBufferAllocation(allocation); DestroyGuestBufferAllocation(allocation);
} }
_guestBufferAllocations.Clear(); _guestBufferAllocations.Clear();
foreach (var allocation in _hostBufferAllocations.Values) _hostBufferPool.Dispose();
{
_vk.DestroyBuffer(_device, allocation.Buffer, null);
_vk.FreeMemory(_device, allocation.Memory, null);
}
_hostBufferAllocations.Clear();
_hostBufferPool.Clear();
foreach (var guestImage in _guestImages.Values) foreach (var guestImage in _guestImages.Values)
{ {
DestroyGuestImage(guestImage); DestroyGuestImage(guestImage);
@@ -1802,24 +1802,6 @@ public static partial class Gen5SpirvTranslator
switch (instruction.Opcode) 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": case "DsWriteB32":
{ {
if (instruction.Sources.Count < 2) if (instruction.Sources.Count < 2)
@@ -1964,6 +1946,11 @@ public static partial class Gen5SpirvTranslator
return true; return true;
} }
default: default:
if (Gen5ShaderTranslator.IsDataShareAtomic(instruction.Opcode))
{
return TryEmitDataShareAtomic(instruction, control, out error);
}
error = $"unsupported LDS opcode {instruction.Opcode}"; error = $"unsupported LDS opcode {instruction.Opcode}";
return false; return false;
} }
@@ -2004,6 +1991,126 @@ public static partial class Gen5SpirvTranslator
Store(pointer, selected); 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( private bool TryEmitInterpolation(
Gen5ShaderInstruction instruction, Gen5ShaderInstruction instruction,
Gen5InterpolationControl interpolation, Gen5InterpolationControl interpolation,
@@ -2239,22 +2346,27 @@ public static partial class Gen5SpirvTranslator
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress); byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); 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(() => EmitExecConditional(() =>
{ {
var inRange = IsBufferWordInRange(bindingIndex, dwordAddress); var inRange = IsBufferWordInRange(bindingIndex, dwordAddress);
EmitConditional(inRange, () => EmitConditional(inRange, () =>
{ {
var original = _module.AddInstruction( var original = EmitAtomic(
instruction.Opcode == "BufferAtomicAdd" atomicOp,
? SpirvOp.AtomicIAdd
: SpirvOp.AtomicUMax,
_uintType, _uintType,
BufferWordPointer(bindingIndex, dwordAddress), BufferWordPointer(bindingIndex, dwordAddress),
UInt(1), scope: 1,
UInt(0x48), semantics: 0x48,
LoadV(control.VectorData)); value: () => LoadV(control.VectorData),
comparator: () => LoadV(control.VectorData + 1));
if (control.Glc) if (control.Glc)
{ {
StoreV(control.VectorData, original); StoreV(control.VectorData, original);
@@ -3196,6 +3308,60 @@ public static partial class Gen5SpirvTranslator
return true; 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 && if (resource.IsStorage &&
instruction.Opcode is not ("ImageLoad" or "ImageLoadMip")) instruction.Opcode is not ("ImageLoad" or "ImageLoadMip"))
{ {
@@ -40,6 +40,7 @@ public enum SpirvOp : ushort
FunctionEnd = 56, FunctionEnd = 56,
FunctionCall = 57, FunctionCall = 57,
Variable = 59, Variable = 59,
ImageTexelPointer = 60,
Load = 61, Load = 61,
Store = 62, Store = 62,
AccessChain = 65, AccessChain = 65,
@@ -142,8 +143,19 @@ public enum SpirvOp : ushort
BitCount = 205, BitCount = 205,
ControlBarrier = 224, ControlBarrier = 224,
MemoryBarrier = 225, MemoryBarrier = 225,
AtomicExchange = 229,
AtomicCompareExchange = 230,
AtomicIIncrement = 232,
AtomicIDecrement = 233,
AtomicIAdd = 234, AtomicIAdd = 234,
AtomicISub = 235,
AtomicSMin = 236,
AtomicUMin = 237,
AtomicSMax = 238,
AtomicUMax = 239, AtomicUMax = 239,
AtomicAnd = 240,
AtomicOr = 241,
AtomicXor = 242,
Phi = 245, Phi = 245,
LoopMerge = 246, LoopMerge = 246,
SelectionMerge = 247, SelectionMerge = 247,
@@ -1172,9 +1172,33 @@ public static class Gen5ShaderTranslator
name = opcode switch name = opcode switch
{ {
0x00 => "DsAddU32", 0x00 => "DsAddU32",
0x01 => "DsSubU32",
0x03 => "DsIncU32",
0x04 => "DsDecU32",
0x05 => "DsMinI32",
0x06 => "DsMaxI32",
0x07 => "DsMinU32",
0x08 => "DsMaxU32",
0x09 => "DsAndB32",
0x0A => "DsOrB32",
0x0B => "DsXorB32",
0x0D => "DsWriteB32", 0x0D => "DsWriteB32",
0x0E => "DsWrite2B32", 0x0E => "DsWrite2B32",
0x0F => "DsWrite2St64B32", 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", 0x35 => "DsSwizzleB32",
0x36 => "DsReadB32", 0x36 => "DsReadB32",
0x37 => "DsRead2B32", 0x37 => "DsRead2B32",
@@ -1272,8 +1296,19 @@ public static class Gen5ShaderTranslator
0x23 => "BufferLoadSbyteD16Hi", 0x23 => "BufferLoadSbyteD16Hi",
0x24 => "BufferLoadShortD16", 0x24 => "BufferLoadShortD16",
0x25 => "BufferLoadShortD16Hi", 0x25 => "BufferLoadShortD16Hi",
0x30 => "BufferAtomicSwap",
0x31 => "BufferAtomicCmpswap",
0x32 => "BufferAtomicAdd", 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}", _ => $"MubufRaw{opcode:X2}",
}; };
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u; sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
@@ -1388,7 +1423,18 @@ public static class Gen5ShaderTranslator
0x08 => "ImageStore", 0x08 => "ImageStore",
0x09 => "ImageStoreMip", 0x09 => "ImageStoreMip",
0x0E => "ImageGetResinfo", 0x0E => "ImageGetResinfo",
0x0F => "ImageAtomicSwap",
0x10 => "ImageAtomicCmpswap", 0x10 => "ImageAtomicCmpswap",
0x11 => "ImageAtomicAdd",
0x12 => "ImageAtomicSub",
0x14 => "ImageAtomicSmin",
0x15 => "ImageAtomicUmin",
0x16 => "ImageAtomicSmax",
0x17 => "ImageAtomicUmax",
0x18 => "ImageAtomicAnd",
0x19 => "ImageAtomicOr",
0x1A => "ImageAtomicXor",
0x1B => "ImageAtomicInc",
0x1C => "ImageAtomicDec", 0x1C => "ImageAtomicDec",
0x20 => "ImageSample", 0x20 => "ImageSample",
0x22 => "ImageSampleD", 0x22 => "ImageSampleD",
@@ -1488,6 +1534,18 @@ public static class Gen5ShaderTranslator
name.StartsWith("ImageStore", StringComparison.Ordinal) || name.StartsWith("ImageStore", StringComparison.Ordinal) ||
name.StartsWith("ImageAtomic", 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( private static Gen5ShaderInstruction CreateInstruction(
uint pc, uint pc,
Gen5ShaderEncoding encoding, Gen5ShaderEncoding encoding,
@@ -1794,10 +1852,6 @@ public static class Gen5ShaderTranslator
((word >> 17) & 1) != 0); ((word >> 17) & 1) != 0);
sources = opcode switch sources = opcode switch
{ {
"DsAddU32" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
],
"DsWriteB32" => [ "DsWriteB32" => [
Gen5Operand.Vector(vectorAddress), Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0), Gen5Operand.Vector(vectorData0),
@@ -1826,6 +1880,17 @@ public static class Gen5ShaderTranslator
Gen5Operand.Vector(vectorData1), Gen5Operand.Vector(vectorData1),
], ],
"DsSwizzleB32" => [Gen5Operand.Vector(vectorData0)], "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)], _ => [Gen5Operand.Vector(vectorAddress)],
}; };
destinations = opcode switch destinations = opcode switch
@@ -1848,6 +1913,10 @@ public static class Gen5ShaderTranslator
Gen5Operand.Vector(vectorDestination + 2), Gen5Operand.Vector(vectorDestination + 2),
Gen5Operand.Vector(vectorDestination + 3), Gen5Operand.Vector(vectorDestination + 3),
], ],
_ when IsDataShareAtomic(opcode) &&
opcode.Contains("Rtn", StringComparison.Ordinal) => [
Gen5Operand.Vector(vectorDestination),
],
_ => [], _ => [],
}; };
break; break;
@@ -1953,8 +2022,8 @@ public static class Gen5ShaderTranslator
"BufferStoreDwordx2" => 2u, "BufferStoreDwordx2" => 2u,
"BufferStoreDwordx3" => 3u, "BufferStoreDwordx3" => 3u,
"BufferStoreDwordx4" => 4u, "BufferStoreDwordx4" => 4u,
"BufferAtomicAdd" => 1u, "BufferAtomicCmpswap" => 2u,
"BufferAtomicUMax" => 1u, _ when opcode.StartsWith("BufferAtomic", StringComparison.Ordinal) => 1u,
_ => 0u, _ => 0u,
}; };
sources = 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)}.");
}
}
@@ -45,6 +45,11 @@ public sealed class KernelMemoryCompatExportsTests
unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)), unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)),
0); 0);
var previousCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("es-ES");
var result = KernelMemoryCompatExports.Sprintf(context); var result = KernelMemoryCompatExports.Sprintf(context);
Assert.Equal(0, result); Assert.Equal(0, result);
@@ -53,35 +58,6 @@ public sealed class KernelMemoryCompatExportsTests
Assert.True(memory.TryRead(destinationAddress, output)); Assert.True(memory.TryRead(destinationAddress, output));
Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output)); Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output));
} }
[Fact]
public void Sprintf_UsesCLocaleForFloatingPoint()
{
var previousCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");
const ulong memoryBase = 0x1_0000_0000;
const ulong destinationAddress = memoryBase + 0x100;
const ulong formatAddress = memoryBase + 0x200;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(formatAddress, "%.4f");
context[CpuRegister.Rdi] = destinationAddress;
context[CpuRegister.Rsi] = formatAddress;
context.SetXmmRegister(
0,
unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)),
0);
var result = KernelMemoryCompatExports.Sprintf(context);
Assert.Equal(0, result);
Span<byte> output = stackalloc byte[7];
Assert.True(memory.TryRead(destinationAddress, output));
Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output));
}
finally finally
{ {
CultureInfo.CurrentCulture = previousCulture; 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);
}