mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-28 13:40:58 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 487bda6a32 |
@@ -89,6 +89,7 @@ jobs:
|
|||||||
DOTNET_NOLOGO: true
|
DOTNET_NOLOGO: true
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||||
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -120,13 +121,24 @@ jobs:
|
|||||||
- name: Publish win-x64 CLI
|
- name: Publish win-x64 CLI
|
||||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||||
|
|
||||||
|
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||||
|
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||||
|
if (Test-Path $archivePath) {
|
||||||
|
Remove-Item $archivePath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.PUBLISH_DIR }}
|
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
build-posix:
|
build-posix:
|
||||||
name: Build ${{ matrix.rid }}
|
name: Build ${{ matrix.rid }}
|
||||||
@@ -146,6 +158,7 @@ jobs:
|
|||||||
DOTNET_NOLOGO: true
|
DOTNET_NOLOGO: true
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
|
||||||
|
RELEASE_DIR: ${{ github.workspace }}/artifacts/release
|
||||||
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
|
||||||
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
|
||||||
SPIRV_TARGET_ENV: vulkan1.2
|
SPIRV_TARGET_ENV: vulkan1.2
|
||||||
@@ -210,13 +223,19 @@ jobs:
|
|||||||
if: matrix.rid == 'osx-x64'
|
if: matrix.rid == 'osx-x64'
|
||||||
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
|
||||||
|
|
||||||
|
- name: Create release archive
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
# tar keeps the executable bit, which zip would drop.
|
||||||
|
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||||
|
-C "$PUBLISH_DIR" .
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||||
path: ${{ env.PUBLISH_DIR }}
|
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Publish GitHub Release
|
name: Publish GitHub Release
|
||||||
@@ -236,28 +255,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Package release assets
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
|
|
||||||
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
|
|
||||||
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
|
|
||||||
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
|
|
||||||
test -d "${package_dir}"
|
|
||||||
done
|
|
||||||
|
|
||||||
mkdir -p release-assets
|
|
||||||
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
|
|
||||||
|
|
||||||
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
|
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -267,9 +264,9 @@ jobs:
|
|||||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||||
if [ "${#assets[@]}" -ne 3 ]; then
|
if [ "${#assets[@]}" -eq 0 ]; then
|
||||||
echo "Expected 3 release assets, found ${#assets[@]}." >&2
|
echo "No release assets found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,3 @@ ehthumbs.db
|
|||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Guest write watch
|
|
||||||
|
|
||||||
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
|
|
||||||
code and HLE code that damage guest memory. The tool starts only if you set one
|
|
||||||
or more `SHARPEMU_WATCH_*` environment variables.
|
|
||||||
|
|
||||||
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
|
|
||||||
does not monitor stores that native guest code makes directly. Use a platform
|
|
||||||
debugger or a hardware watchpoint to monitor these stores.
|
|
||||||
|
|
||||||
## Watch modes
|
|
||||||
|
|
||||||
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
|
|
||||||
block at the specified guest address.
|
|
||||||
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
|
|
||||||
monitors the first 64 direct mappings that have a size of 64 KiB and
|
|
||||||
protection value `0xF2`.
|
|
||||||
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
|
|
||||||
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
|
|
||||||
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
|
|
||||||
memory range. The tool logs a maximum of 128 entries for each process.
|
|
||||||
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
|
|
||||||
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
|
|
||||||
logs a maximum of 64 entries for each process.
|
|
||||||
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
|
|
||||||
specified upper 32 bits in the destination address.
|
|
||||||
|
|
||||||
For each match, the tool logs the destination address, the data pattern, and the
|
|
||||||
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
|
|
||||||
tag.
|
|
||||||
|
|
||||||
Use these variables together to scan bulk writes in the
|
|
||||||
`0x00000080xxxxxxxx` region.
|
|
||||||
|
|
||||||
macOS and Linux:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
SHARPEMU_WATCH_BULK_TORN=1 \
|
|
||||||
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
|
|
||||||
SharpEmu /path/to/eboot.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:SHARPEMU_WATCH_BULK_TORN = "1"
|
|
||||||
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
|
|
||||||
& .\SharpEmu.exe C:\path\to\game\eboot.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
|
|
||||||
address from a crash dump.
|
|
||||||
@@ -607,7 +607,7 @@ internal static partial class Program
|
|||||||
nint jobHandle = 0;
|
nint jobHandle = 0;
|
||||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
var created = CreateProcessW(
|
var created = CreateProcessW(
|
||||||
null,
|
processPath,
|
||||||
cmdLineBuilder,
|
cmdLineBuilder,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -1433,7 +1433,7 @@ internal static partial class Program
|
|||||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CreateProcessW(
|
private static extern bool CreateProcessW(
|
||||||
string? applicationName,
|
string applicationName,
|
||||||
StringBuilder commandLine,
|
StringBuilder commandLine,
|
||||||
nint processAttributes,
|
nint processAttributes,
|
||||||
nint threadAttributes,
|
nint threadAttributes,
|
||||||
|
|||||||
@@ -13,9 +13,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
</application>
|
</application>
|
||||||
</compatibility>
|
</compatibility>
|
||||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<asmv3:windowsSettings>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Emulation;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ
|
|
||||||
/// (immediate-form) instructions.
|
|
||||||
///
|
|
||||||
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2
|
|
||||||
/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple
|
|
||||||
/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the
|
|
||||||
/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time
|
|
||||||
/// (see <see cref="Native.Sse4aExtrqBlendPatch"/>), but any other occurrence of EXTRQ/INSERTQ -
|
|
||||||
/// a different register allocation, a title built with a different compiler version, and so on
|
|
||||||
/// - still aborts the title. This class ported from Kyty's
|
|
||||||
/// <c>Loader::X64InstructionEmulator::TryEmulateSse4a</c> provides the general bit-field
|
|
||||||
/// extract/insert so the illegal-instruction handler can finish *any* immediate-form
|
|
||||||
/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern.
|
|
||||||
///
|
|
||||||
/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit
|
|
||||||
/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend
|
|
||||||
/// adapter (<see cref="Native.DirectExecutionBackend"/>).
|
|
||||||
/// </summary>
|
|
||||||
public static class Sse4aBitFieldEmulator
|
|
||||||
{
|
|
||||||
public static bool IsValidBitField(int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong ExtractBitField(ulong value, int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
if (!IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
|
||||||
return (value >> idx) & mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
|
|
||||||
{
|
|
||||||
var len = length & 0x3F;
|
|
||||||
var idx = index & 0x3F;
|
|
||||||
if (!IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
|
|
||||||
var destinationClearMask = fieldMask << idx;
|
|
||||||
var sourceField = (source & fieldMask) << idx;
|
|
||||||
return (destination & ~destinationClearMask) | sourceField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using Iced.Intel;
|
|
||||||
using SharpEmu.Core.Cpu.Emulation;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
|
||||||
|
|
||||||
// General software fallback for the AMD-only instructions PS5 titles occasionally emit that a
|
|
||||||
// Zen 2-only host implements but Intel hosts (and Rosetta 2 on Apple Silicon) do not:
|
|
||||||
// - SSE4a EXTRQ/INSERTQ, immediate form
|
|
||||||
// - MONITORX/MWAITX
|
|
||||||
//
|
|
||||||
// This is a direct port of Kyty's Loader::X64InstructionEmulator (TryEmulateSse4a /
|
|
||||||
// TryEmulateMonitorxMwaitx). SharpEmu already special-cases exactly one compiled EXTRQ+VPBLENDD
|
|
||||||
// byte sequence at load time (Sse4aExtrqBlendPatch), which only helps the one idiom it was
|
|
||||||
// reverse-engineered from. This file is a general, fault-time fallback that engages for any
|
|
||||||
// immediate-form EXTRQ/INSERTQ or MONITORX/MWAITX the narrower patch (or a title using a
|
|
||||||
// different compiler/register allocation) does not cover, complementing rather than replacing
|
|
||||||
// it: the load-time patch still avoids paying the fault-and-recover cost on the hot path it was
|
|
||||||
// built for, while this method is the safety net for everything else.
|
|
||||||
//
|
|
||||||
// This is deliberately additive: DirectExecutionBackend.IllegalInstruction.cs (the BMI1/BMI2/ABM
|
|
||||||
// fallback) is untouched, and this method is only reached from VectoredHandler after that one
|
|
||||||
// has already declined to handle the fault.
|
|
||||||
public sealed partial class DirectExecutionBackend
|
|
||||||
{
|
|
||||||
// Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE
|
|
||||||
// image) starts right after Rip at offset 256, and XmmRegisters[0] sits 160 bytes into that
|
|
||||||
// area (32-byte header + 8 legacy x87/MMX slots x 16 bytes). 256 + 160 = 416 (0x1A0). Cross-
|
|
||||||
// checked against this file's own Win64ContextSize (0x4D0): rebuilding the whole CONTEXT
|
|
||||||
// layout field-by-field from offset 0 lands on the same 0x4D0 total, which would not happen
|
|
||||||
// if this offset (or anything before it) were wrong.
|
|
||||||
private const int Win64ContextXmm0Offset = 0x1A0;
|
|
||||||
|
|
||||||
private static int _sse4aSoftwareFallbackAnnounced;
|
|
||||||
private static long _sse4aInstructionsEmulated;
|
|
||||||
private static int _monitorxSoftwareFallbackAnnounced;
|
|
||||||
private static long _monitorxInstructionsEmulated;
|
|
||||||
|
|
||||||
private unsafe bool TryRecoverAmdCompatInstruction(void* contextRecord, ulong rip)
|
|
||||||
{
|
|
||||||
if (TryRecoverMonitorxMwaitx(contextRecord, rip))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MONITORX/MWAITX above only ever reads guest code memory and rewrites RIP, both of
|
|
||||||
// which the POSIX signal bridge (DirectExecutionBackend.PosixSignals.cs) faithfully
|
|
||||||
// round-trips through the real ucontext, so it works on every supported OS. EXTRQ/
|
|
||||||
// INSERTQ additionally read and write an XMM register: on Windows contextRecord is the
|
|
||||||
// live CONTEXT the OS resumes the thread from, so touching the Xmm0.. slots is visible
|
|
||||||
// to the guest, but on POSIX contextRecord is a CONTEXT-shaped scratch buffer that the
|
|
||||||
// bridge only populates with the 17 general-purpose registers - the XMM region is never
|
|
||||||
// read from or written back to the real mcontext/ucontext. Running this on POSIX would
|
|
||||||
// silently compute a result from stale/zeroed XMM bytes and then discard whatever it
|
|
||||||
// "wrote", so keep it Windows-only, matching Kyty's own scope for the identical fix.
|
|
||||||
return OperatingSystem.IsWindows() && TryRecoverSse4aExtractInsert(contextRecord, rip);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip)
|
|
||||||
{
|
|
||||||
// MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no
|
|
||||||
// ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous.
|
|
||||||
var opcode = new byte[3];
|
|
||||||
if (!TryReadHostBytes(rip, opcode) ||
|
|
||||||
opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PS5 titles use this pair in idle/wait loops: MONITORX arms a monitor on a cache line
|
|
||||||
// and MWAITX blocks until that line is written (or a timeout elapses). Hosts without
|
|
||||||
// the extension raise #UD on either one. We do not model the monitor itself, only its
|
|
||||||
// observable effect on guest forward progress: MONITORX becomes a no-op (arming a
|
|
||||||
// watch we never honour has no side effect of its own) and MWAITX becomes a plain
|
|
||||||
// thread yield, i.e. treat the awaited condition as already satisfied so the guest
|
|
||||||
// loop keeps making progress instead of executing an illegal opcode forever.
|
|
||||||
if (opcode[2] == 0xFB)
|
|
||||||
{
|
|
||||||
Thread.Yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, CTX_RIP, rip + 3);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _monitorxInstructionsEmulated);
|
|
||||||
if (Interlocked.Exchange(ref _monitorxSoftwareFallbackAnnounced, 1) == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][INFO] Host lacks AMD MONITORX/MWAITX used by the guest; " +
|
|
||||||
"emulating those instructions in software.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip)
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows() || !TryReadFaultingInstruction(rip, out var instruction))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isExtrq = instruction.Mnemonic == Mnemonic.Extrq;
|
|
||||||
var isInsertq = instruction.Mnemonic == Mnemonic.Insertq;
|
|
||||||
if (!isExtrq && !isInsertq)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExtrq && instruction.OpCount != 3 || isInsertq && instruction.OpCount != 4)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.GetOpKind(0) != OpKind.Register ||
|
|
||||||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var destLow = ReadCtxU64(contextRecord, destOffset);
|
|
||||||
if (isExtrq)
|
|
||||||
{
|
|
||||||
var length = (int)instruction.GetImmediate(1);
|
|
||||||
var index = (int)instruction.GetImmediate(2);
|
|
||||||
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
|
|
||||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (instruction.GetOpKind(1) != OpKind.Register ||
|
|
||||||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var length = (int)instruction.GetImmediate(2);
|
|
||||||
var index = (int)instruction.GetImmediate(3);
|
|
||||||
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
|
|
||||||
WriteCtxU64(contextRecord, destOffset + 8, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _sse4aInstructionsEmulated);
|
|
||||||
if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " +
|
|
||||||
"emulating those instructions in software.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an
|
|
||||||
// explicit switch (rather than arithmetic on the Register enum) to match the style already
|
|
||||||
// used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs.
|
|
||||||
private static bool TryGetXmmOffset(Register register, out int offset)
|
|
||||||
{
|
|
||||||
switch (register)
|
|
||||||
{
|
|
||||||
case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true;
|
|
||||||
case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true;
|
|
||||||
case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true;
|
|
||||||
case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true;
|
|
||||||
case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true;
|
|
||||||
case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true;
|
|
||||||
case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true;
|
|
||||||
case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true;
|
|
||||||
case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true;
|
|
||||||
case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true;
|
|
||||||
case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true;
|
|
||||||
case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true;
|
|
||||||
case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true;
|
|
||||||
case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true;
|
|
||||||
case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true;
|
|
||||||
case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true;
|
|
||||||
default:
|
|
||||||
offset = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -133,11 +133,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (exceptionCode == StatusIllegalInstruction &&
|
|
||||||
TryRecoverAmdCompatInstruction(contextRecord, rip))
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (IsBenignHostDebugException(exceptionCode))
|
if (IsBenignHostDebugException(exceptionCode))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -530,12 +530,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
{
|
cpuContext,
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||||
cpuContext,
|
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
|
||||||
}
|
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
if (dispatchResolved &&
|
if (dispatchResolved &&
|
||||||
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||||
@@ -1329,12 +1326,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
{
|
cpuContext,
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||||
cpuContext,
|
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
|
||||||
}
|
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
|
|
||||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
@@ -1444,9 +1438,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
var expectedSemaphoreTrywaitAgain =
|
|
||||||
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
|
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
||||||
var expectedNetAcceptWouldBlock =
|
var expectedNetAcceptWouldBlock =
|
||||||
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
||||||
resultValue == unchecked((int)0x80410123);
|
resultValue == unchecked((int)0x80410123);
|
||||||
@@ -1460,7 +1451,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
!expectedTimedWaitTimeout &&
|
!expectedTimedWaitTimeout &&
|
||||||
!expectedEqueueTimeout &&
|
!expectedEqueueTimeout &&
|
||||||
!expectedMutexTrylockBusy &&
|
!expectedMutexTrylockBusy &&
|
||||||
!expectedSemaphoreTrywaitAgain &&
|
|
||||||
!expectedNetAcceptWouldBlock &&
|
!expectedNetAcceptWouldBlock &&
|
||||||
!expectedUserServiceNoEvent &&
|
!expectedUserServiceNoEvent &&
|
||||||
!expectedPrivacyInvalidParameter)
|
!expectedPrivacyInvalidParameter)
|
||||||
|
|||||||
@@ -712,11 +712,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
||||||
|
|
||||||
// Import dispatch is the hottest managed path in UE titles. Most imports do
|
|
||||||
// not have an exception queued, so publish the dictionary population and let
|
|
||||||
// safe points skip _guestThreadGate entirely in the common case.
|
|
||||||
private int _pendingGuestExceptionCount;
|
|
||||||
|
|
||||||
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
||||||
|
|
||||||
private int _guestThreadPumpDepth;
|
private int _guestThreadPumpDepth;
|
||||||
@@ -3953,10 +3948,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// unwinding. Unity can begin its next stop-the-world cycle in
|
// unwinding. Unity can begin its next stop-the-world cycle in
|
||||||
// that window; treating the new raise as part of the old delivery
|
// that window; treating the new raise as part of the old delivery
|
||||||
// strands the collector waiting for an acknowledgement.
|
// strands the collector waiting for an acknowledgement.
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase));
|
external.ExceptionStackBase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3965,10 +3960,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// managed thread corrupts the worker's control state. Queue the
|
// managed thread corrupts the worker's control state. Queue the
|
||||||
// request and let that exact executor consume it at its next HLE
|
// request and let that exact executor consume it at its next HLE
|
||||||
// boundary, where the original guest thread is safely paused.
|
// boundary, where the original guest thread is safely paused.
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase));
|
external.ExceptionStackBase);
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4013,17 +4008,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
if (target.ExceptionDeliveryActive)
|
if (target.ExceptionDeliveryActive)
|
||||||
{
|
{
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase));
|
exceptionStackBase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase));
|
exceptionStackBase);
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4184,7 +4179,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
RestoreInterruptedGuestThread();
|
RestoreInterruptedGuestThread();
|
||||||
if (target.State == GuestThreadRunState.Blocked &&
|
if (target.State == GuestThreadRunState.Blocked &&
|
||||||
!target.ExecutorActive &&
|
!target.ExecutorActive &&
|
||||||
TryRemovePendingGuestExceptionLocked(threadHandle, out var queued))
|
_pendingGuestExceptions.Remove(threadHandle, out var queued))
|
||||||
{
|
{
|
||||||
followUp = queued;
|
followUp = queued;
|
||||||
}
|
}
|
||||||
@@ -4270,11 +4265,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
CpuContext currentContext,
|
CpuContext currentContext,
|
||||||
GuestCpuContinuation interruptedContinuation)
|
GuestCpuContinuation interruptedContinuation)
|
||||||
{
|
{
|
||||||
if (Volatile.Read(ref _pendingGuestExceptionCount) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
if (threadHandle == 0)
|
if (threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -4288,7 +4278,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending))
|
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4350,27 +4340,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueuePendingGuestExceptionLocked(
|
|
||||||
ulong threadHandle,
|
|
||||||
PendingGuestException pending)
|
|
||||||
{
|
|
||||||
_pendingGuestExceptions[threadHandle] = pending;
|
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryRemovePendingGuestExceptionLocked(
|
|
||||||
ulong threadHandle,
|
|
||||||
out PendingGuestException pending)
|
|
||||||
{
|
|
||||||
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteGuestExceptionContext(
|
private static bool TryWriteGuestExceptionContext(
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -4465,7 +4434,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_guestThreads.Clear();
|
_guestThreads.Clear();
|
||||||
_externalGuestThreads.Clear();
|
_externalGuestThreads.Clear();
|
||||||
_pendingGuestExceptions.Clear();
|
_pendingGuestExceptions.Clear();
|
||||||
Volatile.Write(ref _pendingGuestExceptionCount, 0);
|
|
||||||
_activeGuestExceptionDeliveries.Clear();
|
_activeGuestExceptionDeliveries.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
|
|
||||||
_inner.TryCopy(destinationAddress, sourceAddress, length);
|
|
||||||
|
|
||||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
{
|
{
|
||||||
if (_inner is IGuestMemoryAllocator allocator)
|
if (_inner is IGuestMemoryAllocator allocator)
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static CommittedRangeCache? _committedRangeCache;
|
|
||||||
|
|
||||||
private long _mappingGeneration;
|
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
@@ -33,77 +28,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
private const int CommittedRangeCacheCapacity = 4;
|
|
||||||
|
|
||||||
private sealed class CommittedRangeCache
|
|
||||||
{
|
|
||||||
private readonly CommittedRange[] _ranges = new CommittedRange[CommittedRangeCacheCapacity];
|
|
||||||
private PhysicalVirtualMemory? _owner;
|
|
||||||
private long _generation;
|
|
||||||
private int _count;
|
|
||||||
private int _nextReplacement;
|
|
||||||
|
|
||||||
public bool Contains(
|
|
||||||
PhysicalVirtualMemory owner,
|
|
||||||
long generation,
|
|
||||||
ulong start,
|
|
||||||
ulong end)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = 0; index < _count; index++)
|
|
||||||
{
|
|
||||||
var range = _ranges[index];
|
|
||||||
if (start >= range.Start && end <= range.End)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(
|
|
||||||
PhysicalVirtualMemory owner,
|
|
||||||
long generation,
|
|
||||||
ulong start,
|
|
||||||
ulong end)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_owner, owner) || _generation != generation)
|
|
||||||
{
|
|
||||||
_owner = owner;
|
|
||||||
_generation = generation;
|
|
||||||
_count = 0;
|
|
||||||
_nextReplacement = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index = 0; index < _count; index++)
|
|
||||||
{
|
|
||||||
var range = _ranges[index];
|
|
||||||
if (start <= range.End && end >= range.Start)
|
|
||||||
{
|
|
||||||
_ranges[index] = new CommittedRange(
|
|
||||||
Math.Min(start, range.Start),
|
|
||||||
Math.Max(end, range.End));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_count < _ranges.Length)
|
|
||||||
{
|
|
||||||
_ranges[_count++] = new CommittedRange(start, end);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ranges[_nextReplacement] = new CommittedRange(start, end);
|
|
||||||
_nextReplacement = (_nextReplacement + 1) % _ranges.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct CommittedRange(ulong Start, ulong End);
|
|
||||||
|
|
||||||
// Raw Windows PAGE_* values retained for the internal region/protection
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
// bookkeeping: regions and saved old-protection values always carry the raw
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
@@ -425,87 +349,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
|
||||||
{
|
|
||||||
if (size == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = AlignDown(address, PageSize);
|
|
||||||
var end = AlignUp(address + size, PageSize);
|
|
||||||
if (end <= start)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
|
||||||
|
|
||||||
// Walk the range page-run by page-run. VirtualQuery reports the largest run
|
|
||||||
// of same-state pages from the queried address, so a single query advances
|
|
||||||
// us over whole free or occupied stretches. Only free stretches get backed;
|
|
||||||
// stretches already reserved or committed by another allocation are left as
|
|
||||||
// they are, which is exactly what a fixed mapping does on hardware.
|
|
||||||
var cursor = start;
|
|
||||||
var backedAny = false;
|
|
||||||
while (cursor < end)
|
|
||||||
{
|
|
||||||
if (!_hostMemory.Query(cursor, out var info))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
|
||||||
? ulong.MaxValue
|
|
||||||
: info.BaseAddress + info.RegionSize;
|
|
||||||
var runEnd = Math.Min(end, queriedEnd);
|
|
||||||
if (runEnd <= cursor)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.State == HostRegionState.Free)
|
|
||||||
{
|
|
||||||
var runSize = runEnd - cursor;
|
|
||||||
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
|
|
||||||
if (allocated != cursor)
|
|
||||||
{
|
|
||||||
if (allocated != 0)
|
|
||||||
{
|
|
||||||
_hostMemory.Free(allocated);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
|
||||||
_gate.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InsertRegionSorted(new MemoryRegion
|
|
||||||
{
|
|
||||||
VirtualAddress = cursor,
|
|
||||||
Size = runSize,
|
|
||||||
IsExecutable = executable,
|
|
||||||
IsReservedOnly = false,
|
|
||||||
Protection = protection
|
|
||||||
});
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_gate.ExitWriteLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
|
||||||
backedAny = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = runEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
return backedAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAllocateAtOrAbove(
|
public bool TryAllocateAtOrAbove(
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
ulong size,
|
ulong size,
|
||||||
@@ -597,7 +440,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.ExitWriteLock();
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Increment(ref _mappingGeneration);
|
|
||||||
_hostMemory.Free(address);
|
_hostMemory.Free(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,7 +611,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
_allocationSearchHints.Clear();
|
_allocationSearchHints.Clear();
|
||||||
}
|
}
|
||||||
Interlocked.Increment(ref _mappingGeneration);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1078,7 +919,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1104,68 +944,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NotifyGuestWriteWatch(ulong virtualAddress, ReadOnlySpan<byte> source)
|
|
||||||
{
|
|
||||||
if (GuestWriteWatch.Armed)
|
|
||||||
{
|
|
||||||
GuestWriteWatch.Check(virtualAddress, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length)
|
|
||||||
{
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (length > int.MaxValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match TryWrite's managed-write notification before touching an
|
|
||||||
// identity-mapped guest page protected by the image tracker.
|
|
||||||
GuestImageWriteTracker.NotifyManagedWrite(destinationAddress, length);
|
|
||||||
|
|
||||||
_gate.EnterReadLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sourceRegion = FindRegion(sourceAddress, length);
|
|
||||||
var destinationRegion = FindRegion(destinationAddress, length);
|
|
||||||
if (sourceRegion is null || destinationRegion is null ||
|
|
||||||
!TryResolveRegionOffset(sourceAddress, length, sourceRegion, out var sourceOffset) ||
|
|
||||||
!TryResolveRegionOffset(destinationAddress, length, destinationRegion, out var destinationOffset))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourcePointer = sourceRegion.VirtualAddress + sourceOffset;
|
|
||||||
var destinationPointer = destinationRegion.VirtualAddress + destinationOffset;
|
|
||||||
if ((sourceRegion.IsReservedOnly &&
|
|
||||||
!EnsureRangeCommitted(sourcePointer, length, sourceRegion)) ||
|
|
||||||
(destinationRegion.IsReservedOnly &&
|
|
||||||
!EnsureRangeCommitted(destinationPointer, length, destinationRegion)) ||
|
|
||||||
!CanReadWithoutProtectionChange(sourcePointer, length, sourceRegion) ||
|
|
||||||
!CanWriteWithoutProtectionChange(destinationPointer, length, destinationRegion))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Span.CopyTo has memmove overlap semantics, so this allocation-free
|
|
||||||
// path safely serves both libc memcpy and libc memmove.
|
|
||||||
new ReadOnlySpan<byte>((void*)sourcePointer, checked((int)length)).CopyTo(
|
|
||||||
new Span<byte>((void*)destinationPointer, checked((int)length)));
|
|
||||||
NotifyGuestWriteWatch(
|
|
||||||
destinationAddress,
|
|
||||||
new ReadOnlySpan<byte>((void*)destinationPointer, checked((int)length)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_gate.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
@@ -1238,7 +1016,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1263,7 +1040,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyGuestWriteWatch(virtualAddress, source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1505,12 +1281,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var mappingGeneration = Volatile.Read(ref _mappingGeneration);
|
|
||||||
var committedRangeCache = _committedRangeCache ??= new CommittedRangeCache();
|
|
||||||
if (committedRangeCache.Contains(this, mappingGeneration, startPage, endPage))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var commitProtection = GetCommitProtection(region);
|
var commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
@@ -1532,9 +1302,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if (info.State == HostRegionState.Committed)
|
if (info.State == HostRegionState.Committed)
|
||||||
{
|
{
|
||||||
// The host query proved this whole range is committed. Retain
|
|
||||||
// that result instead of caching only the caller's small span.
|
|
||||||
CacheCommittedRange(info.BaseAddress, queriedEnd, mappingGeneration);
|
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1550,23 +1317,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration);
|
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheCommittedRange(startPage, endPage, mappingGeneration);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration)
|
|
||||||
{
|
|
||||||
(_committedRangeCache ??= new CommittedRangeCache()).Add(
|
|
||||||
this,
|
|
||||||
mappingGeneration,
|
|
||||||
startPage,
|
|
||||||
endPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryTemporarilyProtectForRead(
|
private bool TryTemporarilyProtectForRead(
|
||||||
ulong address,
|
ulong address,
|
||||||
ulong size,
|
ulong size,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
@@ -94,14 +93,8 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
}
|
}
|
||||||
|
|
||||||
CopyToRegions(virtualAddress, source, regionIndex);
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GuestWriteWatch.Armed)
|
|
||||||
{
|
|
||||||
GuestWriteWatch.Check(virtualAddress, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryValidateRange(
|
private bool TryValidateRange(
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
{
|
{
|
||||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
if (!CreateProcessW(
|
if (!CreateProcessW(
|
||||||
null,
|
exePath,
|
||||||
commandLine,
|
commandLine,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -629,7 +629,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CreateProcessW(string? applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
private static extern bool CreateProcessW(string applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint flags, nint environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||||
|
|||||||
@@ -351,13 +351,6 @@ public sealed class GameSurfaceHost : NativeControlHost
|
|||||||
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
||||||
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
||||||
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
||||||
if (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_SURFACE_SIZE") == "1")
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[GUI][TRACE] GameSurfaceHost.UpdateSurfaceSize bounds={Bounds.Width}x{Bounds.Height} " +
|
|
||||||
$"scale={renderScale} computed={width}x{height} changed={sizeChanged} " +
|
|
||||||
$"prevSurface={_surface.PixelWidth}x{_surface.PixelHeight}");
|
|
||||||
}
|
|
||||||
_surface.UpdatePixelSize(width, height);
|
_surface.UpdatePixelSize(width, height);
|
||||||
|
|
||||||
if (!sizeChanged)
|
if (!sizeChanged)
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||||
public List<string> EnvironmentToggles { get; set; } = new();
|
public List<string> EnvironmentToggles { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
|
||||||
public double RenderResolutionScale { get; set; } = 1.0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discord application ID used for Rich Presence; the default is the
|
/// Discord application ID used for Rich Presence; the default is the
|
||||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||||
@@ -74,7 +71,7 @@ public sealed class GuiSettings
|
|||||||
if (File.Exists(SettingsPath))
|
if (File.Exists(SettingsPath))
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(SettingsPath);
|
var json = File.ReadAllText(SettingsPath);
|
||||||
return NormalizeFromJson(json);
|
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -85,39 +82,6 @@ public sealed class GuiSettings
|
|||||||
return new GuiSettings();
|
return new GuiSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserializes settings and normalizes null references and null or empty list
|
|
||||||
/// entries introduced by JSON. Empty scalar strings remain unchanged.
|
|
||||||
/// </summary>
|
|
||||||
internal static GuiSettings NormalizeFromJson(string json)
|
|
||||||
{
|
|
||||||
var settings = JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
|
||||||
|
|
||||||
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
|
|
||||||
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
|
|
||||||
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
|
|
||||||
settings.LogLevel ??= "Info";
|
|
||||||
settings.Language ??= "en";
|
|
||||||
settings.DiscordClientId ??= "1525606762248540221";
|
|
||||||
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
|
|
||||||
{
|
|
||||||
settings.RenderResolutionScale = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON can populate non-nullable lists with null references and entries.
|
|
||||||
private static List<string> FilterNullOrEmpty(List<string>? source)
|
|
||||||
{
|
|
||||||
if (source is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -400,29 +400,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
|
||||||
|
|
||||||
<Border Classes="card">
|
|
||||||
<StackPanel Spacing="14">
|
|
||||||
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
|
|
||||||
|
|
||||||
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
|
|
||||||
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
|
|
||||||
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
|
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
|
||||||
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
|
|
||||||
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
|
|
||||||
</ComboBox>
|
|
||||||
</local:SettingRow>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|||||||
@@ -192,18 +192,6 @@ public partial class MainWindow : Window
|
|||||||
// it is open already uses the new values.
|
// it is open already uses the new values.
|
||||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
RenderResolutionBox.SelectionChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
|
|
||||||
double.TryParse(
|
|
||||||
tag,
|
|
||||||
System.Globalization.NumberStyles.Float,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
|
||||||
out var scale))
|
|
||||||
{
|
|
||||||
_settings.RenderResolutionScale = scale;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||||
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
||||||
@@ -881,13 +869,6 @@ public partial class MainWindow : Window
|
|||||||
_ => 2,
|
_ => 2,
|
||||||
};
|
};
|
||||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||||
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
|
|
||||||
{
|
|
||||||
>= 0.875 => 0,
|
|
||||||
>= 0.625 => 1,
|
|
||||||
>= 0.375 => 2,
|
|
||||||
_ => 3,
|
|
||||||
};
|
|
||||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
@@ -1807,12 +1788,6 @@ public partial class MainWindow : Window
|
|||||||
_appliedEnvironmentVariables.Add(name);
|
_appliedEnvironmentVariables.Add(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(
|
|
||||||
"SHARPEMU_RENDER_SCALE",
|
|
||||||
_settings.RenderResolutionScale.ToString(
|
|
||||||
"0.###",
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
||||||
{
|
{
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
@@ -2055,6 +2030,8 @@ public partial class MainWindow : Window
|
|||||||
RestoreGameViewToFull();
|
RestoreGameViewToFull();
|
||||||
GameView.Background = Brushes.Black;
|
GameView.Background = Brushes.Black;
|
||||||
GameView.IsHitTestVisible = true;
|
GameView.IsHitTestVisible = true;
|
||||||
|
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||||
|
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||||
LibraryPage.IsVisible = false;
|
LibraryPage.IsVisible = false;
|
||||||
OptionsPage.IsVisible = false;
|
OptionsPage.IsVisible = false;
|
||||||
LibraryToolbar.IsVisible = false;
|
LibraryToolbar.IsVisible = false;
|
||||||
@@ -2063,19 +2040,6 @@ public partial class MainWindow : Window
|
|||||||
LaunchBar.IsVisible = false;
|
LaunchBar.IsVisible = false;
|
||||||
HideSessionLoading();
|
HideSessionLoading();
|
||||||
UpdateSessionBarVisibility();
|
UpdateSessionBarVisibility();
|
||||||
|
|
||||||
// Defer so the layout pass from the margin change above settles first.
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (!_isRunning || _isStopping)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameSurfaceHost?.RefreshSurfaceSize();
|
|
||||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
|
||||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed class PerGameSettings
|
|||||||
var path = PathFor(titleId);
|
var path = PathFor(titleId);
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
return NormalizeFromJson(File.ReadAllText(path));
|
return JsonSerializer.Deserialize<PerGameSettings>(File.ReadAllText(path), SerializerOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -59,18 +59,6 @@ public sealed class PerGameSettings
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A null list inherits global settings; only entries in a present list are sanitized.
|
|
||||||
internal static PerGameSettings? NormalizeFromJson(string json)
|
|
||||||
{
|
|
||||||
var settings = JsonSerializer.Deserialize<PerGameSettings>(json, SerializerOptions);
|
|
||||||
if (settings?.EnvironmentToggles is { } toggles)
|
|
||||||
{
|
|
||||||
settings.EnvironmentToggles = toggles.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save(string titleId)
|
public void Save(string titleId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(titleId))
|
if (string.IsNullOrWhiteSpace(titleId))
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
public int Armed;
|
public int Armed;
|
||||||
public int FirstCpuWriteSeen;
|
public int FirstCpuWriteSeen;
|
||||||
public int PendingFirstCpuWrite;
|
public int PendingFirstCpuWrite;
|
||||||
public long WriteGeneration;
|
|
||||||
public bool TraceLifetime;
|
public bool TraceLifetime;
|
||||||
public long SourceSequence;
|
public long SourceSequence;
|
||||||
public long FirstCpuWriteTraceSequence;
|
public long FirstCpuWriteTraceSequence;
|
||||||
@@ -156,21 +155,10 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
{
|
{
|
||||||
// Never resize an object that is still reachable from the
|
// Never resize an object that is still reachable from the
|
||||||
// signal handler's lock-free snapshot. Retire it and publish
|
// signal handler's lock-free snapshot. Retire it and publish
|
||||||
// a fresh immutable range, carrying the write generation so
|
// a fresh immutable range.
|
||||||
// resizes do not hide guest CPU rewrites from cache owners.
|
|
||||||
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
|
|
||||||
DisarmLocked(range, "replace-range");
|
DisarmLocked(range, "replace-range");
|
||||||
_rangesByAddress.Remove(address);
|
_rangesByAddress.Remove(address);
|
||||||
range = new TrackedRange
|
range = null;
|
||||||
{
|
|
||||||
Address = address,
|
|
||||||
ByteCount = byteCount,
|
|
||||||
Start = start,
|
|
||||||
End = start + length,
|
|
||||||
WriteGeneration = writeGeneration,
|
|
||||||
};
|
|
||||||
_rangesByAddress[address] = range;
|
|
||||||
RebuildSnapshotLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range is null)
|
if (range is null)
|
||||||
@@ -284,31 +272,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the monotonic first-write generation for a tracked allocation.
|
|
||||||
/// Unlike the consuming dirty flag, this remains changed after another
|
|
||||||
/// cache owner consumes and re-arms the range.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetWriteGeneration(ulong address, out long generation)
|
|
||||||
{
|
|
||||||
generation = 0;
|
|
||||||
if (!_enabled)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_rangesByAddress.TryGetValue(address, out var range))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generation = Volatile.Read(ref range.WriteGeneration);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares pages touched by a managed HLE memory write. Native guest
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||||
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||||
@@ -462,10 +425,6 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||||
if (wasArmed)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref range.WriteGeneration);
|
|
||||||
}
|
|
||||||
if (wasArmed &&
|
if (wasArmed &&
|
||||||
range.TraceLifetime &&
|
range.TraceLifetime &&
|
||||||
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class GuestTlsTemplate
|
|||||||
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||||
// modules can require more than one host page of Variant II static TLS;
|
// modules can require more than one host page of Variant II static TLS;
|
||||||
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||||
public const ulong StartupStaticTlsReservation = 0x20000UL; // Was 0x10000UL, but thats too small for GTA V
|
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||||
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
|
||||||
|
|
||||||
// This tool monitors guest-memory writes only when a watch mode is active.
|
|
||||||
public static class GuestWriteWatch
|
|
||||||
{
|
|
||||||
private const ulong WatchBytes = 8;
|
|
||||||
private const int MaxBulkReports = 64;
|
|
||||||
|
|
||||||
private static readonly ulong WatchBase = Parse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_WRITE"));
|
|
||||||
|
|
||||||
private static readonly bool WatchPoolHeaders = IsEnabled("SHARPEMU_WATCH_POOL_HEADER");
|
|
||||||
|
|
||||||
private static readonly ulong[] PoolSlots = new ulong[64];
|
|
||||||
private static int _poolSlotCount;
|
|
||||||
|
|
||||||
private static readonly bool WatchValuePattern = IsEnabled("SHARPEMU_WATCH_VALUE_PATTERN");
|
|
||||||
|
|
||||||
private static readonly bool WatchValue1 = IsEnabled("SHARPEMU_WATCH_VALUE1");
|
|
||||||
|
|
||||||
private const ulong DirectBandLow = 0x100_0000_0000;
|
|
||||||
private const ulong DirectBandHigh = 0x1000_0000_0000;
|
|
||||||
private static int _value1Reports;
|
|
||||||
|
|
||||||
private static readonly bool WatchBulkTorn = IsEnabled("SHARPEMU_WATCH_BULK_TORN");
|
|
||||||
|
|
||||||
private static readonly ulong BulkDestHigh = Parse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_BULK_DEST_HI"));
|
|
||||||
private static int _bulkTornReports;
|
|
||||||
private static int _bulkShiftReports;
|
|
||||||
|
|
||||||
public static bool Armed =>
|
|
||||||
WatchBase != 0 || WatchPoolHeaders || WatchValuePattern || WatchValue1 || WatchBulkTorn;
|
|
||||||
|
|
||||||
public static void OnDirectMapping(ulong mappedAddress, ulong length, int protection)
|
|
||||||
{
|
|
||||||
if (!WatchPoolHeaders || !IsPoolMapping(length, protection))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = Interlocked.Increment(ref _poolSlotCount) - 1;
|
|
||||||
if (index < PoolSlots.Length)
|
|
||||||
{
|
|
||||||
Volatile.Write(ref PoolSlots[index], mappedAddress + 0x40);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_write armed on pool header slot 0x{mappedAddress + 0x40:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Check(ulong address, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
if (WatchBulkTorn &&
|
|
||||||
data.Length >= 8 &&
|
|
||||||
(BulkDestHigh != 0
|
|
||||||
? (address >> 32) == BulkDestHigh
|
|
||||||
: address >= DirectBandLow && address < DirectBandHigh))
|
|
||||||
{
|
|
||||||
for (var offset = FirstAlignedOffset(address); offset + 8 <= data.Length; offset += 8)
|
|
||||||
{
|
|
||||||
var qword = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
|
|
||||||
var kind = ClassifyBulkValue(qword);
|
|
||||||
if (kind is not null && ReserveBulkReport(kind))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_bulk_torn HIT ({kind}) " +
|
|
||||||
$"dest=0x{address + (ulong)offset:X16} (base=0x{address:X16}+0x{offset:X}) " +
|
|
||||||
$"len={data.Length} qword=0x{qword:X16}{Environment.NewLine}{Environment.StackTrace}");
|
|
||||||
Console.Error.Flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchValue1 &&
|
|
||||||
address >= DirectBandLow && address < DirectBandHigh &&
|
|
||||||
data.Length is >= 1 and <= 8 &&
|
|
||||||
LittleEndianValue(data) == 1 &&
|
|
||||||
Interlocked.Increment(ref _value1Reports) <= 128)
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchValuePattern && data.Length == 8)
|
|
||||||
{
|
|
||||||
var value = BinaryPrimitives.ReadUInt64LittleEndian(data);
|
|
||||||
if ((value & 0xFFFFFFFF) == 1 && value >> 32 is > 0 and <= 0xFFFF)
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WatchBase != 0 && Overlaps(address, data.Length, WatchBase))
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var slots = Math.Min(Volatile.Read(ref _poolSlotCount), PoolSlots.Length);
|
|
||||||
for (var i = 0; i < slots; i++)
|
|
||||||
{
|
|
||||||
var slot = Volatile.Read(ref PoolSlots[i]);
|
|
||||||
if (slot != 0 && Overlaps(address, data.Length, slot))
|
|
||||||
{
|
|
||||||
Report(address, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string? ClassifyBulkValue(ulong qword)
|
|
||||||
{
|
|
||||||
var low32 = qword & 0xFFFFFFFF;
|
|
||||||
var high32 = qword >> 32;
|
|
||||||
if (low32 == 1 && high32 is > 0 and <= 0xFFFF)
|
|
||||||
{
|
|
||||||
return "torn";
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefix = low32 & 0xFF00_0000;
|
|
||||||
var hasShiftedPointerPrefix = prefix is 0x0800_0000 or 0x8000_0000;
|
|
||||||
return high32 == 0 && hasShiftedPointerPrefix && (low32 & 0xFF) == 0
|
|
||||||
? "shift"
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static int FirstAlignedOffset(ulong address) =>
|
|
||||||
(int)((8 - (address & 7)) & 7);
|
|
||||||
|
|
||||||
internal static bool IsPoolMapping(ulong length, int protection) =>
|
|
||||||
length == 0x10000 && protection == 0xF2;
|
|
||||||
|
|
||||||
internal static bool Overlaps(ulong address, int length, ulong slot)
|
|
||||||
{
|
|
||||||
if (length <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeLength = (ulong)length - 1;
|
|
||||||
var writeEnd = address > ulong.MaxValue - writeLength
|
|
||||||
? ulong.MaxValue
|
|
||||||
: address + writeLength;
|
|
||||||
var slotEnd = slot > ulong.MaxValue - (WatchBytes - 1)
|
|
||||||
? ulong.MaxValue
|
|
||||||
: slot + WatchBytes - 1;
|
|
||||||
return address <= slotEnd && slot <= writeEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong LittleEndianValue(ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
ulong value = 0;
|
|
||||||
for (var i = 0; i < data.Length; i++)
|
|
||||||
{
|
|
||||||
value |= (ulong)data[i] << (i * 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Report(ulong address, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] watch_write HIT addr=0x{address:X16} len={data.Length} " +
|
|
||||||
$"first_qword=0x{LittleEndianValue(data):X16}{Environment.NewLine}{Environment.StackTrace}");
|
|
||||||
Console.Error.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsEnabled(string name) =>
|
|
||||||
string.Equals(Environment.GetEnvironmentVariable(name), "1", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private static bool ReserveBulkReport(string kind) =>
|
|
||||||
kind == "torn"
|
|
||||||
? Interlocked.Increment(ref _bulkTornReports) <= MaxBulkReports
|
|
||||||
: Interlocked.Increment(ref _bulkShiftReports) <= MaxBulkReports;
|
|
||||||
|
|
||||||
internal static ulong Parse(string? text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.Trim();
|
|
||||||
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
text = text[2..];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
|
|
||||||
? value
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,4 @@ public interface ICpuMemory
|
|||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
|
||||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||||
|
|
||||||
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,6 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
|
|||||||
{
|
{
|
||||||
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Backs an entire fixed-address range, matching the guest's
|
|
||||||
/// <c>SCE_KERNEL_MAP_FIXED</c> contract. Unlike <see cref="AllocateAt"/>, which
|
|
||||||
/// reserves the range in one all-or-nothing host call, this walks the range and
|
|
||||||
/// fills only the sub-ranges that are not already backed. That keeps a fixed
|
|
||||||
/// mapping whole when part of the requested window is already occupied — the
|
|
||||||
/// partial-overlap case where the single-call reservation fails outright and
|
|
||||||
/// leaves the remainder unmapped for the guest to fault into.
|
|
||||||
/// </summary>
|
|
||||||
bool TryBackFixedRange(ulong address, ulong size, bool executable);
|
|
||||||
|
|
||||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||||
|
|
||||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||||
|
|||||||
@@ -147,10 +147,6 @@ public static partial class AgcExports
|
|||||||
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
|
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
|
||||||
private const uint Gen5TextureType1D = 8;
|
private const uint Gen5TextureType1D = 8;
|
||||||
private const uint Gen5TextureType2D = 9;
|
private const uint Gen5TextureType2D = 9;
|
||||||
private const uint Gen5TextureType3D = 10;
|
|
||||||
private const uint Gen5TextureTypeCube = 11;
|
|
||||||
private const uint Gen5TextureType1DArray = 12;
|
|
||||||
private const uint Gen5TextureType2DArray = 13;
|
|
||||||
private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL;
|
private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL;
|
||||||
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||||
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||||
@@ -442,8 +438,7 @@ public static partial class AgcExports
|
|||||||
TextureDescriptor Descriptor,
|
TextureDescriptor Descriptor,
|
||||||
bool IsStorage,
|
bool IsStorage,
|
||||||
uint MipLevel,
|
uint MipLevel,
|
||||||
IReadOnlyList<uint> SamplerDescriptor,
|
IReadOnlyList<uint> SamplerDescriptor);
|
||||||
bool IsArrayed = false);
|
|
||||||
|
|
||||||
private readonly record struct RenderTargetWriter(
|
private readonly record struct RenderTargetWriter(
|
||||||
ulong Sequence,
|
ulong Sequence,
|
||||||
@@ -645,21 +640,6 @@ public static partial class AgcExports
|
|||||||
public static int GetRegisterDefaults2Internal(CpuContext ctx) =>
|
public static int GetRegisterDefaults2Internal(CpuContext ctx) =>
|
||||||
ReturnRegisterDefaults(ctx, internalDefaults: true);
|
ReturnRegisterDefaults(ctx, internalDefaults: true);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reports that the GPU is not running in Trinity mode, matching the base
|
|
||||||
/// console this backend emulates.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "BfBDZGbti7A",
|
|
||||||
ExportName = "sceAgcGetIsTrinityMode",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAgc")]
|
|
||||||
public static int GetIsTrinityMode(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "f3dg2CSgRKY",
|
Nid = "f3dg2CSgRKY",
|
||||||
ExportName = "sceAgcCreateShader",
|
ExportName = "sceAgcCreateShader",
|
||||||
@@ -5964,8 +5944,7 @@ public static partial class AgcExports
|
|||||||
binding,
|
binding,
|
||||||
exportEvaluation.ImageBindings),
|
exportEvaluation.ImageBindings),
|
||||||
binding.MipLevel ?? 0,
|
binding.MipLevel ?? 0,
|
||||||
binding.SamplerDescriptor,
|
binding.SamplerDescriptor));
|
||||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||||
@@ -6422,8 +6401,7 @@ public static partial class AgcExports
|
|||||||
texture,
|
texture,
|
||||||
isStorage,
|
isStorage,
|
||||||
binding.MipLevel ?? 0,
|
binding.MipLevel ?? 0,
|
||||||
binding.SamplerDescriptor,
|
binding.SamplerDescriptor));
|
||||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
@@ -7394,7 +7372,6 @@ public static partial class AgcExports
|
|||||||
binding.IsStorage,
|
binding.IsStorage,
|
||||||
binding.MipLevel,
|
binding.MipLevel,
|
||||||
binding.SamplerDescriptor,
|
binding.SamplerDescriptor,
|
||||||
binding.IsArrayed,
|
|
||||||
out var texture))
|
out var texture))
|
||||||
{
|
{
|
||||||
textures.Add(texture);
|
textures.Add(texture);
|
||||||
@@ -7790,10 +7767,7 @@ public static partial class AgcExports
|
|||||||
TextureDescriptor descriptor,
|
TextureDescriptor descriptor,
|
||||||
uint sourceWidth,
|
uint sourceWidth,
|
||||||
int logicalByteCount,
|
int logicalByteCount,
|
||||||
byte[] source,
|
byte[] source)
|
||||||
bool baseMipInTail = false,
|
|
||||||
int tailElementX = 0,
|
|
||||||
int tailElementY = 0)
|
|
||||||
{
|
{
|
||||||
if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
|
if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
|
||||||
!TryGetTextureElementLayout(
|
!TryGetTextureElementLayout(
|
||||||
@@ -7806,48 +7780,6 @@ public static partial class AgcExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseMipInTail)
|
|
||||||
{
|
|
||||||
if (!GnmTiling.TryGetBlockElementDimensions(
|
|
||||||
descriptor.TileMode,
|
|
||||||
bytesPerElement,
|
|
||||||
out var blockWidth,
|
|
||||||
out var blockHeight))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var blockByteCount = (long)blockWidth * blockHeight * bytesPerElement;
|
|
||||||
if (source.Length < blockByteCount ||
|
|
||||||
(long)elementsWide * elementsHigh * bytesPerElement > logicalByteCount)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var blockLinear = new byte[blockByteCount];
|
|
||||||
if (!GnmTiling.TryDetile(
|
|
||||||
source,
|
|
||||||
blockLinear,
|
|
||||||
descriptor.TileMode,
|
|
||||||
blockWidth,
|
|
||||||
blockHeight,
|
|
||||||
bytesPerElement))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tailLinear = new byte[logicalByteCount];
|
|
||||||
var rowBytes = elementsWide * bytesPerElement;
|
|
||||||
for (var y = 0; y < elementsHigh; y++)
|
|
||||||
{
|
|
||||||
var sourceOffset = (((long)tailElementY + y) * blockWidth + tailElementX) * bytesPerElement;
|
|
||||||
blockLinear.AsSpan((int)sourceOffset, rowBytes)
|
|
||||||
.CopyTo(tailLinear.AsSpan(y * rowBytes, rowBytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
return tailLinear;
|
|
||||||
}
|
|
||||||
|
|
||||||
var linear = new byte[logicalByteCount];
|
var linear = new byte[logicalByteCount];
|
||||||
return GnmTiling.TryDetile(
|
return GnmTiling.TryDetile(
|
||||||
source,
|
source,
|
||||||
@@ -7885,23 +7817,18 @@ public static partial class AgcExports
|
|||||||
bool isStorage,
|
bool isStorage,
|
||||||
uint mipLevel,
|
uint mipLevel,
|
||||||
IReadOnlyList<uint> samplerDescriptor,
|
IReadOnlyList<uint> samplerDescriptor,
|
||||||
bool isArrayed,
|
|
||||||
out GuestDrawTexture texture)
|
out GuestDrawTexture texture)
|
||||||
{
|
{
|
||||||
texture = default!;
|
texture = default!;
|
||||||
if ((descriptor.Type != Gen5TextureType1D &&
|
if ((descriptor.Type != Gen5TextureType1D &&
|
||||||
descriptor.Type != Gen5TextureType2D &&
|
descriptor.Type != Gen5TextureType2D) ||
|
||||||
descriptor.Type != Gen5TextureType3D &&
|
|
||||||
descriptor.Type != Gen5TextureTypeCube &&
|
|
||||||
descriptor.Type != Gen5TextureType1DArray &&
|
|
||||||
descriptor.Type != Gen5TextureType2DArray) ||
|
|
||||||
descriptor.Width == 0 ||
|
descriptor.Width == 0 ||
|
||||||
descriptor.Height == 0 ||
|
descriptor.Height == 0 ||
|
||||||
descriptor.Width > 8192 ||
|
descriptor.Width > 8192 ||
|
||||||
descriptor.Height > 8192)
|
descriptor.Height > 8192)
|
||||||
{
|
{
|
||||||
TraceTextureFallback(descriptor, "invalid-descriptor");
|
TraceTextureFallback(descriptor, "invalid-descriptor");
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7922,22 +7849,18 @@ public static partial class AgcExports
|
|||||||
TraceTextureFallback(
|
TraceTextureFallback(
|
||||||
descriptor,
|
descriptor,
|
||||||
$"invalid-byte-count:{sourceByteCount}");
|
$"invalid-byte-count:{sourceByteCount}");
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var physicalSourceByteCount = sourceByteCount;
|
var physicalSourceByteCount = sourceByteCount;
|
||||||
var elementsWide = 0;
|
if (GnmTiling.NeedsDetile(descriptor.TileMode) &&
|
||||||
var elementsHigh = 0;
|
|
||||||
var bytesPerElement = 0;
|
|
||||||
var hasElementLayout = GnmTiling.NeedsDetile(descriptor.TileMode) &&
|
|
||||||
TryGetTextureElementLayout(
|
TryGetTextureElementLayout(
|
||||||
descriptor,
|
descriptor,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
out elementsWide,
|
out var elementsWide,
|
||||||
out elementsHigh,
|
out var elementsHigh,
|
||||||
out bytesPerElement);
|
out var bytesPerElement) &&
|
||||||
if (hasElementLayout &&
|
|
||||||
GnmTiling.TryGetTiledByteCount(
|
GnmTiling.TryGetTiledByteCount(
|
||||||
descriptor.TileMode,
|
descriptor.TileMode,
|
||||||
elementsWide,
|
elementsWide,
|
||||||
@@ -7951,50 +7874,13 @@ public static partial class AgcExports
|
|||||||
if (physicalSourceByteCount > MaxPresentedTextureBytes ||
|
if (physicalSourceByteCount > MaxPresentedTextureBytes ||
|
||||||
physicalSourceByteCount > int.MaxValue)
|
physicalSourceByteCount > int.MaxValue)
|
||||||
{
|
{
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceMipLevels = descriptor.HasExtendedDescriptor
|
|
||||||
? descriptor.ResourceMipLevels
|
|
||||||
: 1u;
|
|
||||||
var baseMipByteOffset = 0UL;
|
|
||||||
var baseMipInTail = false;
|
|
||||||
var mipTailElementX = 0;
|
|
||||||
var mipTailElementY = 0;
|
|
||||||
var chainSliceBytes = physicalSourceByteCount;
|
|
||||||
if (hasElementLayout && resourceMipLevels > 1 &&
|
|
||||||
GnmTiling.TryGetBaseMipPlacement(
|
|
||||||
descriptor.TileMode,
|
|
||||||
elementsWide,
|
|
||||||
elementsHigh,
|
|
||||||
bytesPerElement,
|
|
||||||
resourceMipLevels,
|
|
||||||
out baseMipByteOffset,
|
|
||||||
out baseMipInTail,
|
|
||||||
out mipTailElementX,
|
|
||||||
out mipTailElementY,
|
|
||||||
out var placedChainSliceBytes))
|
|
||||||
{
|
|
||||||
chainSliceBytes = placedChainSliceBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
var wantsArrayUpload = isArrayed &&
|
|
||||||
!isStorage &&
|
|
||||||
descriptor.Address != 0 &&
|
|
||||||
(descriptor.Type == Gen5TextureType2DArray ||
|
|
||||||
descriptor.Type == Gen5TextureType1DArray) &&
|
|
||||||
descriptor.Depth > 1;
|
|
||||||
var arrayUploadLayers = wantsArrayUpload ? descriptor.Depth : 1u;
|
|
||||||
|
|
||||||
// Upload-known (not plain availability): the presenter's answer goes
|
|
||||||
// generation-stale when the guest CPU rewrites a CPU-backed image
|
|
||||||
// (video planes, streamed font atlases), which routes this draw back
|
|
||||||
// through the texel copy below so the refresh path re-uploads.
|
|
||||||
if (!isStorage &&
|
if (!isStorage &&
|
||||||
!wantsArrayUpload &&
|
|
||||||
descriptor.Address != 0 &&
|
descriptor.Address != 0 &&
|
||||||
GuestGpu.Current.IsGuestImageUploadKnown(
|
GuestGpu.Current.IsGpuGuestImageAvailable(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.Format,
|
descriptor.Format,
|
||||||
descriptor.NumberType))
|
descriptor.NumberType))
|
||||||
@@ -8015,8 +7901,7 @@ public static partial class AgcExports
|
|||||||
Pitch: sourceWidth,
|
Pitch: sourceWidth,
|
||||||
TileMode: descriptor.TileMode,
|
TileMode: descriptor.TileMode,
|
||||||
DstSelect: descriptor.DstSelect,
|
DstSelect: descriptor.DstSelect,
|
||||||
Sampler: ToGuestSampler(samplerDescriptor),
|
Sampler: ToGuestSampler(samplerDescriptor));
|
||||||
ArrayedView: isArrayed);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8039,17 +7924,14 @@ public static partial class AgcExports
|
|||||||
// and run the same AddrLib-derived detile path used below for
|
// and run the same AddrLib-derived detile path used below for
|
||||||
// sampled textures before seeding the Vulkan image.
|
// sampled textures before seeding the Vulkan image.
|
||||||
var storageSource = new byte[(int)physicalSourceByteCount];
|
var storageSource = new byte[(int)physicalSourceByteCount];
|
||||||
if (ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, storageSource))
|
if (ctx.Memory.TryRead(descriptor.Address, storageSource))
|
||||||
{
|
{
|
||||||
readSucceeded = true;
|
readSucceeded = true;
|
||||||
var linearStorage = TryDetileTextureSource(
|
var linearStorage = TryDetileTextureSource(
|
||||||
descriptor,
|
descriptor,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
checked((int)sourceByteCount),
|
checked((int)sourceByteCount),
|
||||||
storageSource,
|
storageSource) ?? storageSource
|
||||||
baseMipInTail,
|
|
||||||
mipTailElementX,
|
|
||||||
mipTailElementY) ?? storageSource
|
|
||||||
.AsSpan(0, checked((int)sourceByteCount))
|
.AsSpan(0, checked((int)sourceByteCount))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
||||||
@@ -8105,19 +7987,6 @@ public static partial class AgcExports
|
|||||||
// (skipping would leave the draw with no pixels and a fallback
|
// (skipping would leave the draw with no pixels and a fallback
|
||||||
// texture for the frame — visible flicker on animated textures).
|
// texture for the frame — visible flicker on animated textures).
|
||||||
var sampler = ToGuestSampler(samplerDescriptor);
|
var sampler = ToGuestSampler(samplerDescriptor);
|
||||||
// Track the guest allocation before reading its texels so a CPU
|
|
||||||
// rewrite landing after the copy still bumps the write generation.
|
|
||||||
// The generation rides on the texture and is recorded by the
|
|
||||||
// presenter after upload, where the upload-known skip compares it
|
|
||||||
// against the tracker to force fresh texels for rewritten memory.
|
|
||||||
SharpEmu.HLE.GuestImageWriteTracker.Track(
|
|
||||||
descriptor.Address,
|
|
||||||
physicalSourceByteCount,
|
|
||||||
source: "agc.decoded-texture");
|
|
||||||
var hasWriteGeneration =
|
|
||||||
SharpEmu.HLE.GuestImageWriteTracker.TryGetWriteGeneration(
|
|
||||||
descriptor.Address,
|
|
||||||
out var writeGeneration);
|
|
||||||
if (!_textureCopySkipDisabled &&
|
if (!_textureCopySkipDisabled &&
|
||||||
descriptor.Address != 0 &&
|
descriptor.Address != 0 &&
|
||||||
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
|
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
|
||||||
@@ -8131,9 +8000,7 @@ public static partial class AgcExports
|
|||||||
descriptor.DstSelect,
|
descriptor.DstSelect,
|
||||||
descriptor.TileMode,
|
descriptor.TileMode,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
sampler,
|
sampler)))
|
||||||
isArrayed,
|
|
||||||
arrayUploadLayers)))
|
|
||||||
{
|
{
|
||||||
texture = new GuestDrawTexture(
|
texture = new GuestDrawTexture(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
@@ -8151,77 +8018,17 @@ public static partial class AgcExports
|
|||||||
Pitch: sourceWidth,
|
Pitch: sourceWidth,
|
||||||
TileMode: descriptor.TileMode,
|
TileMode: descriptor.TileMode,
|
||||||
DstSelect: descriptor.DstSelect,
|
DstSelect: descriptor.DstSelect,
|
||||||
Sampler: sampler,
|
Sampler: sampler);
|
||||||
ArrayedView: isArrayed,
|
|
||||||
ArrayLayers: arrayUploadLayers);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsArrayUpload)
|
|
||||||
{
|
|
||||||
var arrayLayers = arrayUploadLayers;
|
|
||||||
var layerBytes = checked((int)sourceByteCount);
|
|
||||||
var totalBytes = (long)layerBytes * arrayLayers;
|
|
||||||
if (totalBytes <= int.MaxValue)
|
|
||||||
{
|
|
||||||
var layered = new byte[totalBytes];
|
|
||||||
var uploadedLayers = 0u;
|
|
||||||
for (var layer = 0u; layer < arrayLayers; layer++)
|
|
||||||
{
|
|
||||||
var sliceSource = new byte[(int)physicalSourceByteCount];
|
|
||||||
if (!ctx.Memory.TryRead(
|
|
||||||
descriptor.Address + layer * chainSliceBytes + baseMipByteOffset,
|
|
||||||
sliceSource))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sliceLinear = TryDetileTextureSource(
|
|
||||||
descriptor,
|
|
||||||
sourceWidth,
|
|
||||||
layerBytes,
|
|
||||||
sliceSource,
|
|
||||||
baseMipInTail,
|
|
||||||
mipTailElementX,
|
|
||||||
mipTailElementY) ?? sliceSource.AsSpan(0, layerBytes).ToArray();
|
|
||||||
sliceLinear.AsSpan(0, layerBytes)
|
|
||||||
.CopyTo(layered.AsSpan(checked((int)(layer * layerBytes))));
|
|
||||||
uploadedLayers++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadedLayers == arrayLayers)
|
|
||||||
{
|
|
||||||
texture = new GuestDrawTexture(
|
|
||||||
descriptor.Address,
|
|
||||||
descriptor.Width,
|
|
||||||
descriptor.Height,
|
|
||||||
descriptor.Format,
|
|
||||||
descriptor.NumberType,
|
|
||||||
layered,
|
|
||||||
IsFallback: false,
|
|
||||||
IsStorage: false,
|
|
||||||
MipLevels: descriptor.MipLevels,
|
|
||||||
MipLevel: mipLevel,
|
|
||||||
BaseMipLevel: descriptor.ViewBaseLevel,
|
|
||||||
ResourceMipLevels: descriptor.ResourceMipLevels,
|
|
||||||
Pitch: sourceWidth,
|
|
||||||
TileMode: descriptor.TileMode,
|
|
||||||
DstSelect: descriptor.DstSelect,
|
|
||||||
Sampler: sampler,
|
|
||||||
ArrayedView: true,
|
|
||||||
ArrayLayers: arrayLayers);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var source = new byte[(int)physicalSourceByteCount];
|
var source = new byte[(int)physicalSourceByteCount];
|
||||||
if (!ctx.Memory.TryRead(descriptor.Address + baseMipByteOffset, source))
|
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
||||||
{
|
{
|
||||||
TraceTextureFallback(
|
TraceTextureFallback(
|
||||||
descriptor,
|
descriptor,
|
||||||
$"guest-read-failed:{sourceByteCount}");
|
$"guest-read-failed:{sourceByteCount}");
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8253,10 +8060,7 @@ public static partial class AgcExports
|
|||||||
descriptor,
|
descriptor,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
checked((int)sourceByteCount),
|
checked((int)sourceByteCount),
|
||||||
source,
|
source) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
|
||||||
baseMipInTail,
|
|
||||||
mipTailElementX,
|
|
||||||
mipTailElementY) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
|
|
||||||
DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba);
|
DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba);
|
||||||
texture = new GuestDrawTexture(
|
texture = new GuestDrawTexture(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
@@ -8274,9 +8078,7 @@ public static partial class AgcExports
|
|||||||
Pitch: sourceWidth,
|
Pitch: sourceWidth,
|
||||||
TileMode: descriptor.TileMode,
|
TileMode: descriptor.TileMode,
|
||||||
DstSelect: descriptor.DstSelect,
|
DstSelect: descriptor.DstSelect,
|
||||||
Sampler: ToGuestSampler(samplerDescriptor),
|
Sampler: ToGuestSampler(samplerDescriptor));
|
||||||
WriteGeneration: hasWriteGeneration ? writeGeneration : -1,
|
|
||||||
ArrayedView: isArrayed);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8555,8 +8357,7 @@ public static partial class AgcExports
|
|||||||
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
|
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||||
bool isStorage,
|
bool isStorage,
|
||||||
uint format,
|
uint format,
|
||||||
uint numberType,
|
uint numberType)
|
||||||
bool isArrayed = false)
|
|
||||||
{
|
{
|
||||||
var fallbackFormat = format == 0 ? 10u : format;
|
var fallbackFormat = format == 0 ? 10u : format;
|
||||||
var fallbackNumberType = numberType;
|
var fallbackNumberType = numberType;
|
||||||
@@ -8570,8 +8371,7 @@ public static partial class AgcExports
|
|||||||
IsFallback: true,
|
IsFallback: true,
|
||||||
IsStorage: isStorage,
|
IsStorage: isStorage,
|
||||||
MipLevels: 1,
|
MipLevels: 1,
|
||||||
MipLevel: 0,
|
MipLevel: 0);
|
||||||
ArrayedView: isArrayed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
|
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
|
||||||
@@ -8947,8 +8747,7 @@ public static partial class AgcExports
|
|||||||
texture,
|
texture,
|
||||||
isStorage,
|
isStorage,
|
||||||
binding.MipLevel ?? 0,
|
binding.MipLevel ?? 0,
|
||||||
binding.SamplerDescriptor,
|
binding.SamplerDescriptor));
|
||||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding)));
|
|
||||||
hasStorageBinding |= isStorage;
|
hasStorageBinding |= isStorage;
|
||||||
|
|
||||||
var descriptorState = descriptorValid ? string.Empty : "/invalid-desc";
|
var descriptorState = descriptorValid ? string.Empty : "/invalid-desc";
|
||||||
@@ -11598,69 +11397,6 @@ public static partial class AgcExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int RemoveResourcesForOwner(SubmittedGpuState state, uint owner)
|
|
||||||
{
|
|
||||||
var stale = new List<uint>();
|
|
||||||
foreach (var (handle, resource) in state.RegisteredResources)
|
|
||||||
{
|
|
||||||
if (resource.Owner == owner)
|
|
||||||
{
|
|
||||||
stale.Add(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var handle in stale)
|
|
||||||
{
|
|
||||||
state.RegisteredResources.Remove(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stale.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "ZLJk9r2+2Aw",
|
|
||||||
ExportName = "sceAgcDriverUnregisterOwnerAndResources",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAgc")]
|
|
||||||
public static int DriverUnregisterOwnerAndResources(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var owner = (uint)ctx[CpuRegister.Rdi];
|
|
||||||
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
|
||||||
int resources;
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
if (!state.ResourceOwners.Remove(owner))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
resources = RemoveResourcesForOwner(state, owner);
|
|
||||||
state.ComputeQueues.Remove(owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceAgc($"agc.driver_unregister_owner owner={owner} resources={resources}");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "SCoAN5fYlUM",
|
|
||||||
ExportName = "sceAgcDriverUnregisterAllResourcesForOwner",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAgc")]
|
|
||||||
public static int DriverUnregisterAllResourcesForOwner(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var owner = (uint)ctx[CpuRegister.Rdi];
|
|
||||||
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
|
||||||
int resources;
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
resources = RemoveResourcesForOwner(state, owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceAgc($"agc.driver_unregister_owner_resources owner={owner} resources={resources}");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "pWLG7WOpVcw",
|
Nid = "pWLG7WOpVcw",
|
||||||
ExportName = "sceAgcDriverUnregisterResource",
|
ExportName = "sceAgcDriverUnregisterResource",
|
||||||
|
|||||||
@@ -194,164 +194,6 @@ internal static class GnmTiling
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetBlockElementDimensions(
|
|
||||||
uint swizzleMode,
|
|
||||||
int bytesPerElement,
|
|
||||||
out int blockWidth,
|
|
||||||
out int blockHeight)
|
|
||||||
{
|
|
||||||
blockWidth = 0;
|
|
||||||
blockHeight = 0;
|
|
||||||
if (bytesPerElement <= 0 ||
|
|
||||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
|
||||||
if (bppLog2 < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
(blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
|
|
||||||
return blockWidth != 0 && blockHeight != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Locates mip 0 in a GFX10 mip chain, which AddrLib stores smallest-first
|
|
||||||
/// (Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled).
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetBaseMipPlacement(
|
|
||||||
uint swizzleMode,
|
|
||||||
int elementsWide,
|
|
||||||
int elementsHigh,
|
|
||||||
int bytesPerElement,
|
|
||||||
uint resourceMipLevels,
|
|
||||||
out ulong byteOffset,
|
|
||||||
out bool inMipTail,
|
|
||||||
out int tailElementX,
|
|
||||||
out int tailElementY,
|
|
||||||
out ulong chainSliceBytes)
|
|
||||||
{
|
|
||||||
byteOffset = 0;
|
|
||||||
inMipTail = false;
|
|
||||||
tailElementX = 0;
|
|
||||||
tailElementY = 0;
|
|
||||||
chainSliceBytes = 0;
|
|
||||||
if (resourceMipLevels <= 1 ||
|
|
||||||
!ShouldDetile(swizzleMode) ||
|
|
||||||
elementsWide <= 0 ||
|
|
||||||
elementsHigh <= 0 ||
|
|
||||||
bytesPerElement <= 0 ||
|
|
||||||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
|
||||||
if (bppLog2 < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var (blockWidth, blockHeight) = SquareBlockDimensions(blockBytes >> bppLog2);
|
|
||||||
var blockSizeLog2 = BitLog2((uint)blockBytes);
|
|
||||||
if (blockWidth == 0 || blockHeight == 0 || blockSizeLog2 < 8)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mipLevels = (int)Math.Min(resourceMipLevels, 16u);
|
|
||||||
var maxMipsInTail = blockSizeLog2 <= 8 ? 0
|
|
||||||
: blockSizeLog2 <= 11
|
|
||||||
? 1 + (1 << (blockSizeLog2 - 9))
|
|
||||||
: blockSizeLog2 - 4;
|
|
||||||
var tailWidth = (blockSizeLog2 & 1) != 0 ? blockWidth >> 1 : blockWidth;
|
|
||||||
var tailHeight = (blockSizeLog2 & 1) != 0 ? blockHeight : blockHeight >> 1;
|
|
||||||
|
|
||||||
var firstMipInTail = mipLevels;
|
|
||||||
var mipSizes = new ulong[mipLevels];
|
|
||||||
for (var i = 0; i < mipLevels; i++)
|
|
||||||
{
|
|
||||||
var mipWidth = Math.Max(elementsWide >> i, 1);
|
|
||||||
var mipHeight = Math.Max(elementsHigh >> i, 1);
|
|
||||||
if (maxMipsInTail > 0 &&
|
|
||||||
mipWidth <= tailWidth &&
|
|
||||||
mipHeight <= tailHeight &&
|
|
||||||
mipLevels - i <= maxMipsInTail)
|
|
||||||
{
|
|
||||||
firstMipInTail = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var alignedWidth = (ulong)(mipWidth + blockWidth - 1) / (ulong)blockWidth * (ulong)blockWidth;
|
|
||||||
var alignedHeight = (ulong)(mipHeight + blockHeight - 1) / (ulong)blockHeight * (ulong)blockHeight;
|
|
||||||
mipSizes[i] = alignedWidth * alignedHeight * (ulong)bytesPerElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstMipInTail == 0)
|
|
||||||
{
|
|
||||||
var m = maxMipsInTail - 1;
|
|
||||||
var mipOffset = m > 6 ? 16 << m : m << 8;
|
|
||||||
var mipX = ((mipOffset >> 9) & 1) |
|
|
||||||
((mipOffset >> 10) & 2) |
|
|
||||||
((mipOffset >> 11) & 4) |
|
|
||||||
((mipOffset >> 12) & 8) |
|
|
||||||
((mipOffset >> 13) & 16) |
|
|
||||||
((mipOffset >> 14) & 32);
|
|
||||||
var mipY = ((mipOffset >> 8) & 1) |
|
|
||||||
((mipOffset >> 9) & 2) |
|
|
||||||
((mipOffset >> 10) & 4) |
|
|
||||||
((mipOffset >> 11) & 8) |
|
|
||||||
((mipOffset >> 12) & 16) |
|
|
||||||
((mipOffset >> 13) & 32);
|
|
||||||
if ((blockSizeLog2 & 1) != 0)
|
|
||||||
{
|
|
||||||
(mipX, mipY) = (mipY, mipX);
|
|
||||||
if ((bppLog2 & 1) != 0)
|
|
||||||
{
|
|
||||||
mipY = (mipY << 1) | (mipX & 1);
|
|
||||||
mipX >>= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (microWidth, microHeight) = SquareBlockDimensions(256 >> bppLog2);
|
|
||||||
if (microWidth == 0 || microHeight == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
tailElementX = mipX * microWidth;
|
|
||||||
tailElementY = mipY * microHeight;
|
|
||||||
if (tailElementX + elementsWide > blockWidth ||
|
|
||||||
tailElementY + elementsHigh > blockHeight)
|
|
||||||
{
|
|
||||||
tailElementX = 0;
|
|
||||||
tailElementY = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inMipTail = true;
|
|
||||||
chainSliceBytes = (ulong)blockBytes;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
byteOffset = firstMipInTail < mipLevels ? (ulong)blockBytes : 0;
|
|
||||||
chainSliceBytes = byteOffset;
|
|
||||||
for (var i = firstMipInTail - 1; i >= 1; i--)
|
|
||||||
{
|
|
||||||
byteOffset += mipSizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < firstMipInTail; i++)
|
|
||||||
{
|
|
||||||
chainSliceBytes += mipSizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||||
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ public static class AvPlayerExports
|
|||||||
private const int FrameBufferCount = 3;
|
private const int FrameBufferCount = 3;
|
||||||
private const int FrameInfoSize = 40;
|
private const int FrameInfoSize = 40;
|
||||||
private const int FrameInfoExSize = 104;
|
private const int FrameInfoExSize = 104;
|
||||||
// This structure is 32 bytes. A larger write can damage the guest stack.
|
private const int StreamInfoSize = 40;
|
||||||
private const int StreamInfoSize = 32;
|
|
||||||
private const int StreamInfoExSize = 32;
|
|
||||||
private const int MaxGuestPathLength = 4096;
|
private const int MaxGuestPathLength = 4096;
|
||||||
private static readonly object StateGate = new();
|
private static readonly object StateGate = new();
|
||||||
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
||||||
@@ -406,8 +404,7 @@ public static class AvPlayerExports
|
|||||||
ExportName = "sceAvPlayerGetStreamInfoEx",
|
ExportName = "sceAvPlayerGetStreamInfoEx",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
|
public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx);
|
||||||
GetStreamInfoCore(ctx, StreamInfoExSize);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XC9wM+xULz8",
|
Nid = "XC9wM+xULz8",
|
||||||
@@ -564,48 +561,12 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RegisterPlayerForTest(
|
|
||||||
ulong handle,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
ulong durationMilliseconds)
|
|
||||||
{
|
|
||||||
PlayerState? previous;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Remove(handle, out previous);
|
|
||||||
Players[handle] = new PlayerState
|
|
||||||
{
|
|
||||||
Handle = handle,
|
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
DurationMilliseconds = durationMilliseconds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
previous?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void RemovePlayerForTest(ulong handle)
|
|
||||||
{
|
|
||||||
PlayerState? player;
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Remove(handle, out player);
|
|
||||||
}
|
|
||||||
|
|
||||||
player?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "d8FcbzfAdQw",
|
Nid = "d8FcbzfAdQw",
|
||||||
ExportName = "sceAvPlayerGetStreamInfo",
|
ExportName = "sceAvPlayerGetStreamInfo",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
|
public static int AvPlayerGetStreamInfo(CpuContext ctx)
|
||||||
GetStreamInfoCore(ctx, StreamInfoSize);
|
|
||||||
|
|
||||||
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
|
|
||||||
{
|
{
|
||||||
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
var infoAddress = ctx[CpuRegister.Rdx];
|
||||||
@@ -617,7 +578,7 @@ public static class AvPlayerExports
|
|||||||
return SetReturn(ctx, InvalidParameters);
|
return SetReturn(ctx, InvalidParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[infoSize];
|
Span<byte> info = stackalloc byte[StreamInfoSize];
|
||||||
info.Clear();
|
info.Clear();
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
||||||
if (streamIndex == 0)
|
if (streamIndex == 0)
|
||||||
@@ -1048,7 +1009,7 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows());
|
var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe");
|
||||||
if (!File.Exists(ffprobe))
|
if (!File.Exists(ffprobe))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -1131,33 +1092,13 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FindFfmpeg() =>
|
private static string? FindFfmpeg()
|
||||||
FindFfmpeg(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
|
|
||||||
Environment.GetEnvironmentVariable("PATH"),
|
|
||||||
OperatingSystem.IsWindows());
|
|
||||||
|
|
||||||
internal static string? FindFfmpeg(
|
|
||||||
string? configured,
|
|
||||||
string? searchPath,
|
|
||||||
bool isWindows)
|
|
||||||
{
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH");
|
||||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||||
{
|
{
|
||||||
return configured;
|
return configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
|
||||||
foreach (var directory in (searchPath ?? string.Empty)
|
|
||||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
var candidate = Path.Combine(RemovePathQuotes(directory), executable);
|
|
||||||
if (File.Exists(candidate))
|
|
||||||
{
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
|
foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" })
|
||||||
{
|
{
|
||||||
if (File.Exists(candidate))
|
if (File.Exists(candidate))
|
||||||
@@ -1168,16 +1109,6 @@ public static class AvPlayerExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string GetFfprobePath(string ffmpeg, bool isWindows) =>
|
|
||||||
Path.Combine(
|
|
||||||
Path.GetDirectoryName(ffmpeg) ?? string.Empty,
|
|
||||||
isWindows ? "ffprobe.exe" : "ffprobe");
|
|
||||||
|
|
||||||
private static string RemovePathQuotes(string directory) =>
|
|
||||||
directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"'
|
|
||||||
? directory[1..^1]
|
|
||||||
: directory;
|
|
||||||
|
|
||||||
internal static string? ResolveGuestPath(string guestPath)
|
internal static string? ResolveGuestPath(string guestPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(guestPath))
|
if (string.IsNullOrWhiteSpace(guestPath))
|
||||||
@@ -1187,9 +1118,7 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
var normalized = guestPath.Replace('\\', '/');
|
var normalized = guestPath.Replace('\\', '/');
|
||||||
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase);
|
||||||
var unrealProjectRelative =
|
var unrealProjectRelative = false;
|
||||||
normalized.StartsWith("../", StringComparison.Ordinal) ||
|
|
||||||
normalized.StartsWith("./", StringComparison.Ordinal);
|
|
||||||
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
|
||||||
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
|
||||||
uri.IsFile)
|
uri.IsFile)
|
||||||
@@ -1220,10 +1149,7 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
if (unrealProjectRelative)
|
if (unrealProjectRelative)
|
||||||
{
|
{
|
||||||
if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized))
|
normalized = RemoveUnrealLeadingDotSegments(normalized);
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
@@ -1307,20 +1233,15 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryRemoveUnrealLeadingDotSegments(
|
private static string RemoveUnrealLeadingDotSegments(string guestPath)
|
||||||
string guestPath,
|
|
||||||
out string normalized)
|
|
||||||
{
|
{
|
||||||
var removedParent = false;
|
|
||||||
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
|
||||||
guestPath.StartsWith("./", StringComparison.Ordinal))
|
guestPath.StartsWith("./", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal);
|
|
||||||
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized = guestPath;
|
return guestPath;
|
||||||
return !removedParent || guestPath.Contains('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
private static bool TryDecodeFileReference(string encoded, out string decoded)
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ internal sealed record GuestDrawTexture(
|
|||||||
uint Pitch = 0,
|
uint Pitch = 0,
|
||||||
uint TileMode = 0,
|
uint TileMode = 0,
|
||||||
uint DstSelect = 0xFAC,
|
uint DstSelect = 0xFAC,
|
||||||
GuestSampler Sampler = default,
|
GuestSampler Sampler = default);
|
||||||
// Guest CPU write-tracker generation of the memory RgbaPixels was read
|
|
||||||
// from; -1 when the range is untracked or the pixels were not read here.
|
|
||||||
long WriteGeneration = -1,
|
|
||||||
bool ArrayedView = false,
|
|
||||||
uint ArrayLayers = 1);
|
|
||||||
|
|
||||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||||
internal readonly record struct GuestSampler(
|
internal readonly record struct GuestSampler(
|
||||||
@@ -53,9 +48,7 @@ internal readonly record struct TextureContentIdentity(
|
|||||||
uint DstSelect,
|
uint DstSelect,
|
||||||
uint TileMode,
|
uint TileMode,
|
||||||
uint Pitch,
|
uint Pitch,
|
||||||
GuestSampler Sampler,
|
GuestSampler Sampler);
|
||||||
bool Arrayed = false,
|
|
||||||
uint ArrayLayers = 1);
|
|
||||||
|
|
||||||
internal sealed record GuestMemoryBuffer(
|
internal sealed record GuestMemoryBuffer(
|
||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ public static class KernelExports
|
|||||||
ExportName = "open",
|
ExportName = "open",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.PosixOpen(ctx);
|
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "1G3lF1Gg1k8",
|
Nid = "1G3lF1Gg1k8",
|
||||||
@@ -376,7 +376,7 @@ public static class KernelExports
|
|||||||
ExportName = "fstat",
|
ExportName = "fstat",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.PosixFstat(ctx);
|
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hcuQgD53UxM",
|
Nid = "hcuQgD53UxM",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -65,10 +65,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
private const uint HostPageExecuteReadWrite = 0x40;
|
private const uint HostPageExecuteReadWrite = 0x40;
|
||||||
private const uint HostPageExecuteWriteCopy = 0x80;
|
private const uint HostPageExecuteWriteCopy = 0x80;
|
||||||
private const uint HostPageGuard = 0x100;
|
private const uint HostPageGuard = 0x100;
|
||||||
private const int Enoent = 2;
|
|
||||||
private const int Ebadf = 9;
|
|
||||||
private const int Enomem = 12;
|
private const int Enomem = 12;
|
||||||
private const int Eacces = 13;
|
|
||||||
private const int Efault = 14;
|
private const int Efault = 14;
|
||||||
private const int Einval = 22;
|
private const int Einval = 22;
|
||||||
private const int Erange = 34;
|
private const int Erange = 34;
|
||||||
@@ -1108,13 +1105,10 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count > 0 && !ctx.Memory.TryCopy(destination, source, (ulong)count))
|
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||||
|
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||||
{
|
{
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
if (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
@@ -1548,13 +1542,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
ExportName = "close",
|
ExportName = "close",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixClose(CpuContext ctx)
|
public static int PosixClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
||||||
{
|
|
||||||
var result = KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "UK2Tl2DWUns",
|
Nid = "UK2Tl2DWUns",
|
||||||
@@ -1611,29 +1599,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translates a failed raw Orbis kernel result into the libc/POSIX ABI:
|
|
||||||
// return -1 with errno set. The raw sceKernel* implementations report the
|
|
||||||
// 0x8002xxxx sentinel through the return value, but the POSIX-named exports
|
|
||||||
// (open/fstat/close/read/write/stat) are called by libc code that expects a
|
|
||||||
// negative result on failure. Leaking the raw sentinel makes callers store
|
|
||||||
// it as a "valid" fd or handle and later dereference it - the null-pointer
|
|
||||||
// access violation seen when Unity's IL2CPP file layer probes an absent
|
|
||||||
// il2cpp.usym. fd-based calls pass notFoundErrno=Ebadf; path-based calls
|
|
||||||
// leave the Enoent default.
|
|
||||||
private static int PosixFailure(CpuContext ctx, int orbisResult, int notFoundErrno = Enoent)
|
|
||||||
{
|
|
||||||
var errno = orbisResult switch
|
|
||||||
{
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => Eacces,
|
|
||||||
_ => notFoundErrno,
|
|
||||||
};
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
|
||||||
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "E6ao34wPw+U",
|
Nid = "E6ao34wPw+U",
|
||||||
ExportName = "stat",
|
ExportName = "stat",
|
||||||
@@ -1642,29 +1607,23 @@ public static partial class KernelMemoryCompatExports
|
|||||||
public static int PosixStat(CpuContext ctx)
|
public static int PosixStat(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var result = KernelStat(ctx);
|
var result = KernelStat(ctx);
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
? 0
|
{
|
||||||
: PosixFailure(ctx, result);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSIX open(2): translates a failed raw open into -1/errno. On success
|
// stat(2) follows the libc/POSIX ABI: failures return -1 and expose
|
||||||
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
|
// the reason through errno. Returning the raw Orbis kernel code here
|
||||||
// prefers a written RAX over the return value), so returning 0 is correct.
|
// makes callers treat a missing file as a non-negative success value.
|
||||||
public static int PosixOpen(CpuContext ctx)
|
var errno = result switch
|
||||||
{
|
{
|
||||||
var result = KernelOpenUnderscore(ctx);
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
|
||||||
? 0
|
_ => 2, // ENOENT
|
||||||
: PosixFailure(ctx, result);
|
};
|
||||||
}
|
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
||||||
|
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
// POSIX fstat(2): a bad fd maps to EBADF rather than the path-oriented ENOENT.
|
return -1;
|
||||||
public static int PosixFstat(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var result = KernelFstat(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1678,7 +1637,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
var count = ctx[CpuRegister.Rsi];
|
var count = ctx[CpuRegister.Rsi];
|
||||||
var idsAddress = ctx[CpuRegister.Rdx];
|
var idsAddress = ctx[CpuRegister.Rdx];
|
||||||
var sizesAddress = ctx[CpuRegister.Rcx];
|
var sizesAddress = ctx[CpuRegister.Rcx];
|
||||||
var errorIndexAddress = ctx[CpuRegister.R8];
|
|
||||||
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
|
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
|
||||||
{
|
{
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||||
@@ -1703,8 +1661,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
var hostPath = ResolveGuestPath(guestPath);
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
if (!TryGetAprFileSize(hostPath, out var fileSize))
|
if (!TryGetAprFileSize(hostPath, out var fileSize))
|
||||||
{
|
{
|
||||||
// Stop at the first miss and report its index.
|
// Per-file resolve: a missing entry gets an invalid id
|
||||||
// The caller can then use its normal file-open fallback.
|
// (0xFFFFFFFF, already written above) and size 0, and the batch
|
||||||
|
// CONTINUES. Aborting the whole batch on the first miss left the
|
||||||
|
// remaining paths unresolved and could stall the guest's asset
|
||||||
|
// streaming when a batch happens to include an absent (e.g.
|
||||||
|
// patch/DLC) file; the caller checks per-file id/size.
|
||||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
||||||
if (sizesAddress != 0 &&
|
if (sizesAddress != 0 &&
|
||||||
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
||||||
@@ -1713,16 +1675,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorIndexAddress != 0 &&
|
continue;
|
||||||
!TryWriteUInt32Compat(ctx, errorIndexAddress, (uint)i))
|
|
||||||
{
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, 2); // ENOENT
|
|
||||||
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
|
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
|
||||||
@@ -2140,15 +2093,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
ExportName = "read",
|
ExportName = "read",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixRead(CpuContext ctx)
|
public static int PosixRead(CpuContext ctx) => KernelReadUnderscore(ctx);
|
||||||
{
|
|
||||||
// On success KernelReadUnderscore writes the byte count into RAX, which
|
|
||||||
// the import bridge prefers over this return value.
|
|
||||||
var result = KernelReadUnderscore(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Cg4srZ6TKbU",
|
Nid = "Cg4srZ6TKbU",
|
||||||
@@ -2347,15 +2292,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
ExportName = "write",
|
ExportName = "write",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixWrite(CpuContext ctx)
|
public static int PosixWrite(CpuContext ctx) => KernelWriteUnderscore(ctx);
|
||||||
{
|
|
||||||
// On success KernelWriteUnderscore writes the byte count into RAX, which
|
|
||||||
// the import bridge prefers over this return value.
|
|
||||||
var result = KernelWriteUnderscore(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
? 0
|
|
||||||
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "4wSze92BhLI",
|
Nid = "4wSze92BhLI",
|
||||||
@@ -2390,37 +2327,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "smIj7eqzZE8",
|
|
||||||
ExportName = "clock_getres",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int ClockGetres(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var timespecAddress = ctx[CpuRegister.Rsi];
|
|
||||||
|
|
||||||
// POSIX allows a null resolution pointer: the call then only validates
|
|
||||||
// the clock id, which every id a title passes here does.
|
|
||||||
if (timespecAddress == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clock_gettime above is backed by DateTimeOffset.UtcNow, whose tick is
|
|
||||||
// 100 ns, so that is the honest resolution to report rather than the 1 ns
|
|
||||||
// a caller might otherwise assume it can rely on.
|
|
||||||
const ulong ResolutionNanoseconds = 100;
|
|
||||||
if (!ctx.TryWriteUInt64(timespecAddress, 0) ||
|
|
||||||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), ResolutionNanoseconds))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "vNe1w4diLCs",
|
Nid = "vNe1w4diLCs",
|
||||||
ExportName = "__tls_get_addr",
|
ExportName = "__tls_get_addr",
|
||||||
@@ -2921,51 +2827,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelMapDirectMemory(CpuContext ctx)
|
public static int KernelMapDirectMemory(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return MapDirectMemoryCore(
|
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
||||||
ctx,
|
var length = ctx[CpuRegister.Rsi];
|
||||||
inOutAddressPointer: ctx[CpuRegister.Rdi],
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
length: ctx[CpuRegister.Rsi],
|
var flags = ctx[CpuRegister.Rcx];
|
||||||
protection: unchecked((int)ctx[CpuRegister.Rdx]),
|
var directMemoryStart = ctx[CpuRegister.R8];
|
||||||
flags: ctx[CpuRegister.Rcx],
|
var alignment = ctx[CpuRegister.R9];
|
||||||
directMemoryStart: ctx[CpuRegister.R8],
|
|
||||||
alignment: ctx[CpuRegister.R9]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "BQQniolj9tQ",
|
|
||||||
ExportName = "sceKernelMapDirectMemory2",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelMapDirectMemory2(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// The "2" variant inserts a memoryType argument (rdx) ahead of v1's
|
|
||||||
// protection, shifting protection/flags/directMemoryStart down one
|
|
||||||
// register each and pushing alignment onto the stack (the 7th argument,
|
|
||||||
// at [rsp + 8], above the return address). The memoryType only selects
|
|
||||||
// cache/GPU access attributes, which this HLE does not model per
|
|
||||||
// mapping, so it is accepted but does not affect placement.
|
|
||||||
ulong alignment = 0;
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out alignment);
|
|
||||||
|
|
||||||
return MapDirectMemoryCore(
|
|
||||||
ctx,
|
|
||||||
inOutAddressPointer: ctx[CpuRegister.Rdi],
|
|
||||||
length: ctx[CpuRegister.Rsi],
|
|
||||||
protection: unchecked((int)ctx[CpuRegister.Rcx]),
|
|
||||||
flags: ctx[CpuRegister.R8],
|
|
||||||
directMemoryStart: ctx[CpuRegister.R9],
|
|
||||||
alignment: alignment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int MapDirectMemoryCore(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong inOutAddressPointer,
|
|
||||||
ulong length,
|
|
||||||
int protection,
|
|
||||||
ulong flags,
|
|
||||||
ulong directMemoryStart,
|
|
||||||
ulong alignment)
|
|
||||||
{
|
|
||||||
if (ShouldTraceDirectMemory())
|
if (ShouldTraceDirectMemory())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -3078,7 +2945,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestWriteWatch.OnDirectMapping(mappedAddress, length, protection);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3461,7 +3327,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
public static int KernelDirectMemoryQuery(CpuContext ctx)
|
public static int KernelDirectMemoryQuery(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var offset = ctx[CpuRegister.Rdi];
|
var offset = ctx[CpuRegister.Rdi];
|
||||||
var flags = ctx[CpuRegister.Rsi];
|
_ = ctx[CpuRegister.Rsi]; // flags
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
var infoAddress = ctx[CpuRegister.Rdx];
|
||||||
var infoSize = ctx[CpuRegister.Rcx];
|
var infoSize = ctx[CpuRegister.Rcx];
|
||||||
if (infoAddress == 0 || infoSize < 24)
|
if (infoAddress == 0 || infoSize < 24)
|
||||||
@@ -3469,90 +3335,27 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset >= DirectMemorySizeBytes)
|
|
||||||
{
|
|
||||||
// Real hardware returns EACCES here (0x8002000D), not ENOENT.
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
|
||||||
}
|
|
||||||
|
|
||||||
var findNext = (flags & 1) != 0;
|
|
||||||
var found = false;
|
|
||||||
var matchStart = 0UL;
|
|
||||||
var matchEnd = 0UL;
|
|
||||||
var matchMemoryType = 0;
|
|
||||||
|
|
||||||
lock (_memoryGate)
|
lock (_memoryGate)
|
||||||
{
|
{
|
||||||
var candidates = _directAllocations.Values
|
foreach (var block in _directAllocations.Values)
|
||||||
.Where(block => findNext
|
|
||||||
? block.Start + block.Length > offset
|
|
||||||
: offset >= block.Start && offset < block.Start + block.Length)
|
|
||||||
.OrderBy(block => block.Start);
|
|
||||||
|
|
||||||
foreach (var block in candidates)
|
|
||||||
{
|
{
|
||||||
found = true;
|
if (offset < block.Start || offset >= block.Start + block.Length)
|
||||||
matchStart = block.Start;
|
{
|
||||||
matchEnd = block.Start + block.Length;
|
continue;
|
||||||
matchMemoryType = block.MemoryType;
|
}
|
||||||
break;
|
|
||||||
|
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
||||||
|
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
||||||
|
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found)
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(infoAddress, matchStart) ||
|
|
||||||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), matchEnd) ||
|
|
||||||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), matchMemoryType))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// POSIX alias of <see cref="KernelMprotect"/>; identical (addr, len, prot)
|
|
||||||
/// argument order. Imported by libcohtml, whose embedded V8 changes page
|
|
||||||
/// permissions through this name when moving JIT pages between writable and
|
|
||||||
/// executable.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "YQOfxL4QfeU",
|
|
||||||
ExportName = "mprotect",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixMprotect(CpuContext ctx) => KernelMprotect(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// POSIX alias of <see cref="KernelMunmap"/>; identical (addr, len) argument
|
|
||||||
/// order.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "UqDGjXA5yUM",
|
|
||||||
ExportName = "munmap",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixMunmap(CpuContext ctx) => KernelMunmap(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reports the 16 KiB page granularity this backend maps and aligns against
|
|
||||||
/// (<see cref="OrbisPageSize"/>), not the host's 4 KiB. An allocator that
|
|
||||||
/// rounded to the host value would hand back sub-page offsets that every
|
|
||||||
/// mapping call here then rejects for misalignment.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "k+AXqu2-eBc",
|
|
||||||
ExportName = "getpagesize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixGetPageSize(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = OrbisPageSize;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -4685,8 +4488,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
allowSearch: false,
|
allowSearch: false,
|
||||||
allowAllocateAtAlternative: false,
|
allowAllocateAtAlternative: false,
|
||||||
"reserve fixed range",
|
"reserve fixed range",
|
||||||
out _,
|
out _);
|
||||||
backPartialOverlap: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
||||||
@@ -4887,7 +4689,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
!guestPath.StartsWith("/", StringComparison.Ordinal) &&
|
!guestPath.StartsWith("/", StringComparison.Ordinal) &&
|
||||||
!guestPath.StartsWith("\\", StringComparison.Ordinal))
|
!guestPath.StartsWith("\\", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var relative = NormalizeMountRelativePath(guestPath);
|
var relative = guestPath.Replace('/', Path.DirectorySeparatorChar);
|
||||||
return Path.Combine(app0Root, relative);
|
return Path.Combine(app0Root, relative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4962,40 +4764,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return _cachedApp0Root;
|
return _cachedApp0Root;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves "." and ".." inside a mount-relative guest path and clamps the
|
|
||||||
// result at the mount root, so a guest path can never escape into the host
|
|
||||||
// filesystem. Unreal Engine titles depend on this: their base directory is
|
|
||||||
// <app>/binaries/<platform>, so they address content with "../../../"
|
|
||||||
// prefixes that land back inside /app0 on real hardware. Combining those
|
|
||||||
// raw against the app0 root walked out of the game folder entirely, so the
|
|
||||||
// title enumerated an unrelated host directory and never found its .pak files.
|
|
||||||
private static string NormalizeMountRelativePath(string relativePath)
|
private static string NormalizeMountRelativePath(string relativePath)
|
||||||
{
|
{
|
||||||
var segments = relativePath.Split(
|
return relativePath
|
||||||
new[] { '/', '\\' },
|
.TrimStart('/', '\\')
|
||||||
StringSplitOptions.RemoveEmptyEntries);
|
.Replace('/', Path.DirectorySeparatorChar)
|
||||||
var resolved = new List<string>(segments.Length);
|
.Replace('\\', Path.DirectorySeparatorChar);
|
||||||
foreach (var segment in segments)
|
|
||||||
{
|
|
||||||
if (segment == ".")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment == "..")
|
|
||||||
{
|
|
||||||
if (resolved.Count > 0)
|
|
||||||
{
|
|
||||||
resolved.RemoveAt(resolved.Count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved.Add(segment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join(Path.DirectorySeparatorChar, resolved);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveDevlogAppRoot()
|
private static string ResolveDevlogAppRoot()
|
||||||
@@ -5818,74 +5592,6 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Inserts <paramref name="replacement"/> into the tracked-region table,
|
|
||||||
/// carving it out of any regions it overlaps and preserving the parts of
|
|
||||||
/// those regions that fall outside it.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The table is a SortedList keyed by start address, so assigning directly
|
|
||||||
/// destroys any existing entry that happens to share a start address. That
|
|
||||||
/// silently discarded enclosing reservations: a title reserves a large range
|
|
||||||
/// and then commits a small mapping at the same base, and the record of
|
|
||||||
/// everything past the small mapping disappears — leaving sceKernelVirtualQuery
|
|
||||||
/// unable to find memory the guest legitimately owns.
|
|
||||||
///
|
|
||||||
/// Carving also keeps the table non-overlapping. Previously a new region
|
|
||||||
/// starting inside an existing one produced two overlapping entries, which
|
|
||||||
/// the ordered scan in TryFindVirtualQueryRegionLocked is not written to
|
|
||||||
/// expect.
|
|
||||||
/// </remarks>
|
|
||||||
private static void ReplaceMappedRegionRangeLocked(MappedRegion replacement)
|
|
||||||
{
|
|
||||||
if (replacement.Length == 0 ||
|
|
||||||
!TryAddU64(replacement.Address, replacement.Length, out var replacementEnd))
|
|
||||||
{
|
|
||||||
_mappedRegions[replacement.Address] = replacement;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = replacement.Address;
|
|
||||||
List<MappedRegion>? overlapping = null;
|
|
||||||
foreach (var region in _mappedRegions.Values)
|
|
||||||
{
|
|
||||||
if (region.Length == 0 ||
|
|
||||||
!TryAddU64(region.Address, region.Length, out var regionEnd))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (region.Address < replacementEnd && regionEnd > start)
|
|
||||||
{
|
|
||||||
(overlapping ??= []).Add(region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlapping is not null)
|
|
||||||
{
|
|
||||||
foreach (var region in overlapping)
|
|
||||||
{
|
|
||||||
_mappedRegions.Remove(region.Address);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var region in overlapping)
|
|
||||||
{
|
|
||||||
var regionEnd = region.Address + region.Length;
|
|
||||||
if (region.Address < start)
|
|
||||||
{
|
|
||||||
AddMappedRegionSliceLocked(region, region.Address, start, region.Protection);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regionEnd > replacementEnd)
|
|
||||||
{
|
|
||||||
AddMappedRegionSliceLocked(region, replacementEnd, regionEnd, region.Protection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mappedRegions[start] = replacement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddMappedRegionSliceLocked(
|
private static void AddMappedRegionSliceLocked(
|
||||||
MappedRegion source,
|
MappedRegion source,
|
||||||
ulong start,
|
ulong start,
|
||||||
|
|||||||
@@ -41,87 +41,18 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
private long _ownerThreadId;
|
public ulong OwnerThreadId { get; set; }
|
||||||
private int _recursionCount;
|
public int RecursionCount { get; set; }
|
||||||
private int _queuedWaiterCount;
|
|
||||||
|
|
||||||
public Lock SyncRoot { get; } = new();
|
|
||||||
public ulong OwnerThreadId
|
|
||||||
{
|
|
||||||
get => unchecked((ulong)Volatile.Read(ref _ownerThreadId));
|
|
||||||
set => Volatile.Write(ref _ownerThreadId, unchecked((long)value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RecursionCount
|
|
||||||
{
|
|
||||||
get => Volatile.Read(ref _recursionCount);
|
|
||||||
set => Volatile.Write(ref _recursionCount, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int QueuedWaiterCount => Volatile.Read(ref _queuedWaiterCount);
|
|
||||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
||||||
|
|
||||||
public bool TryAcquireUncontended(ulong threadId, bool allowWaiterBarge)
|
|
||||||
{
|
|
||||||
if (!allowWaiterBarge && QueuedWaiterCount != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryAcquireOwner(threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAcquireOwner(ulong threadId)
|
|
||||||
{
|
|
||||||
if (Interlocked.CompareExchange(
|
|
||||||
ref _ownerThreadId,
|
|
||||||
unchecked((long)threadId),
|
|
||||||
0) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryReleaseUncontended(ulong threadId)
|
|
||||||
{
|
|
||||||
if (QueuedWaiterCount != 0 || RecursionCount != 1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 0);
|
|
||||||
if (Interlocked.CompareExchange(
|
|
||||||
ref _ownerThreadId,
|
|
||||||
0,
|
|
||||||
unchecked((long)threadId)) == unchecked((long)threadId))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Volatile.Write(ref _recursionCount, 1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int IncrementRecursion() => Interlocked.Increment(ref _recursionCount);
|
|
||||||
|
|
||||||
public int DecrementRecursion() => Interlocked.Decrement(ref _recursionCount);
|
|
||||||
|
|
||||||
public void WaiterAddedLocked() => Interlocked.Increment(ref _queuedWaiterCount);
|
|
||||||
|
|
||||||
public void WaiterRemovedLocked() => Interlocked.Decrement(ref _queuedWaiterCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter
|
||||||
{
|
{
|
||||||
public required ulong ThreadId { get; init; }
|
public required ulong ThreadId { get; init; }
|
||||||
public required string WakeKey { get; init; }
|
public required string WakeKey { get; init; }
|
||||||
public required bool Cooperative { get; set; }
|
public required bool Cooperative { get; init; }
|
||||||
public ManualResetEventSlim? HostSignal { get; set; }
|
|
||||||
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
||||||
public int Granted;
|
public int Granted;
|
||||||
}
|
}
|
||||||
@@ -163,10 +94,7 @@ public static class KernelPthreadCompatExports
|
|||||||
public static int PthreadSelf(CpuContext ctx)
|
public static int PthreadSelf(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle)
|
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
||||||
{
|
|
||||||
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
|
||||||
}
|
|
||||||
ctx[CpuRegister.Rax] = currentThreadHandle;
|
ctx[CpuRegister.Rax] = currentThreadHandle;
|
||||||
TracePthreadSelf(ctx, currentThreadHandle);
|
TracePthreadSelf(ctx, currentThreadHandle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -211,13 +139,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "B5GmVDKwpn0",
|
|
||||||
ExportName = "pthread_yield",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "GBUY7ywdULE",
|
Nid = "GBUY7ywdULE",
|
||||||
ExportName = "scePthreadRename",
|
ExportName = "scePthreadRename",
|
||||||
@@ -650,30 +571,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadOnce"/>. libKernel exports the
|
|
||||||
/// same routine under two NIDs, and shipped middleware links the plain name:
|
|
||||||
/// DOOM's libcohtml, PlayFab and party modules all import this one rather
|
|
||||||
/// than scePthreadOnce.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Z4QosVuAsA0",
|
|
||||||
ExportName = "pthread_once",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadOncePOSIX(CpuContext ctx) => PthreadOnce(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadRename"/>, following the same
|
|
||||||
/// two-NID pattern as <see cref="PthreadOncePOSIX"/>.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "9vyP6Z7bqzc",
|
|
||||||
ExportName = "pthread_rename_np",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadRenameNpPOSIX(CpuContext ctx) => PthreadRename(ctx);
|
|
||||||
|
|
||||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
{
|
{
|
||||||
if (mutexAddress == 0)
|
if (mutexAddress == 0)
|
||||||
@@ -724,7 +621,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.SyncRoot)
|
lock (state)
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
||||||
{
|
{
|
||||||
@@ -756,63 +653,11 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (state.TryAcquireUncontended(currentThreadId, allowWaiterBarge: tryOnly))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
|
||||||
{
|
|
||||||
if (state.Type == MutexTypeRecursive)
|
|
||||||
{
|
|
||||||
state.IncrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
|
||||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeAdaptiveNp)
|
|
||||||
{
|
|
||||||
var adaptiveResult = tryOnly
|
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, adaptiveResult);
|
|
||||||
return adaptiveResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeNormal)
|
|
||||||
{
|
|
||||||
if (tryOnly)
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.IncrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedResult = tryOnly
|
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
|
||||||
return ownedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var canCooperativelyBlock = !tryOnly &&
|
var canCooperativelyBlock = !tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||||
PthreadMutexWaiter? waiter = null;
|
PthreadMutexWaiter? waiter = null;
|
||||||
var acquiredWhileQueueing = false;
|
lock (state)
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
@@ -823,30 +668,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tryOnly && state.Type == MutexTypeAdaptiveNp &&
|
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||||
IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId))
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeAdaptiveNp)
|
|
||||||
{
|
|
||||||
if (tryOnly)
|
|
||||||
{
|
|
||||||
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gen5 runtime wrappers can layer an adaptive lock call over
|
|
||||||
// scePthreadMutexLock for one logical acquisition, followed by
|
|
||||||
// only one unlock. Keep the duplicate acquisition idempotent so
|
|
||||||
// the matching unlock fully releases the HLE mutex.
|
|
||||||
TracePthreadMutex(ctx, "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Type == MutexTypeNormal)
|
|
||||||
{
|
{
|
||||||
if (tryOnly)
|
if (tryOnly)
|
||||||
{
|
{
|
||||||
@@ -855,7 +677,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
||||||
// over a NORMAL kernel mutex. Returning EDEADLK here
|
// over a NORMAL or ADAPTIVE kernel mutex. Returning EDEADLK here
|
||||||
// leaves that guest bookkeeping out of sync with the HLE owner and
|
// leaves that guest bookkeeping out of sync with the HLE owner and
|
||||||
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
||||||
// the compatibility recursion used by the original implementation;
|
// the compatibility recursion used by the original implementation;
|
||||||
@@ -881,10 +703,10 @@ public static class KernelPthreadCompatExports
|
|||||||
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
||||||
// is free (owner==0). The blocking lock still honours FIFO so real
|
// is free (owner==0). The blocking lock still honours FIFO so real
|
||||||
// blocked waiters are not starved by a barging locker.
|
// blocked waiters are not starved by a barging locker.
|
||||||
if (state.OwnerThreadId == 0 &&
|
if (state.OwnerThreadId == 0 && (tryOnly || state.Waiters.Count == 0))
|
||||||
(tryOnly || state.Waiters.Count == 0) &&
|
|
||||||
state.TryAcquireOwner(currentThreadId))
|
|
||||||
{
|
{
|
||||||
|
state.OwnerThreadId = currentThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -896,14 +718,6 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
||||||
acquiredWhileQueueing = TryGrantMutexWaiterLocked(state, waiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acquiredWhileQueueing)
|
|
||||||
{
|
|
||||||
waiter!.HostSignal?.Dispose();
|
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canCooperativelyBlock && waiter is not null &&
|
if (canCooperativelyBlock && waiter is not null &&
|
||||||
@@ -937,29 +751,8 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
string? nextWakeKey = null;
|
||||||
{
|
lock (state)
|
||||||
if (state.RecursionCount > 1)
|
|
||||||
{
|
|
||||||
state.DecrementRecursion();
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.TryReleaseUncontended(currentThreadId))
|
|
||||||
{
|
|
||||||
if (state.QueuedWaiterCount != 0)
|
|
||||||
{
|
|
||||||
WakeFirstMutexWaiter(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PthreadMutexWaiter? nextWaiter = null;
|
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
{
|
||||||
if (state.RecursionCount <= 0)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
@@ -977,29 +770,16 @@ public static class KernelPthreadCompatExports
|
|||||||
if (state.RecursionCount == 0)
|
if (state.RecursionCount == 0)
|
||||||
{
|
{
|
||||||
state.OwnerThreadId = 0;
|
state.OwnerThreadId = 0;
|
||||||
|
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||||
// Hand the mutex directly to the head waiter instead of only
|
? state.Waiters.First.Value.WakeKey
|
||||||
// waking it and relying on it to re-acquire. A woken waiter that
|
: null;
|
||||||
// fails to self-grant (its wake races or is lost) would leave the
|
Monitor.PulseAll(state);
|
||||||
// mutex "free with a queued waiter"; the fast-acquire path refuses
|
|
||||||
// such a mutex (OwnerThreadId == 0 && Waiters.Count == 0), so every
|
|
||||||
// later locker — including the game's main thread — then queues
|
|
||||||
// behind a head that never advances and the process wedges.
|
|
||||||
if (state.Waiters.First is { } headNode &&
|
|
||||||
TryGrantMutexWaiterLocked(state, headNode.Value))
|
|
||||||
{
|
|
||||||
nextWaiter = headNode.Value;
|
|
||||||
if (!nextWaiter.Cooperative)
|
|
||||||
{
|
|
||||||
nextWaiter.HostSignal!.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextWaiter is { Cooperative: true })
|
if (nextWakeKey is not null)
|
||||||
{
|
{
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
@@ -1459,7 +1239,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
lock (mutexState.SyncRoot)
|
lock (mutexState)
|
||||||
{
|
{
|
||||||
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
|
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
|
||||||
{
|
{
|
||||||
@@ -1472,10 +1252,10 @@ public static class KernelPthreadCompatExports
|
|||||||
// mutex held (the unlock below is skipped), wedging every thread
|
// mutex held (the unlock below is skipped), wedging every thread
|
||||||
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
||||||
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
||||||
_ = mutexState.TryAcquireOwner(currentThreadId);
|
mutexState.OwnerThreadId = currentThreadId;
|
||||||
|
mutexState.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
|
else if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
||||||
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
|
||||||
{
|
{
|
||||||
return mutexState.OwnerThreadId == currentThreadId
|
return mutexState.OwnerThreadId == currentThreadId
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
||||||
@@ -1644,7 +1424,6 @@ public static class KernelPthreadCompatExports
|
|||||||
if (node.Value.ThreadId == threadId)
|
if (node.Value.ThreadId == threadId)
|
||||||
{
|
{
|
||||||
state.Waiters.Remove(node);
|
state.Waiters.Remove(node);
|
||||||
state.WaiterRemovedLocked();
|
|
||||||
node.Value.Node = null;
|
node.Value.Node = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1659,10 +1438,8 @@ public static class KernelPthreadCompatExports
|
|||||||
WakeKey = cooperative
|
WakeKey = cooperative
|
||||||
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||||
: string.Empty,
|
: string.Empty,
|
||||||
HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false),
|
|
||||||
};
|
};
|
||||||
waiter.Node = state.Waiters.AddLast(waiter);
|
waiter.Node = state.Waiters.AddLast(waiter);
|
||||||
state.WaiterAddedLocked();
|
|
||||||
return waiter;
|
return waiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,7 +1449,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var mutex = new PthreadMutexState();
|
var mutex = new PthreadMutexState();
|
||||||
PthreadMutexWaiter first;
|
PthreadMutexWaiter first;
|
||||||
PthreadMutexWaiter second;
|
PthreadMutexWaiter second;
|
||||||
lock (mutex.SyncRoot)
|
lock (mutex)
|
||||||
{
|
{
|
||||||
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
||||||
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
||||||
@@ -1716,72 +1493,26 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.TryAcquireOwner(waiter.ThreadId))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Waiters.Remove(waiter.Node);
|
state.Waiters.Remove(waiter.Node);
|
||||||
state.WaiterRemovedLocked();
|
|
||||||
waiter.Node = null;
|
waiter.Node = null;
|
||||||
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
Volatile.Write(ref waiter.Granted, 1);
|
Volatile.Write(ref waiter.Granted, 1);
|
||||||
|
Monitor.PulseAll(state);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WakeFirstMutexWaiter(PthreadMutexState state)
|
|
||||||
{
|
|
||||||
PthreadMutexWaiter? nextWaiter;
|
|
||||||
lock (state.SyncRoot)
|
|
||||||
{
|
|
||||||
if (state.OwnerThreadId != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWaiter = state.Waiters.First?.Value;
|
|
||||||
if (nextWaiter is { Cooperative: false })
|
|
||||||
{
|
|
||||||
nextWaiter.HostSignal!.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextWaiter is { Cooperative: true })
|
|
||||||
{
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
ManualResetEventSlim? hostSignal = null;
|
lock (state)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
while (true)
|
while (!TryGrantMutexWaiterLocked(state, waiter))
|
||||||
{
|
{
|
||||||
lock (state.SyncRoot)
|
Monitor.Wait(state);
|
||||||
{
|
|
||||||
if (waiter.HostSignal is null)
|
|
||||||
{
|
|
||||||
waiter.Cooperative = false;
|
|
||||||
waiter.HostSignal = new ManualResetEventSlim(initialState: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal = waiter.HostSignal;
|
|
||||||
if (TryGrantMutexWaiterLocked(state, waiter))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSignal.Wait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
hostSignal?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGrantBlockedMutexLock(
|
private static bool TryGrantBlockedMutexLock(
|
||||||
@@ -1792,7 +1523,7 @@ public static class KernelPthreadCompatExports
|
|||||||
PthreadMutexWaiter waiter)
|
PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
var granted = false;
|
var granted = false;
|
||||||
lock (state.SyncRoot)
|
lock (state)
|
||||||
{
|
{
|
||||||
granted = TryGrantMutexWaiterLocked(state, waiter);
|
granted = TryGrantMutexWaiterLocked(state, waiter);
|
||||||
}
|
}
|
||||||
@@ -1826,10 +1557,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) =>
|
|
||||||
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) &&
|
|
||||||
guestOwner == currentThreadId;
|
|
||||||
|
|
||||||
private static bool CompleteCondWaiterLocked(
|
private static bool CompleteCondWaiterLocked(
|
||||||
PthreadCondState state,
|
PthreadCondState state,
|
||||||
PthreadCondWaiter waiter,
|
PthreadCondWaiter waiter,
|
||||||
@@ -1845,7 +1572,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waiter.TimeoutTimer?.Dispose();
|
waiter.TimeoutTimer?.Dispose();
|
||||||
waiter.TimeoutTimer = null;
|
waiter.TimeoutTimer = null;
|
||||||
|
|
||||||
lock (waiter.MutexState.SyncRoot)
|
lock (waiter.MutexState)
|
||||||
{
|
{
|
||||||
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
||||||
waiter.MutexState,
|
waiter.MutexState,
|
||||||
@@ -1893,7 +1620,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (waiter.MutexState.SyncRoot)
|
lock (waiter.MutexState)
|
||||||
{
|
{
|
||||||
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -860,18 +860,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The POSIX-named alias of <see cref="PthreadAttrGetschedparam"/>. libKernel
|
|
||||||
/// exports the same routine under two NIDs; middleware compiled against the
|
|
||||||
/// plain POSIX headers links this one rather than scePthreadAttrGetschedparam.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "qlk9pSLsUmM",
|
|
||||||
ExportName = "pthread_attr_getschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetschedparamPOSIX(CpuContext ctx) => PthreadAttrGetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "FXPWHNk8Of0",
|
Nid = "FXPWHNk8Of0",
|
||||||
ExportName = "scePthreadAttrGetschedparam",
|
ExportName = "scePthreadAttrGetschedparam",
|
||||||
@@ -1145,90 +1133,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "SFxTMOfuCkE",
|
|
||||||
ExportName = "pthread_rwlock_tryrdlock",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) =>
|
|
||||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "XhWHn6P5R7U",
|
|
||||||
ExportName = "pthread_rwlock_trywrlock",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) =>
|
|
||||||
PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Non-blocking counterpart of <see cref="PthreadRwlockLockCore"/>: acquires
|
|
||||||
/// only if the lock is free right now, otherwise reports BUSY.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists
|
|
||||||
/// for the scheduler resume path and decrements WaitingWriters on success,
|
|
||||||
/// which is correct only for a thread that previously incremented it. A fresh
|
|
||||||
/// try never did, so reusing it would silently consume another thread's
|
|
||||||
/// waiter count and let a queued writer be skipped.
|
|
||||||
/// </remarks>
|
|
||||||
private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
|
||||||
{
|
|
||||||
if (rwlockAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
|
||||||
lock (rwlock.SyncRoot)
|
|
||||||
{
|
|
||||||
if (write)
|
|
||||||
{
|
|
||||||
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirrors the blocking path's re-entrant compat-writer grant so the
|
|
||||||
// two agree on what counts as already owning the lock.
|
|
||||||
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
|
||||||
{
|
|
||||||
rwlock.AddCompatWriter(currentThreadId);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rwlock.WriterThreadId != 0 ||
|
|
||||||
rwlock.ReaderTotalCount != 0 ||
|
|
||||||
rwlock.CompatWriterTotalCount != 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock");
|
|
||||||
rwlock.WriterThreadId = currentThreadId;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rwlock.WriterThreadId == currentThreadId)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
||||||
}
|
|
||||||
|
|
||||||
rwlock.AddReader(currentThreadId);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "+L98PIbGttk",
|
Nid = "+L98PIbGttk",
|
||||||
ExportName = "scePthreadRwlockUnlock",
|
ExportName = "scePthreadRwlockUnlock",
|
||||||
@@ -1915,94 +1819,4 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSIX-named aliases. libKernel exports each of these routines under two
|
|
||||||
// NIDs -- a scePthread* name and the plain POSIX name -- and middleware
|
|
||||||
// compiled against POSIX headers links the latter. Both take identical
|
|
||||||
// arguments and, per the convention already used by scePthreadOnce's alias,
|
|
||||||
// return the same OrbisGen2Result rather than translating to errno.
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "a2P9wYGeZvc",
|
|
||||||
ExportName = "pthread_setprio",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSetprioPOSIX(CpuContext ctx) => PthreadSetprio(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "FIs3-UQT9sg",
|
|
||||||
ExportName = "pthread_getschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadGetschedparamPOSIX(CpuContext ctx) => PthreadGetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "vQm4fDEsWi8",
|
|
||||||
ExportName = "pthread_attr_getstack",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetstackPOSIX(CpuContext ctx) => PthreadAttrGetstack(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Ucsu-OK+els",
|
|
||||||
ExportName = "pthread_attr_get_np",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetNpPOSIX(CpuContext ctx) => PthreadAttrGet(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JarMIy8kKEY",
|
|
||||||
ExportName = "pthread_attr_setschedpolicy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetschedpolicyPOSIX(CpuContext ctx) => PthreadAttrSetschedpolicy(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "E+tyo3lp5Lw",
|
|
||||||
ExportName = "pthread_attr_setdetachstate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetdetachstatePOSIX(CpuContext ctx) => PthreadAttrSetdetachstate(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "euKRgm0Vn2M",
|
|
||||||
ExportName = "pthread_attr_setschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetschedparamPOSIX(CpuContext ctx) => PthreadAttrSetschedparam(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7ZlAakEf0Qg",
|
|
||||||
ExportName = "pthread_attr_setinheritsched",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetinheritschedPOSIX(CpuContext ctx) => PthreadAttrSetinheritsched(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "0qOtCR-ZHck",
|
|
||||||
ExportName = "pthread_attr_getstacksize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetstacksizePOSIX(CpuContext ctx) => PthreadAttrGetstacksize(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "VUT1ZSrHT0I",
|
|
||||||
ExportName = "pthread_attr_getdetachstate",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetdetachstatePOSIX(CpuContext ctx) => PthreadAttrGetdetachstate(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JKyG3SWyA10",
|
|
||||||
ExportName = "pthread_attr_setguardsize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetguardsizePOSIX(CpuContext ctx) => PthreadAttrSetguardsize(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JNkVVsVDmOk",
|
|
||||||
ExportName = "pthread_attr_getguardsize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetguardsizePOSIX(CpuContext ctx) => PthreadAttrGetguardsize(ctx);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2058,13 +2058,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "NhpspxdjEKU",
|
|
||||||
ExportName = "_nanosleep",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixNanosleepUnderscore(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yS8U2TGCe1A",
|
Nid = "yS8U2TGCe1A",
|
||||||
ExportName = "nanosleep",
|
ExportName = "nanosleep",
|
||||||
|
|||||||
@@ -428,22 +428,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "GEnUkDZoUwY",
|
|
||||||
ExportName = "scePthreadSemInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// scePthreadSemInit(sem, flag, value, name) seems to only support private semaphores
|
|
||||||
if (ctx[CpuRegister.Rsi] != 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PosixSemInit(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "YCV5dGGBcCo",
|
Nid = "YCV5dGGBcCo",
|
||||||
ExportName = "sem_wait",
|
ExportName = "sem_wait",
|
||||||
@@ -462,13 +446,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelWaitSema(ctx);
|
return KernelWaitSema(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "C36iRE0F5sE",
|
|
||||||
ExportName = "scePthreadSemWait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemWait(CpuContext ctx) => PosixSemWait(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WBWzsRifCEA",
|
Nid = "WBWzsRifCEA",
|
||||||
ExportName = "sem_trywait",
|
ExportName = "sem_trywait",
|
||||||
@@ -486,19 +463,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelPollSema(ctx, handle, 1);
|
return KernelPollSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "H2a+IN9TP0E",
|
|
||||||
ExportName = "scePthreadSemTrywait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemTryWait(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var result = PosixSemTryWait(ctx);
|
|
||||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN)
|
|
||||||
: result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "w5IHyvahg-o",
|
Nid = "w5IHyvahg-o",
|
||||||
ExportName = "sem_timedwait",
|
ExportName = "sem_timedwait",
|
||||||
@@ -535,13 +499,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return KernelSignalSema(ctx, handle, 1);
|
return KernelSignalSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "aishVAiFaYM",
|
|
||||||
ExportName = "scePthreadSemPost",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemPost(CpuContext ctx) => PosixSemPost(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Bq+LRV-N6Hk",
|
Nid = "Bq+LRV-N6Hk",
|
||||||
ExportName = "sem_getvalue",
|
ExportName = "sem_getvalue",
|
||||||
@@ -592,13 +549,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Vwc+L05e6oE",
|
|
||||||
ExportName = "scePthreadSemDestroy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadSemDestroy(CpuContext ctx) => PosixSemDestroy(ctx);
|
|
||||||
|
|
||||||
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
||||||
{
|
{
|
||||||
handle = 0;
|
handle = 0;
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
bool allowSearch,
|
bool allowSearch,
|
||||||
bool allowAllocateAtAlternative,
|
bool allowAllocateAtAlternative,
|
||||||
string traceName,
|
string traceName,
|
||||||
out ulong mappedAddress,
|
out ulong mappedAddress)
|
||||||
bool backPartialOverlap = false)
|
|
||||||
{
|
{
|
||||||
mappedAddress = 0;
|
mappedAddress = 0;
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
@@ -43,18 +42,6 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed mappings must cover the whole requested window even when part of
|
|
||||||
// it is already backed by another allocation. The single-call AllocateAt
|
|
||||||
// below is all-or-nothing and fails outright on partial overlap, leaving
|
|
||||||
// the untouched pages unmapped for the guest to fault into. Fill the free
|
|
||||||
// pages directly instead.
|
|
||||||
if (backPartialOverlap &&
|
|
||||||
addressSpace.TryBackFixedRange(desiredAddress, length, executable))
|
|
||||||
{
|
|
||||||
mappedAddress = desiredAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||||
if (allocated == 0)
|
if (allocated == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -184,212 +184,6 @@ public static class NetExports
|
|||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// POSIX alias of <see cref="NetSetsockopt"/>; identical
|
|
||||||
/// (fd, level, option, value, length) argument order.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "fFxGkxF2bVo",
|
|
||||||
ExportName = "setsockopt",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads back the socket options this backend actually tracks: SO_NBIO,
|
|
||||||
/// SO_REUSEADDR and SO_ERROR.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Anything else returns EINVAL rather than a zero-filled buffer. A caller
|
|
||||||
/// that receives success for an option nobody stored would treat whatever
|
|
||||||
/// happens to be in its output buffer as the real setting, which is a harder
|
|
||||||
/// failure to trace than an explicit rejection.
|
|
||||||
/// </remarks>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "6O8EwYOgH9Y",
|
|
||||||
ExportName = "getsockopt",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixGetsockopt(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var level = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
||||||
var option = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
var valueAddress = ctx[CpuRegister.Rcx];
|
|
||||||
var lengthAddress = ctx[CpuRegister.R8];
|
|
||||||
if (!_sockets.TryGetValue(id, out var socket))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueAddress == 0 || lengthAddress == 0 || level != 0xFFFF)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> lengthBytes = stackalloc byte[sizeof(int)];
|
|
||||||
if (!ctx.Memory.TryRead(lengthAddress, lengthBytes))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BinaryPrimitives.ReadInt32LittleEndian(lengthBytes) < sizeof(int))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
int value;
|
|
||||||
switch (option)
|
|
||||||
{
|
|
||||||
// ORBIS_NET_SO_NBIO: mirrors what sceNetSetsockopt stored.
|
|
||||||
case 0x1200:
|
|
||||||
value = socket.Blocking ? 0 : 1;
|
|
||||||
break;
|
|
||||||
case 0x0004:
|
|
||||||
value = (int)socket.GetSocketOption(
|
|
||||||
SocketOptionLevel.Socket,
|
|
||||||
SocketOptionName.ReuseAddress)! != 0 ? 1 : 0;
|
|
||||||
break;
|
|
||||||
// ORBIS_NET_SO_ERROR: nothing here records per-socket async errors,
|
|
||||||
// so report "no pending error" rather than inventing one.
|
|
||||||
case 0x1007:
|
|
||||||
value = 0;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(lengthBytes, sizeof(int));
|
|
||||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes) ||
|
|
||||||
!ctx.Memory.TryWrite(lengthAddress, lengthBytes))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceNet("socket.getsockopt", id, unchecked((uint)option), unchecked((uint)value), 0);
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "fZOeZIOEmLw",
|
|
||||||
ExportName = "send",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixSend(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var bufferAddress = ctx[CpuRegister.Rsi];
|
|
||||||
var length = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
if (!_sockets.TryGetValue(id, out var socket))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length < 0 || (length != 0 && bufferAddress == 0))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = new byte[length];
|
|
||||||
if (!ctx.Memory.TryRead(bufferAddress, payload))
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sent = socket.Send(payload, SocketFlags.None);
|
|
||||||
TraceNet("socket.send", id, unchecked((uint)length), unchecked((uint)sent), 0);
|
|
||||||
return ctx.SetReturn(sent);
|
|
||||||
}
|
|
||||||
catch (SocketException exception)
|
|
||||||
when (exception.SocketErrorCode == SocketError.WouldBlock)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
|
|
||||||
}
|
|
||||||
catch (SocketException)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Formats a binary address as text. Pure conversion with no socket state,
|
|
||||||
/// so it behaves identically to the console version for AF_INET/AF_INET6.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "5jRCs2axtr4",
|
|
||||||
ExportName = "inet_ntop",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixInetNtop(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var family = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
|
||||||
var destinationAddress = ctx[CpuRegister.Rdx];
|
|
||||||
var destinationSize = unchecked((int)ctx[CpuRegister.Rcx]);
|
|
||||||
if (sourceAddress == 0 || destinationAddress == 0 || destinationSize <= 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ORBIS_NET_AF_INET / ORBIS_NET_AF_INET6, matching TryMapAddressFamily.
|
|
||||||
var addressLength = family switch
|
|
||||||
{
|
|
||||||
2 => 4,
|
|
||||||
28 => 16,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (addressLength == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rawAddress = new byte[addressLength];
|
|
||||||
if (!ctx.Memory.TryRead(sourceAddress, rawAddress))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = new IPAddress(rawAddress).ToString();
|
|
||||||
var encoded = Encoding.ASCII.GetBytes(text);
|
|
||||||
|
|
||||||
// POSIX requires the terminator to fit as well; a truncated address string
|
|
||||||
// is worse than a reported failure because the caller cannot detect it.
|
|
||||||
if (encoded.Length + 1 > destinationSize)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = new byte[encoded.Length + 1];
|
|
||||||
encoded.CopyTo(buffer, 0);
|
|
||||||
if (!ctx.Memory.TryWrite(destinationAddress, buffer))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inet_ntop returns the destination pointer on success.
|
|
||||||
ctx[CpuRegister.Rax] = destinationAddress;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bErx49PgxyY",
|
Nid = "bErx49PgxyY",
|
||||||
ExportName = "sceNetBind",
|
ExportName = "sceNetBind",
|
||||||
|
|||||||
@@ -69,23 +69,6 @@ public static class NpManagerExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Accepts the reachability callback and never invokes it. Reachability
|
|
||||||
/// transitions only ever fire on a real PSN connection, which an offline
|
|
||||||
/// session does not have, so registering successfully and staying silent is
|
|
||||||
/// the accurate emulation of a signed-out console rather than a stub.
|
|
||||||
/// </summary>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "hw5KNqAAels",
|
|
||||||
ExportName = "sceNpRegisterNpReachabilityStateCallback",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpManager")]
|
|
||||||
public static int NpRegisterNpReachabilityStateCallback(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "qQJfO8HAiaY",
|
Nid = "qQJfO8HAiaY",
|
||||||
ExportName = "sceNpRegisterStateCallbackA",
|
ExportName = "sceNpRegisterStateCallbackA",
|
||||||
|
|||||||
@@ -80,25 +80,6 @@ public static class NpTrophy2Exports
|
|||||||
LibraryName = "libSceNpTrophy2")]
|
LibraryName = "libSceNpTrophy2")]
|
||||||
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gen5 ABI: context, handle, trophy id, then SceNpTrophy2Details and
|
|
||||||
/// SceNpTrophy2Data output pointers.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Reports "no such trophy" rather than succeeding. Succeeding would require
|
|
||||||
/// filling both output structures, and their exact layouts are not confirmed
|
|
||||||
/// here — a title that trusted zeroed details would read an empty name and a
|
|
||||||
/// grade of zero as real data. NOT_FOUND is a documented outcome that callers
|
|
||||||
/// must already handle, so it degrades along a path the game tests.
|
|
||||||
/// </remarks>
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "EwNylPdWUTM",
|
|
||||||
ExportName = "sceNpTrophy2GetTrophyInfo",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpTrophy2")]
|
|
||||||
public static int NpTrophy2GetTrophyInfo(CpuContext ctx) =>
|
|
||||||
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
|
||||||
|
|
||||||
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||||
{
|
{
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -698,22 +698,14 @@ public static class PlayGoExports
|
|||||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||||
if (!hasMetadata)
|
if (!hasMetadata)
|
||||||
{
|
{
|
||||||
// No PlayGo sidecar: derive the installed chunk set from the pak files
|
// No PlayGo sidecar: report a fully-installed single chunk. Available must
|
||||||
// actually present on disk. A locally dumped title has all of its data
|
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
|
||||||
// installed, and a package that splits content across chunks names them
|
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
||||||
// pakchunk<N>-<platform>.pak, so those N are exactly the chunks that
|
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
||||||
// exist. Reporting only chunk 0 told such a title its remaining content
|
TracePlayGo("metadata_missing; fully-installed single chunk");
|
||||||
// was missing: The Invincible (PPSA06426) ships pakchunk0..8 and spun
|
|
||||||
// forever re-querying scePlayGoGetLocus for a chunk that never became
|
|
||||||
// available. Available must stay true or scePlayGoOpen fails with
|
|
||||||
// NotSupportPlayGo (fatal PS5-component init failure for UE titles).
|
|
||||||
// Ids outside the discovered set still return BAD_CHUNK_ID, so
|
|
||||||
// title-side chunk enumeration still terminates.
|
|
||||||
var installedChunkIds = DiscoverInstalledChunkIds(app0Root);
|
|
||||||
TracePlayGo($"metadata_missing; fully-installed chunks=[{string.Join(',', installedChunkIds)}]");
|
|
||||||
return new PlayGoMetadata(
|
return new PlayGoMetadata(
|
||||||
true,
|
true,
|
||||||
installedChunkIds,
|
[(ushort)0],
|
||||||
PlayGoChunkIdKnowledge.Authoritative);
|
PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,41 +718,6 @@ public static class PlayGoExports
|
|||||||
: PlayGoChunkIdKnowledge.Authoritative);
|
: PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
|
|
||||||
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
|
|
||||||
// is the base chunk and must resolve even for a title with no pak files at
|
|
||||||
// all (which keeps the single-chunk behaviour for such titles).
|
|
||||||
private static ushort[] DiscoverInstalledChunkIds(string app0Root)
|
|
||||||
{
|
|
||||||
var ids = new SortedSet<ushort> { 0 };
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var pakFile in Directory.EnumerateFiles(app0Root, "pakchunk*.pak", SearchOption.AllDirectories))
|
|
||||||
{
|
|
||||||
var name = Path.GetFileNameWithoutExtension(pakFile);
|
|
||||||
var digits = name.AsSpan("pakchunk".Length);
|
|
||||||
var length = 0;
|
|
||||||
while (length < digits.Length && char.IsAsciiDigit(digits[length]))
|
|
||||||
{
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length > 0 && ushort.TryParse(digits[..length], out var chunkId))
|
|
||||||
{
|
|
||||||
ids.Add(chunkId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||||
{
|
{
|
||||||
if (!File.Exists(chunkDefsXml))
|
if (!File.Exists(chunkDefsXml))
|
||||||
|
|||||||
@@ -792,15 +792,28 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
// A small RDX value is a flag, and RCX contains the output address.
|
// The resource-out pointer's argument slot varies by SDK revision: some
|
||||||
// A larger RDX value is the output address for the older ABI.
|
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
|
||||||
|
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
|
||||||
|
// real out-pointer in rcx. Probe the plausible candidates and write the
|
||||||
|
// handle to the first writable one instead of faulting on a bad rdx.
|
||||||
|
// This is a stub-level create (matches shadPS4's return-OK semantics);
|
||||||
|
// never return MEMORY_FAULT for it, or the guest treats savedata init as
|
||||||
|
// failed and never advances.
|
||||||
var resourceAddress = 0UL;
|
var resourceAddress = 0UL;
|
||||||
var selectedAddress = SelectTransactionResourceAddress(
|
foreach (var candidate in new[]
|
||||||
ctx[CpuRegister.Rdx],
|
{
|
||||||
ctx[CpuRegister.Rcx]);
|
ctx[CpuRegister.Rdx],
|
||||||
if (selectedAddress != 0 && TryWriteUInt32(ctx, selectedAddress, id))
|
ctx[CpuRegister.Rcx],
|
||||||
|
ctx[CpuRegister.R8],
|
||||||
|
ctx[CpuRegister.R9],
|
||||||
|
})
|
||||||
{
|
{
|
||||||
resourceAddress = selectedAddress;
|
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
|
||||||
|
{
|
||||||
|
resourceAddress = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
TraceSaveData(
|
||||||
@@ -809,16 +822,6 @@ public static class SaveDataExports
|
|||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ulong SelectTransactionResourceAddress(ulong rdx, ulong rcx)
|
|
||||||
{
|
|
||||||
if (rdx == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rdx <= ushort.MaxValue ? rcx : rdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "lJUQuaKqoKY",
|
Nid = "lJUQuaKqoKY",
|
||||||
ExportName = "sceSaveDataDeleteTransactionResource",
|
ExportName = "sceSaveDataDeleteTransactionResource",
|
||||||
|
|||||||
@@ -472,11 +472,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private static readonly Queue<Presentation> _pendingGuestImagePresentations = new();
|
private static readonly Queue<Presentation> _pendingGuestImagePresentations = new();
|
||||||
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
||||||
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
||||||
// Write-tracker generation last uploaded for a CPU-backed guest image.
|
|
||||||
// A newer generation in the tracker means the guest CPU rewrote the
|
|
||||||
// memory (video frames, streamed atlases) and the upload-known skip in
|
|
||||||
// draw translation must ship fresh texels instead of reusing the image.
|
|
||||||
private static readonly Dictionary<ulong, long> _cpuBackedUploadGenerations = new();
|
|
||||||
private static readonly Dictionary<(int Handle, int BufferIndex), long>
|
private static readonly Dictionary<(int Handle, int BufferIndex), long>
|
||||||
_lastOrderedGuestFlipVersions = new();
|
_lastOrderedGuestFlipVersions = new();
|
||||||
private static long _orderedGuestFlipVersionSequence;
|
private static long _orderedGuestFlipVersionSequence;
|
||||||
@@ -816,7 +811,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_pendingGuestImagePresentations.Clear();
|
_pendingGuestImagePresentations.Clear();
|
||||||
_guestImageWorkSequences.Clear();
|
_guestImageWorkSequences.Clear();
|
||||||
_availableGuestImages.Clear();
|
_availableGuestImages.Clear();
|
||||||
_cpuBackedUploadGenerations.Clear();
|
|
||||||
_lastOrderedGuestFlipVersions.Clear();
|
_lastOrderedGuestFlipVersions.Clear();
|
||||||
_orderedGuestFlipVersionSequence = 0;
|
_orderedGuestFlipVersionSequence = 0;
|
||||||
_pendingGuestImageUploads.Clear();
|
_pendingGuestImageUploads.Clear();
|
||||||
@@ -1690,21 +1684,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return checked((ulong)width * height * bytesPerPixel);
|
return checked((ulong)width * height * bytesPerPixel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps a UNORM swapchain format to the sRGB view of the same bit layout,
|
|
||||||
// or Undefined when no counterpart exists. Used to encode linear-float
|
|
||||||
// guest flips on their way into a UNORM swapchain.
|
|
||||||
internal static Format GetSrgbCounterpart(Format format) => format switch
|
|
||||||
{
|
|
||||||
Format.B8G8R8A8Unorm => Format.B8G8R8A8Srgb,
|
|
||||||
Format.R8G8B8A8Unorm => Format.R8G8B8A8Srgb,
|
|
||||||
_ => Format.Undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Float VideoOut flip buffers hold linear scRGB light; presenting them
|
|
||||||
// requires a linear->sRGB encode that a plain blit does not perform.
|
|
||||||
internal static bool IsLinearFloatPresentSource(Format format) =>
|
|
||||||
format is Format.R16G16B16A16Sfloat or Format.R32G32B32A32Sfloat;
|
|
||||||
|
|
||||||
private static byte[]? TakeGuestImageInitialData(ulong address)
|
private static byte[]? TakeGuestImageInitialData(ulong address)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -1811,30 +1790,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
var known =
|
return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||||
_availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
|
||||||
availableFormat == guestFormat ||
|
availableFormat == guestFormat ||
|
||||||
_pendingGuestImageUploads.ContainsKey((address, guestFormat));
|
_pendingGuestImageUploads.ContainsKey((address, guestFormat));
|
||||||
if (!known)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CPU-backed images (video frames, streamed atlases) go stale when
|
|
||||||
// the guest CPU rewrites the memory after the recorded upload; a
|
|
||||||
// changed write-tracker generation forces a fresh texel copy so
|
|
||||||
// the refresh path can re-upload. GPU-rendered images have no
|
|
||||||
// generation entry and keep the plain availability answer.
|
|
||||||
if (_cpuBackedUploadGenerations.TryGetValue(address, out var uploadedGeneration) &&
|
|
||||||
SharpEmu.HLE.GuestImageWriteTracker.TryGetWriteGeneration(
|
|
||||||
address,
|
|
||||||
out var currentGeneration) &&
|
|
||||||
currentGeneration != uploadedGeneration)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3022,7 +2980,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public uint Height;
|
public uint Height;
|
||||||
public uint RowLength;
|
public uint RowLength;
|
||||||
public uint DstSelect;
|
public uint DstSelect;
|
||||||
public uint Layers = 1;
|
|
||||||
public bool NeedsUpload;
|
public bool NeedsUpload;
|
||||||
public bool OwnsStorage;
|
public bool OwnsStorage;
|
||||||
public bool IsStorage;
|
public bool IsStorage;
|
||||||
@@ -3033,10 +2990,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public Sampler Sampler;
|
public Sampler Sampler;
|
||||||
public GuestImageResource? GuestImage;
|
public GuestImageResource? GuestImage;
|
||||||
public GuestDepthResource? GuestDepth;
|
public GuestDepthResource? GuestDepth;
|
||||||
// Write-tracker generation of the guest memory the staged pixels
|
|
||||||
// were read from; -1 when unknown. Recorded on the guest image
|
|
||||||
// after upload so stale-content skips can be detected.
|
|
||||||
public long WriteGeneration = -1;
|
|
||||||
// A sampled render-target alias cannot remain bound to the same
|
// A sampled render-target alias cannot remain bound to the same
|
||||||
// image while that image is a color attachment. The per-draw
|
// image while that image is a color attachment. The per-draw
|
||||||
// snapshot uses this source to copy the target's pre-draw contents
|
// snapshot uses this source to copy the target's pre-draw contents
|
||||||
@@ -3089,9 +3042,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public ulong WriteAddress;
|
public ulong WriteAddress;
|
||||||
public uint Width;
|
public uint Width;
|
||||||
public uint Height;
|
public uint Height;
|
||||||
// Unscaled guest-requested size; Width/Height are the physical (scaled) backing size.
|
|
||||||
public uint LogicalWidth;
|
|
||||||
public uint LogicalHeight;
|
|
||||||
public uint GuestFormat;
|
public uint GuestFormat;
|
||||||
public uint SwizzleMode;
|
public uint SwizzleMode;
|
||||||
public Image Image;
|
public Image Image;
|
||||||
@@ -3121,9 +3071,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public long FlipVersion;
|
public long FlipVersion;
|
||||||
public uint Width;
|
public uint Width;
|
||||||
public uint Height;
|
public uint Height;
|
||||||
// Unscaled guest-requested size; Width/Height are the physical (scaled) backing size.
|
|
||||||
public uint LogicalWidth;
|
|
||||||
public uint LogicalHeight;
|
|
||||||
public uint MipLevels;
|
public uint MipLevels;
|
||||||
public uint GuestFormat;
|
public uint GuestFormat;
|
||||||
public Format Format;
|
public Format Format;
|
||||||
@@ -5457,8 +5404,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
FlipVersion = version,
|
FlipVersion = version,
|
||||||
Width = source.Width,
|
Width = source.Width,
|
||||||
Height = source.Height,
|
Height = source.Height,
|
||||||
LogicalWidth = source.LogicalWidth,
|
|
||||||
LogicalHeight = source.LogicalHeight,
|
|
||||||
MipLevels = 1,
|
MipLevels = 1,
|
||||||
GuestFormat = source.GuestFormat,
|
GuestFormat = source.GuestFormat,
|
||||||
Format = source.Format,
|
Format = source.Format,
|
||||||
@@ -6942,7 +6887,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
|
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
|
||||||
if (texture.Address != 0 &&
|
if (texture.Address != 0 &&
|
||||||
!(texture.ArrayedView && texture.ArrayLayers > 1) &&
|
|
||||||
TryResolveGuestImageAlias(texture, vkFormat, out var guestImage) &&
|
TryResolveGuestImageAlias(texture, vkFormat, out var guestImage) &&
|
||||||
TryGetOrCreateGuestImageView(
|
TryGetOrCreateGuestImageView(
|
||||||
guestImage,
|
guestImage,
|
||||||
@@ -6950,8 +6894,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
mipLevel: texture.BaseMipLevel,
|
mipLevel: texture.BaseMipLevel,
|
||||||
levelCount: texture.MipLevels,
|
levelCount: texture.MipLevels,
|
||||||
dstSelect: texture.DstSelect,
|
dstSelect: texture.DstSelect,
|
||||||
out var view,
|
out var view))
|
||||||
arrayedView: texture.ArrayedView))
|
|
||||||
{
|
{
|
||||||
if (ShouldTraceVulkanResources() &&
|
if (ShouldTraceVulkanResources() &&
|
||||||
_tracedTextureCacheHits.Add(
|
_tracedTextureCacheHits.Add(
|
||||||
@@ -7061,18 +7004,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
|
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
|
||||||
guestImage.CpuContentFingerprint == fingerprint)
|
guestImage.CpuContentFingerprint == fingerprint)
|
||||||
{
|
{
|
||||||
// Content unchanged despite a newer write generation: advance
|
|
||||||
// the recorded generation so later draws can skip the copy
|
|
||||||
// again instead of restaging identical texels every draw.
|
|
||||||
if (texture.WriteGeneration >= 0)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_cpuBackedUploadGenerations[texture.Address] =
|
|
||||||
texture.WriteGeneration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7102,7 +7033,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
GuestImage = guestImage,
|
GuestImage = guestImage,
|
||||||
CpuContentFingerprint = fingerprint,
|
CpuContentFingerprint = fingerprint,
|
||||||
UpdatesCpuContent = true,
|
UpdatesCpuContent = true,
|
||||||
WriteGeneration = texture.WriteGeneration,
|
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -7127,8 +7057,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// but it must not make a differently sized alias outrank the image
|
// but it must not make a differently sized alias outrank the image
|
||||||
// that the texture descriptor actually names.
|
// that the texture descriptor actually names.
|
||||||
var score = 0;
|
var score = 0;
|
||||||
if (candidate.LogicalWidth == texture.Width &&
|
if (candidate.Width == texture.Width &&
|
||||||
candidate.LogicalHeight == texture.Height)
|
candidate.Height == texture.Height)
|
||||||
{
|
{
|
||||||
score += 32;
|
score += 32;
|
||||||
}
|
}
|
||||||
@@ -7213,7 +7143,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (texture.Width > depth.LogicalWidth || texture.Height > depth.LogicalHeight)
|
if (texture.Width > depth.Width || texture.Height > depth.Height)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -7298,9 +7228,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
texture.DstSelect,
|
texture.DstSelect,
|
||||||
texture.TileMode,
|
texture.TileMode,
|
||||||
texture.Pitch,
|
texture.Pitch,
|
||||||
texture.Sampler,
|
texture.Sampler);
|
||||||
texture.ArrayedView,
|
|
||||||
Math.Max(texture.ArrayLayers, 1));
|
|
||||||
if (_textureCache.TryGetValue(key, out var cached))
|
if (_textureCache.TryGetValue(key, out var cached))
|
||||||
{
|
{
|
||||||
return cached;
|
return cached;
|
||||||
@@ -7425,8 +7353,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
GuestDrawTexture texture,
|
GuestDrawTexture texture,
|
||||||
GuestImageResource guestImage)
|
GuestImageResource guestImage)
|
||||||
{
|
{
|
||||||
if (guestImage.LogicalWidth == texture.Width &&
|
if (guestImage.Width == texture.Width &&
|
||||||
guestImage.LogicalHeight == texture.Height)
|
guestImage.Height == texture.Height)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -7438,8 +7366,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return texture.Width <= guestImage.LogicalWidth &&
|
return texture.Width <= guestImage.Width &&
|
||||||
texture.Height <= guestImage.LogicalHeight;
|
texture.Height <= guestImage.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
@@ -7606,8 +7534,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Address = 0,
|
Address = 0,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
LogicalWidth = width,
|
|
||||||
LogicalHeight = height,
|
|
||||||
MipLevels = 1,
|
MipLevels = 1,
|
||||||
GuestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType),
|
GuestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType),
|
||||||
Format = vkFormat,
|
Format = vkFormat,
|
||||||
@@ -7690,7 +7616,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
: width;
|
: width;
|
||||||
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
|
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
|
||||||
|
|
||||||
var layers = Math.Max(texture.ArrayLayers, 1);
|
|
||||||
var expectedSize = GetTextureByteCount(texture.Format, rowLength, height);
|
var expectedSize = GetTextureByteCount(texture.Format, rowLength, height);
|
||||||
if (ShouldTraceVulkanResources() &&
|
if (ShouldTraceVulkanResources() &&
|
||||||
_tracedTextureUploads.Add((texture.Address, width, height, vkFormat)))
|
_tracedTextureUploads.Add((texture.Address, width, height, vkFormat)))
|
||||||
@@ -7699,16 +7624,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " +
|
$"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " +
|
||||||
$"fmt={texture.Format} num={texture.NumberType} vk={vkFormat} " +
|
$"fmt={texture.Format} num={texture.NumberType} vk={vkFormat} " +
|
||||||
$"size={width}x{height} row={rowLength} tile={texture.TileMode} " +
|
$"size={width}x{height} row={rowLength} tile={texture.TileMode} " +
|
||||||
$"layers={layers} dst=0x{texture.DstSelect:X3} " +
|
$"dst=0x{texture.DstSelect:X3} " +
|
||||||
$"bytes={texture.RgbaPixels.Length} expected={expectedSize}");
|
$"bytes={texture.RgbaPixels.Length} expected={expectedSize}");
|
||||||
}
|
}
|
||||||
var pixels = texture.RgbaPixels.Length == (int)(expectedSize * layers)
|
var pixels = texture.RgbaPixels.Length == (int)expectedSize
|
||||||
? texture.RgbaPixels
|
? texture.RgbaPixels
|
||||||
: CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize);
|
: CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize);
|
||||||
if (!ReferenceEquals(pixels, texture.RgbaPixels))
|
|
||||||
{
|
|
||||||
layers = 1;
|
|
||||||
}
|
|
||||||
if (AddressListContains("SHARPEMU_FORCE_WHITE_TEXTURE_TARGETS", texture.Address))
|
if (AddressListContains("SHARPEMU_FORCE_WHITE_TEXTURE_TARGETS", texture.Address))
|
||||||
{
|
{
|
||||||
pixels = pixels.ToArray();
|
pixels = pixels.ToArray();
|
||||||
@@ -7741,7 +7662,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Format = vkFormat,
|
Format = vkFormat,
|
||||||
Extent = new Extent3D(width, height, 1),
|
Extent = new Extent3D(width, height, 1),
|
||||||
MipLevels = 1,
|
MipLevels = 1,
|
||||||
ArrayLayers = layers,
|
ArrayLayers = 1,
|
||||||
Samples = SampleCountFlags.Count1Bit,
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
Tiling = ImageTiling.Optimal,
|
Tiling = ImageTiling.Optimal,
|
||||||
Usage = supportsAttachmentUsage
|
Usage = supportsAttachmentUsage
|
||||||
@@ -7771,12 +7692,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
SType = StructureType.ImageViewCreateInfo,
|
SType = StructureType.ImageViewCreateInfo,
|
||||||
Image = image,
|
Image = image,
|
||||||
ViewType = texture.ArrayedView
|
ViewType = ImageViewType.Type2D,
|
||||||
? ImageViewType.Type2DArray
|
|
||||||
: ImageViewType.Type2D,
|
|
||||||
Format = vkFormat,
|
Format = vkFormat,
|
||||||
Components = ToVkComponentMapping(texture.DstSelect),
|
Components = ToVkComponentMapping(texture.DstSelect),
|
||||||
SubresourceRange = ColorSubresourceRange(layerCount: layers),
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
};
|
};
|
||||||
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
|
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
|
||||||
var debugName = TextureDebugName(texture, vkFormat);
|
var debugName = TextureDebugName(texture, vkFormat);
|
||||||
@@ -7794,18 +7713,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Height = height,
|
Height = height,
|
||||||
RowLength = rowLength,
|
RowLength = rowLength,
|
||||||
DstSelect = texture.DstSelect,
|
DstSelect = texture.DstSelect,
|
||||||
Layers = layers,
|
|
||||||
NeedsUpload = true,
|
NeedsUpload = true,
|
||||||
OwnsStorage = true,
|
OwnsStorage = true,
|
||||||
SamplerState = texture.Sampler,
|
SamplerState = texture.Sampler,
|
||||||
CpuContentFingerprint = contentFingerprint,
|
CpuContentFingerprint = contentFingerprint,
|
||||||
UpdatesCpuContent = texture.Address != 0,
|
UpdatesCpuContent = texture.Address != 0,
|
||||||
WriteGeneration = texture.WriteGeneration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (texture.Address != 0 &&
|
if (texture.Address != 0 &&
|
||||||
!texture.ArrayedView &&
|
|
||||||
layers == 1 &&
|
|
||||||
!_guestImages.ContainsKey(texture.Address))
|
!_guestImages.ContainsKey(texture.Address))
|
||||||
{
|
{
|
||||||
var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType);
|
var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType);
|
||||||
@@ -7814,8 +7729,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Address = texture.Address,
|
Address = texture.Address,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
LogicalWidth = width,
|
|
||||||
LogicalHeight = height,
|
|
||||||
MipLevels = 1,
|
MipLevels = 1,
|
||||||
GuestFormat = guestFormat,
|
GuestFormat = guestFormat,
|
||||||
Format = vkFormat,
|
Format = vkFormat,
|
||||||
@@ -7837,12 +7750,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_availableGuestImages[texture.Address] = guestFormat;
|
_availableGuestImages[texture.Address] = guestFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (texture.WriteGeneration >= 0)
|
|
||||||
{
|
|
||||||
_cpuBackedUploadGenerations[texture.Address] =
|
|
||||||
texture.WriteGeneration;
|
|
||||||
}
|
|
||||||
|
|
||||||
_guestImageExtents[texture.Address] =
|
_guestImageExtents[texture.Address] =
|
||||||
(width, height, expectedSize);
|
(width, height, expectedSize);
|
||||||
}
|
}
|
||||||
@@ -9188,19 +9095,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private static GuestRect ClampScissor(GuestRect? scissor, Extent2D extent)
|
private static GuestRect ClampScissor(GuestRect? scissor, Extent2D extent)
|
||||||
{
|
{
|
||||||
if (scissor is not { } guestRect)
|
if (scissor is not { } rect)
|
||||||
{
|
{
|
||||||
return new GuestRect(0, 0, extent.Width, extent.Height);
|
return new GuestRect(0, 0, extent.Width, extent.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rect = _renderResolutionScale == 1.0
|
|
||||||
? guestRect
|
|
||||||
: new GuestRect(
|
|
||||||
(int)Math.Round(guestRect.X * _renderResolutionScale),
|
|
||||||
(int)Math.Round(guestRect.Y * _renderResolutionScale),
|
|
||||||
Math.Max(1u, (uint)Math.Round(guestRect.Width * _renderResolutionScale)),
|
|
||||||
Math.Max(1u, (uint)Math.Round(guestRect.Height * _renderResolutionScale)));
|
|
||||||
|
|
||||||
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
|
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
|
||||||
var top = Math.Clamp(rect.Y, 0, checked((int)extent.Height));
|
var top = Math.Clamp(rect.Y, 0, checked((int)extent.Height));
|
||||||
var right = Math.Clamp(
|
var right = Math.Clamp(
|
||||||
@@ -9228,22 +9127,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private static Viewport ClampViewport(GuestViewport? viewport, Extent2D extent)
|
private static Viewport ClampViewport(GuestViewport? viewport, Extent2D extent)
|
||||||
{
|
{
|
||||||
if (viewport is not { } guestRect)
|
if (viewport is not { } rect)
|
||||||
{
|
{
|
||||||
return new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
|
return new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scale = (float)_renderResolutionScale;
|
|
||||||
var rect = scale == 1f
|
|
||||||
? guestRect
|
|
||||||
: guestRect with
|
|
||||||
{
|
|
||||||
X = guestRect.X * scale,
|
|
||||||
Y = guestRect.Y * scale,
|
|
||||||
Width = guestRect.Width * scale,
|
|
||||||
Height = guestRect.Height * scale,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Do NOT trim the rectangle to the render target: Vulkan allows
|
// Do NOT trim the rectangle to the render target: Vulkan allows
|
||||||
// viewports that extend beyond the framebuffer (rendering is
|
// viewports that extend beyond the framebuffer (rendering is
|
||||||
// confined by the scissor), and trimming changes the guest's
|
// confined by the scissor), and trimming changes the guest's
|
||||||
@@ -10559,11 +10447,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
draw.RenderState.Depth) &&
|
draw.RenderState.Depth) &&
|
||||||
work.DepthTarget is { } depthTarget)
|
work.DepthTarget is { } depthTarget)
|
||||||
{
|
{
|
||||||
// Logical dims: GetOrCreateGuestDepth below scales itself.
|
|
||||||
var resolution = GuestDepthExtentResolver.Resolve(
|
var resolution = GuestDepthExtentResolver.Resolve(
|
||||||
depthTarget,
|
depthTarget,
|
||||||
firstTarget.LogicalWidth,
|
firstTarget.Width,
|
||||||
firstTarget.LogicalHeight,
|
firstTarget.Height,
|
||||||
draw.Textures);
|
draw.Textures);
|
||||||
var effectiveDepthTarget = resolution.IsUsable &&
|
var effectiveDepthTarget = resolution.IsUsable &&
|
||||||
(resolution.Width != depthTarget.Width ||
|
(resolution.Width != depthTarget.Width ||
|
||||||
@@ -11263,14 +11150,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"address 0x{target.Address:X16}.");
|
$"address 0x{target.Address:X16}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage/UAV images keep native guest dimensions (compute shaders index them directly).
|
var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels);
|
||||||
var physicalWidth = requiresStorage
|
|
||||||
? target.Width
|
|
||||||
: ScaleGuestDimension(target.Width);
|
|
||||||
var physicalHeight = requiresStorage
|
|
||||||
? target.Height
|
|
||||||
: ScaleGuestDimension(target.Height);
|
|
||||||
var mipLevels = ClampMipLevels(physicalWidth, physicalHeight, target.MipLevels);
|
|
||||||
var guestFormat = GetGuestTextureFormat(target.Format, target.NumberType);
|
var guestFormat = GetGuestTextureFormat(target.Format, target.NumberType);
|
||||||
var requestedKey = new GuestImageVariantKey(
|
var requestedKey = new GuestImageVariantKey(
|
||||||
target.Address,
|
target.Address,
|
||||||
@@ -11281,8 +11161,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
format);
|
format);
|
||||||
if (_guestImages.TryGetValue(target.Address, out var existing))
|
if (_guestImages.TryGetValue(target.Address, out var existing))
|
||||||
{
|
{
|
||||||
if (existing.LogicalWidth == target.Width &&
|
if (existing.Width == target.Width &&
|
||||||
existing.LogicalHeight == target.Height &&
|
existing.Height == target.Height &&
|
||||||
existing.MipLevels == mipLevels &&
|
existing.MipLevels == mipLevels &&
|
||||||
existing.GuestFormat == guestFormat &&
|
existing.GuestFormat == guestFormat &&
|
||||||
existing.Format == format)
|
existing.Format == format)
|
||||||
@@ -11332,8 +11212,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_guestImageVariants.Add(
|
_guestImageVariants.Add(
|
||||||
new GuestImageVariantKey(
|
new GuestImageVariantKey(
|
||||||
existing.Address,
|
existing.Address,
|
||||||
existing.LogicalWidth,
|
existing.Width,
|
||||||
existing.LogicalHeight,
|
existing.Height,
|
||||||
existing.MipLevels,
|
existing.MipLevels,
|
||||||
existing.GuestFormat,
|
existing.GuestFormat,
|
||||||
existing.Format),
|
existing.Format),
|
||||||
@@ -11342,7 +11222,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Remove(target.Address);
|
_availableGuestImages.Remove(target.Address);
|
||||||
_cpuBackedUploadGenerations.Remove(target.Address);
|
|
||||||
_guestImageExtents.Remove(target.Address);
|
_guestImageExtents.Remove(target.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11367,7 +11246,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_guestImages.Add(target.Address, retained);
|
_guestImages.Add(target.Address, retained);
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_cpuBackedUploadGenerations.Remove(target.Address);
|
|
||||||
_guestImageExtents[target.Address] = (
|
_guestImageExtents[target.Address] = (
|
||||||
target.Width,
|
target.Width,
|
||||||
target.Height,
|
target.Height,
|
||||||
@@ -11402,7 +11280,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ImageCreateFlags.CreateExtendedUsageBit,
|
ImageCreateFlags.CreateExtendedUsageBit,
|
||||||
ImageType = ImageType.Type2D,
|
ImageType = ImageType.Type2D,
|
||||||
Format = format,
|
Format = format,
|
||||||
Extent = new Extent3D(physicalWidth, physicalHeight, 1),
|
Extent = new Extent3D(target.Width, target.Height, 1),
|
||||||
MipLevels = mipLevels,
|
MipLevels = mipLevels,
|
||||||
ArrayLayers = 1,
|
ArrayLayers = 1,
|
||||||
Samples = SampleCountFlags.Count1Bit,
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
@@ -11475,17 +11353,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CreateRenderPassAndFramebuffer(
|
CreateRenderPassAndFramebuffer(
|
||||||
format,
|
format,
|
||||||
mipViews[0],
|
mipViews[0],
|
||||||
physicalWidth,
|
target.Width,
|
||||||
physicalHeight);
|
target.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resource = new GuestImageResource
|
var resource = new GuestImageResource
|
||||||
{
|
{
|
||||||
Address = target.Address,
|
Address = target.Address,
|
||||||
Width = physicalWidth,
|
Width = target.Width,
|
||||||
Height = physicalHeight,
|
Height = target.Height,
|
||||||
LogicalWidth = target.Width,
|
|
||||||
LogicalHeight = target.Height,
|
|
||||||
MipLevels = mipLevels,
|
MipLevels = mipLevels,
|
||||||
GuestFormat = guestFormat,
|
GuestFormat = guestFormat,
|
||||||
Format = format,
|
Format = format,
|
||||||
@@ -11741,19 +11617,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
var physicalWidth = ScaleGuestDimension(target.Width);
|
var (image, memory, view) = CreateDepthAttachment(target.Width, target.Height);
|
||||||
var physicalHeight = ScaleGuestDimension(target.Height);
|
|
||||||
var (image, memory, view) = CreateDepthAttachment(physicalWidth, physicalHeight);
|
|
||||||
var resource = new GuestDepthResource
|
var resource = new GuestDepthResource
|
||||||
{
|
{
|
||||||
Key = key,
|
Key = key,
|
||||||
Address = target.Address,
|
Address = target.Address,
|
||||||
ReadAddress = target.ReadAddress,
|
ReadAddress = target.ReadAddress,
|
||||||
WriteAddress = target.WriteAddress,
|
WriteAddress = target.WriteAddress,
|
||||||
Width = physicalWidth,
|
Width = target.Width,
|
||||||
Height = physicalHeight,
|
Height = target.Height,
|
||||||
LogicalWidth = target.Width,
|
|
||||||
LogicalHeight = target.Height,
|
|
||||||
GuestFormat = target.GuestFormat,
|
GuestFormat = target.GuestFormat,
|
||||||
SwizzleMode = target.SwizzleMode,
|
SwizzleMode = target.SwizzleMode,
|
||||||
Image = image,
|
Image = image,
|
||||||
@@ -12126,12 +11998,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
uint mipLevel,
|
uint mipLevel,
|
||||||
uint levelCount,
|
uint levelCount,
|
||||||
uint dstSelect,
|
uint dstSelect,
|
||||||
out ImageView view,
|
out ImageView view)
|
||||||
bool arrayedView = false)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
view = GetOrCreateGuestImageView(resource, format, mipLevel, levelCount, dstSelect, arrayedView);
|
view = GetOrCreateGuestImageView(resource, format, mipLevel, levelCount, dstSelect);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -12149,8 +12020,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Format format,
|
Format format,
|
||||||
uint mipLevel,
|
uint mipLevel,
|
||||||
uint levelCount,
|
uint levelCount,
|
||||||
uint dstSelect = 0xFAC,
|
uint dstSelect = 0xFAC)
|
||||||
bool arrayedView = false)
|
|
||||||
{
|
{
|
||||||
if (mipLevel >= resource.MipLevels)
|
if (mipLevel >= resource.MipLevels)
|
||||||
{
|
{
|
||||||
@@ -12160,7 +12030,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
levelCount = Math.Max(levelCount, 1);
|
levelCount = Math.Max(levelCount, 1);
|
||||||
levelCount = Math.Min(levelCount, resource.MipLevels - mipLevel);
|
levelCount = Math.Min(levelCount, resource.MipLevels - mipLevel);
|
||||||
if (format == resource.Format && dstSelect == 0xFAC && !arrayedView)
|
if (format == resource.Format && dstSelect == 0xFAC)
|
||||||
{
|
{
|
||||||
if (mipLevel == 0 && levelCount == resource.MipLevels)
|
if (mipLevel == 0 && levelCount == resource.MipLevels)
|
||||||
{
|
{
|
||||||
@@ -12179,7 +12049,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"Incompatible image view format {format} for image {resource.Format}.");
|
$"Incompatible image view format {format} for image {resource.Format}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = (format, mipLevel + (arrayedView ? 0x100u : 0), levelCount, dstSelect);
|
var key = (format, mipLevel, levelCount, dstSelect);
|
||||||
if (resource.FormatViews.TryGetValue(key, out var existing))
|
if (resource.FormatViews.TryGetValue(key, out var existing))
|
||||||
{
|
{
|
||||||
return existing;
|
return existing;
|
||||||
@@ -12189,7 +12059,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
SType = StructureType.ImageViewCreateInfo,
|
SType = StructureType.ImageViewCreateInfo,
|
||||||
Image = resource.Image,
|
Image = resource.Image,
|
||||||
ViewType = arrayedView ? ImageViewType.Type2DArray : ImageViewType.Type2D,
|
ViewType = ImageViewType.Type2D,
|
||||||
Format = format,
|
Format = format,
|
||||||
Components = ToVkComponentMapping(dstSelect),
|
Components = ToVkComponentMapping(dstSelect),
|
||||||
SubresourceRange = ColorSubresourceRange(mipLevel, levelCount),
|
SubresourceRange = ColorSubresourceRange(mipLevel, levelCount),
|
||||||
@@ -13295,7 +13165,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
Image = texture.Image,
|
Image = texture.Image,
|
||||||
SubresourceRange = ColorSubresourceRange(layerCount: texture.Layers),
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
};
|
};
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
@@ -13317,7 +13187,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ImageSubresource = new ImageSubresourceLayers
|
ImageSubresource = new ImageSubresourceLayers
|
||||||
{
|
{
|
||||||
AspectMask = ImageAspectFlags.ColorBit,
|
AspectMask = ImageAspectFlags.ColorBit,
|
||||||
LayerCount = texture.Layers,
|
LayerCount = 1,
|
||||||
},
|
},
|
||||||
ImageExtent = new Extent3D(texture.Width, texture.Height, 1),
|
ImageExtent = new Extent3D(texture.Width, texture.Height, 1),
|
||||||
};
|
};
|
||||||
@@ -13339,7 +13209,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
Image = texture.Image,
|
Image = texture.Image,
|
||||||
SubresourceRange = ColorSubresourceRange(layerCount: texture.Layers),
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
};
|
};
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
@@ -14003,11 +13873,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (texture.UpdatesCpuContent)
|
if (texture.UpdatesCpuContent)
|
||||||
{
|
{
|
||||||
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
||||||
if (texture.WriteGeneration >= 0)
|
|
||||||
{
|
|
||||||
_cpuBackedUploadGenerations[guestImage.Address] =
|
|
||||||
texture.WriteGeneration;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14215,22 +14080,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
!string.IsNullOrWhiteSpace(
|
!string.IsNullOrWhiteSpace(
|
||||||
Environment.GetEnvironmentVariable(
|
Environment.GetEnvironmentVariable(
|
||||||
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||||
private static readonly double _renderResolutionScale =
|
|
||||||
double.TryParse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_RENDER_SCALE"),
|
|
||||||
System.Globalization.NumberStyles.Float,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
|
||||||
out var renderResolutionScale) &&
|
|
||||||
renderResolutionScale > 0 &&
|
|
||||||
renderResolutionScale <= 2.0
|
|
||||||
? renderResolutionScale
|
|
||||||
: 1.0;
|
|
||||||
|
|
||||||
private static uint ScaleGuestDimension(uint value) =>
|
|
||||||
_renderResolutionScale == 1.0
|
|
||||||
? value
|
|
||||||
: Math.Max(1u, (uint)Math.Round(value * _renderResolutionScale));
|
|
||||||
|
|
||||||
private static readonly bool _forceFullscreenPipeline =
|
private static readonly bool _forceFullscreenPipeline =
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_FULLSCREEN_PIPELINE") == "1";
|
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_FULLSCREEN_PIPELINE") == "1";
|
||||||
private static readonly bool _forceFullscreenVertex =
|
private static readonly bool _forceFullscreenVertex =
|
||||||
@@ -14745,99 +14594,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
&toPresent);
|
&toPresent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PS5 float VideoOut buffers (A16B16G16R16F flips) hold linear scRGB
|
|
||||||
// light where 1.0 is SDR white; hardware scan-out applies the display
|
|
||||||
// transfer. vkCmdBlitImage converts numerically only, so presenting a
|
|
||||||
// linear-float guest frame into a UNORM swapchain shows near-black
|
|
||||||
// for any dim scene. Encode linear->sRGB by blitting through an sRGB
|
|
||||||
// intermediate (sRGB stores encode), then raw-copying the encoded
|
|
||||||
// bytes into the same-class UNORM swapchain image.
|
|
||||||
private Image _presentEncodeImage;
|
|
||||||
private DeviceMemory _presentEncodeMemory;
|
|
||||||
private Extent2D _presentEncodeExtent;
|
|
||||||
|
|
||||||
private bool TryGetPresentEncodeImage(out Image encodeImage)
|
|
||||||
{
|
|
||||||
encodeImage = default;
|
|
||||||
var encodeFormat = GetSrgbCounterpart(_swapchainFormat);
|
|
||||||
if (encodeFormat == Format.Undefined)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_presentEncodeImage.Handle != 0 &&
|
|
||||||
(_presentEncodeExtent.Width != _extent.Width ||
|
|
||||||
_presentEncodeExtent.Height != _extent.Height))
|
|
||||||
{
|
|
||||||
DestroyPresentEncodeImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_presentEncodeImage.Handle == 0)
|
|
||||||
{
|
|
||||||
var imageInfo = new ImageCreateInfo
|
|
||||||
{
|
|
||||||
SType = StructureType.ImageCreateInfo,
|
|
||||||
ImageType = ImageType.Type2D,
|
|
||||||
Format = encodeFormat,
|
|
||||||
Extent = new Extent3D(_extent.Width, _extent.Height, 1),
|
|
||||||
MipLevels = 1,
|
|
||||||
ArrayLayers = 1,
|
|
||||||
Samples = SampleCountFlags.Count1Bit,
|
|
||||||
Tiling = ImageTiling.Optimal,
|
|
||||||
Usage = ImageUsageFlags.TransferDstBit |
|
|
||||||
ImageUsageFlags.TransferSrcBit,
|
|
||||||
SharingMode = SharingMode.Exclusive,
|
|
||||||
InitialLayout = ImageLayout.Undefined,
|
|
||||||
};
|
|
||||||
Check(
|
|
||||||
_vk.CreateImage(_device, &imageInfo, null, out _presentEncodeImage),
|
|
||||||
"vkCreateImage(present encode)");
|
|
||||||
_vk.GetImageMemoryRequirements(
|
|
||||||
_device,
|
|
||||||
_presentEncodeImage,
|
|
||||||
out var requirements);
|
|
||||||
var allocationInfo = new MemoryAllocateInfo
|
|
||||||
{
|
|
||||||
SType = StructureType.MemoryAllocateInfo,
|
|
||||||
AllocationSize = requirements.Size,
|
|
||||||
MemoryTypeIndex = FindMemoryType(
|
|
||||||
requirements.MemoryTypeBits,
|
|
||||||
MemoryPropertyFlags.DeviceLocalBit),
|
|
||||||
};
|
|
||||||
Check(
|
|
||||||
_vk.AllocateMemory(_device, &allocationInfo, null, out _presentEncodeMemory),
|
|
||||||
"vkAllocateMemory(present encode)");
|
|
||||||
Check(
|
|
||||||
_vk.BindImageMemory(_device, _presentEncodeImage, _presentEncodeMemory, 0),
|
|
||||||
"vkBindImageMemory(present encode)");
|
|
||||||
_presentEncodeExtent = new Extent2D(_extent.Width, _extent.Height);
|
|
||||||
SetDebugName(
|
|
||||||
ObjectType.Image,
|
|
||||||
_presentEncodeImage.Handle,
|
|
||||||
"SharpEmu present sRGB-encode image");
|
|
||||||
}
|
|
||||||
|
|
||||||
encodeImage = _presentEncodeImage;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DestroyPresentEncodeImage()
|
|
||||||
{
|
|
||||||
if (_presentEncodeImage.Handle != 0)
|
|
||||||
{
|
|
||||||
_vk.DestroyImage(_device, _presentEncodeImage, null);
|
|
||||||
_presentEncodeImage = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_presentEncodeMemory.Handle != 0)
|
|
||||||
{
|
|
||||||
_vk.FreeMemory(_device, _presentEncodeMemory, null);
|
|
||||||
_presentEncodeMemory = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
_presentEncodeExtent = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecordGuestImageBlit(
|
private void RecordGuestImageBlit(
|
||||||
uint imageIndex,
|
uint imageIndex,
|
||||||
GuestImageResource source)
|
GuestImageResource source)
|
||||||
@@ -14889,33 +14645,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Image = _swapchainImages[imageIndex],
|
Image = _swapchainImages[imageIndex],
|
||||||
SubresourceRange = ColorSubresourceRange(),
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
};
|
};
|
||||||
// Linear-float flips need a linear->sRGB encode on the way to a
|
var barriers = stackalloc ImageMemoryBarrier[2];
|
||||||
// UNORM swapchain; sRGB (or unknown-counterpart) swapchains keep
|
|
||||||
// the direct blit.
|
|
||||||
var encodeForPresent = false;
|
|
||||||
Image encodeImage = default;
|
|
||||||
if (IsLinearFloatPresentSource(source.Format) &&
|
|
||||||
GetSrgbCounterpart(_swapchainFormat) != Format.Undefined)
|
|
||||||
{
|
|
||||||
encodeForPresent = TryGetPresentEncodeImage(out encodeImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
var encodeToTransferDst = new ImageMemoryBarrier
|
|
||||||
{
|
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
|
||||||
SrcAccessMask = AccessFlags.TransferReadBit,
|
|
||||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
|
||||||
OldLayout = ImageLayout.Undefined,
|
|
||||||
NewLayout = ImageLayout.TransferDstOptimal,
|
|
||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
||||||
Image = _presentEncodeImage,
|
|
||||||
SubresourceRange = ColorSubresourceRange(),
|
|
||||||
};
|
|
||||||
var barriers = stackalloc ImageMemoryBarrier[3];
|
|
||||||
barriers[0] = sourceToTransfer;
|
barriers[0] = sourceToTransfer;
|
||||||
barriers[1] = destinationToTransfer;
|
barriers[1] = destinationToTransfer;
|
||||||
barriers[2] = encodeToTransferDst;
|
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
PipelineStageFlags.AllCommandsBit,
|
PipelineStageFlags.AllCommandsBit,
|
||||||
@@ -14925,7 +14657,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
null,
|
null,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
encodeForPresent ? 3u : 2u,
|
2,
|
||||||
barriers);
|
barriers);
|
||||||
|
|
||||||
var sourceX = 0u;
|
var sourceX = 0u;
|
||||||
@@ -14994,58 +14726,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
source.Image,
|
source.Image,
|
||||||
ImageLayout.TransferSrcOptimal,
|
ImageLayout.TransferSrcOptimal,
|
||||||
encodeForPresent ? encodeImage : _swapchainImages[imageIndex],
|
_swapchainImages[imageIndex],
|
||||||
ImageLayout.TransferDstOptimal,
|
ImageLayout.TransferDstOptimal,
|
||||||
1,
|
1,
|
||||||
®ion,
|
®ion,
|
||||||
isIntegerUpscale ? Filter.Nearest : Filter.Linear);
|
isIntegerUpscale ? Filter.Nearest : Filter.Linear);
|
||||||
|
|
||||||
if (encodeForPresent)
|
|
||||||
{
|
|
||||||
var encodeToTransferSrc = new ImageMemoryBarrier
|
|
||||||
{
|
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
|
||||||
SrcAccessMask = AccessFlags.TransferWriteBit,
|
|
||||||
DstAccessMask = AccessFlags.TransferReadBit,
|
|
||||||
OldLayout = ImageLayout.TransferDstOptimal,
|
|
||||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
|
||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
|
||||||
Image = encodeImage,
|
|
||||||
SubresourceRange = ColorSubresourceRange(),
|
|
||||||
};
|
|
||||||
_vk.CmdPipelineBarrier(
|
|
||||||
_commandBuffer,
|
|
||||||
PipelineStageFlags.TransferBit,
|
|
||||||
PipelineStageFlags.TransferBit,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
1,
|
|
||||||
&encodeToTransferSrc);
|
|
||||||
|
|
||||||
// Raw same-class copy keeps the sRGB-encoded bytes unchanged
|
|
||||||
// while landing them in the UNORM swapchain image.
|
|
||||||
var encodedCopy = new ImageCopy
|
|
||||||
{
|
|
||||||
SrcSubresource = new ImageSubresourceLayers(
|
|
||||||
ImageAspectFlags.ColorBit, 0, 0, 1),
|
|
||||||
DstSubresource = new ImageSubresourceLayers(
|
|
||||||
ImageAspectFlags.ColorBit, 0, 0, 1),
|
|
||||||
Extent = new Extent3D(_extent.Width, _extent.Height, 1),
|
|
||||||
};
|
|
||||||
_vk.CmdCopyImage(
|
|
||||||
_commandBuffer,
|
|
||||||
encodeImage,
|
|
||||||
ImageLayout.TransferSrcOptimal,
|
|
||||||
_swapchainImages[imageIndex],
|
|
||||||
ImageLayout.TransferDstOptimal,
|
|
||||||
1,
|
|
||||||
&encodedCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (traceDestination)
|
if (traceDestination)
|
||||||
{
|
{
|
||||||
var destinationToReadback = new ImageMemoryBarrier
|
var destinationToReadback = new ImageMemoryBarrier
|
||||||
@@ -15310,14 +14996,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private static ImageSubresourceRange ColorSubresourceRange(
|
private static ImageSubresourceRange ColorSubresourceRange(
|
||||||
uint baseMipLevel = 0,
|
uint baseMipLevel = 0,
|
||||||
uint levelCount = 1,
|
uint levelCount = 1) =>
|
||||||
uint layerCount = 1) =>
|
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
AspectMask = ImageAspectFlags.ColorBit,
|
AspectMask = ImageAspectFlags.ColorBit,
|
||||||
BaseMipLevel = baseMipLevel,
|
BaseMipLevel = baseMipLevel,
|
||||||
LevelCount = levelCount,
|
LevelCount = levelCount,
|
||||||
LayerCount = layerCount,
|
LayerCount = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static byte[] ScaleBgra(byte[] source, uint sourceWidth, uint sourceHeight, uint width, uint height)
|
private static byte[] ScaleBgra(byte[] source, uint sourceWidth, uint sourceHeight, uint width, uint height)
|
||||||
@@ -15505,7 +15190,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Clear();
|
_availableGuestImages.Clear();
|
||||||
_cpuBackedUploadGenerations.Clear();
|
|
||||||
_lastOrderedGuestFlipVersions.Clear();
|
_lastOrderedGuestFlipVersions.Clear();
|
||||||
}
|
}
|
||||||
DestroySwapchainResources();
|
DestroySwapchainResources();
|
||||||
@@ -15582,7 +15266,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void DestroySwapchainResources()
|
private void DestroySwapchainResources()
|
||||||
{
|
{
|
||||||
DestroyPresentEncodeImage();
|
|
||||||
if (_stagingBuffer.Handle != 0)
|
if (_stagingBuffer.Handle != 0)
|
||||||
{
|
{
|
||||||
_vk.DestroyBuffer(_device, _stagingBuffer, null);
|
_vk.DestroyBuffer(_device, _stagingBuffer, null);
|
||||||
|
|||||||
@@ -959,15 +959,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
case "VFmaMixF32":
|
|
||||||
case "VFmaMixloF16":
|
|
||||||
case "VFmaMixhiF16":
|
|
||||||
if (!TryEmitFmaMix(instruction, destination, out result, out error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||||
@@ -1024,6 +1015,12 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (control.Clamp)
|
||||||
|
{
|
||||||
|
error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
|
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
|
||||||
for (var index = 0; index < sourceCount; index++)
|
for (var index = 0; index < sourceCount; index++)
|
||||||
{
|
{
|
||||||
@@ -1042,112 +1039,7 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16 (VOP3P opcodes 0x20 / 0x21 /
|
|
||||||
// 0x22). Unlike the packed v_pk_* ops these compute a single f32
|
|
||||||
// fma(a, b, c): each of the three sources is *independently* read as
|
|
||||||
// either a full f32 register/constant or one f16 half widened to f32,
|
|
||||||
// selected per operand by op_sel_hi (read as f16 when set) and op_sel
|
|
||||||
// (which half feeds the f32). For the mix ops the VOP3P neg_hi field is
|
|
||||||
// the absolute-value modifier and neg negates, applied abs-then-neg to
|
|
||||||
// match the hardware and shadPS4's GetSrcMix. _MIXLO / _MIXHI round the
|
|
||||||
// f32 result back to f16 and write it into the low / high 16 bits of
|
|
||||||
// vdst, leaving the other half intact.
|
|
||||||
private bool TryEmitFmaMix(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
uint destination,
|
|
||||||
out uint result,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
result = 0;
|
|
||||||
error = string.Empty;
|
|
||||||
if (instruction.Control is not Gen5Vop3pControl control)
|
|
||||||
{
|
|
||||||
error = $"missing vop3p control for {instruction.Opcode}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var product = Bitcast(
|
|
||||||
_uintType,
|
|
||||||
Ext(
|
|
||||||
50,
|
|
||||||
_floatType,
|
|
||||||
EmitFmaMixOperand(instruction, control, 0),
|
|
||||||
EmitFmaMixOperand(instruction, control, 1),
|
|
||||||
EmitFmaMixOperand(instruction, control, 2)));
|
|
||||||
if (control.Clamp)
|
|
||||||
{
|
|
||||||
product = EmitClampToUnitInterval(product);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.Opcode == "VFmaMixF32")
|
|
||||||
{
|
|
||||||
result = product;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _MIXLO / _MIXHI: narrow to f16 and merge into one half of vdst.
|
|
||||||
var half = EmitFloatToHalf(product);
|
|
||||||
var existing = LoadV(destination);
|
|
||||||
result = instruction.Opcode == "VFmaMixloF16"
|
|
||||||
? BitwiseOr(BitwiseAnd(existing, UInt(0xFFFF_0000)), half)
|
|
||||||
: BitwiseOr(
|
|
||||||
BitwiseAnd(existing, UInt(0x0000_FFFF)),
|
|
||||||
ShiftLeftLogical(half, UInt(16)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads one V_FMA_MIX source as an f32. op_sel_hi selects whether a
|
|
||||||
// register operand is taken as an f16 (the half picked by op_sel, widened
|
|
||||||
// exactly to f32) or as a full f32; inline constants are always f32. The
|
|
||||||
// per-operand neg_hi bit takes the absolute value and neg negates, in that
|
|
||||||
// order (abs-then-neg), reusing the VOP3P modifier fields the way the mix
|
|
||||||
// ops define them rather than the packed low/high-lane meaning.
|
|
||||||
private uint EmitFmaMixOperand(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
Gen5Vop3pControl control,
|
|
||||||
int index)
|
|
||||||
{
|
|
||||||
var source = instruction.Sources[index];
|
|
||||||
var readAsHalf =
|
|
||||||
((control.OpSelHiMask >> index) & 1) != 0 &&
|
|
||||||
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister;
|
|
||||||
|
|
||||||
uint value;
|
|
||||||
if (readAsHalf)
|
|
||||||
{
|
|
||||||
var raw = GetRawSource(instruction, index);
|
|
||||||
var half = ((control.OpSelMask >> index) & 1) != 0
|
|
||||||
? ShiftRightLogical(raw, UInt(16))
|
|
||||||
: raw;
|
|
||||||
value = Bitcast(_floatType, EmitHalfToFloat(half));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = GetFloatSource(instruction, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (((control.NegHiMask >> index) & 1) != 0)
|
|
||||||
{
|
|
||||||
value = Ext(4, _floatType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (((control.NegLoMask >> index) & 1) != 0)
|
|
||||||
{
|
|
||||||
value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
||||||
// The op runs in f32 and its result is narrowed back to f16 exactly (see
|
|
||||||
// EmitFloatToHalf). When the clamp modifier is set the pre-narrowing f32
|
|
||||||
// value is saturated to [0, 1] first; because 0.0 and 1.0 are exact in both
|
|
||||||
// f32 and f16 and the clamp is monotonic, clamping before the narrowing
|
|
||||||
// gives the same f16 the hardware produces by clamping the f16 result. For
|
|
||||||
// the fused multiply-add the pre-narrowing value is the round-to-odd f32
|
|
||||||
// from EmitPackedF16FusedMultiplyAdd, and round-to-odd preserves that
|
|
||||||
// equivalence through the final round-to-nearest-even.
|
|
||||||
private uint EmitPackedF16Lane(
|
private uint EmitPackedF16Lane(
|
||||||
Gen5ShaderInstruction instruction,
|
Gen5ShaderInstruction instruction,
|
||||||
Gen5Vop3pControl control,
|
Gen5Vop3pControl control,
|
||||||
@@ -1155,44 +1047,21 @@ public static partial class Gen5SpirvTranslator
|
|||||||
{
|
{
|
||||||
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
||||||
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
|
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
|
||||||
uint value;
|
|
||||||
if (instruction.Opcode == "VPkFmaF16")
|
if (instruction.Opcode == "VPkFmaF16")
|
||||||
{
|
{
|
||||||
var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
|
var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
|
||||||
value = EmitPackedF16FusedMultiplyAdd(left, right, addend);
|
return EmitFloatToHalf(EmitPackedF16FusedMultiplyAdd(left, right, addend));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
var value = instruction.Opcode switch
|
||||||
{
|
{
|
||||||
value = Bitcast(_uintType, instruction.Opcode switch
|
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||||
{
|
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
|
||||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
|
||||||
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
|
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
|
||||||
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
|
_ => left,
|
||||||
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
|
};
|
||||||
_ => left,
|
return EmitFloatToHalf(Bitcast(_uintType, value));
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (control.Clamp)
|
|
||||||
{
|
|
||||||
value = EmitClampToUnitInterval(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return EmitFloatToHalf(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saturates an f32 bit pattern to [0, 1] the way the VOP3P clamp modifier
|
|
||||||
// does: below 0 (and NaN, since the ordered compare is false for it) becomes
|
|
||||||
// 0, above 1 becomes 1. Ordered compares match the hardware's NaN-to-zero
|
|
||||||
// behaviour without a separate IsNan test.
|
|
||||||
private uint EmitClampToUnitInterval(uint valueBits)
|
|
||||||
{
|
|
||||||
var value = Bitcast(_floatType, valueBits);
|
|
||||||
var aboveZero = _module.AddInstruction(SpirvOp.FOrdGreaterThan, _boolType, value, Float(0));
|
|
||||||
var lowerBounded = _module.AddInstruction(SpirvOp.Select, _floatType, aboveZero, value, Float(0));
|
|
||||||
var belowOne = _module.AddInstruction(SpirvOp.FOrdLessThan, _boolType, lowerBounded, Float(1));
|
|
||||||
var clamped = _module.AddInstruction(SpirvOp.Select, _floatType, belowOne, lowerBounded, Float(1));
|
|
||||||
return Bitcast(_uintType, clamped);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fused f16 multiply-add with a single rounding, emulated in f32 without the
|
// Fused f16 multiply-add with a single rounding, emulated in f32 without the
|
||||||
|
|||||||
@@ -313,8 +313,7 @@ public static partial class Gen5SpirvTranslator
|
|||||||
uint ComponentType,
|
uint ComponentType,
|
||||||
uint VectorType,
|
uint VectorType,
|
||||||
ImageComponentKind ComponentKind,
|
ImageComponentKind ComponentKind,
|
||||||
bool IsStorage,
|
bool IsStorage);
|
||||||
bool Arrayed);
|
|
||||||
|
|
||||||
private readonly record struct SpirvVertexInput(
|
private readonly record struct SpirvVertexInput(
|
||||||
uint Variable,
|
uint Variable,
|
||||||
@@ -1001,13 +1000,11 @@ public static partial class Gen5SpirvTranslator
|
|||||||
SpirvCapability.StorageImageExtendedFormats);
|
SpirvCapability.StorageImageExtendedFormats);
|
||||||
}
|
}
|
||||||
|
|
||||||
var isArrayed = !isStorage &&
|
|
||||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding);
|
|
||||||
var imageType = _module.TypeImage(
|
var imageType = _module.TypeImage(
|
||||||
componentType,
|
componentType,
|
||||||
SpirvImageDim.Dim2D,
|
SpirvImageDim.Dim2D,
|
||||||
depth: false,
|
depth: false,
|
||||||
arrayed: isArrayed,
|
arrayed: false,
|
||||||
multisampled: false,
|
multisampled: false,
|
||||||
sampled: isStorage ? 2u : 1u,
|
sampled: isStorage ? 2u : 1u,
|
||||||
isStorage ? format : SpirvImageFormat.Unknown);
|
isStorage ? format : SpirvImageFormat.Unknown);
|
||||||
@@ -1034,8 +1031,7 @@ public static partial class Gen5SpirvTranslator
|
|||||||
componentType,
|
componentType,
|
||||||
_module.TypeVector(componentType, 4),
|
_module.TypeVector(componentType, 4),
|
||||||
componentKind,
|
componentKind,
|
||||||
isStorage,
|
isStorage));
|
||||||
isArrayed));
|
|
||||||
_interfaces.Add(variable);
|
_interfaces.Add(variable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3533,16 +3529,12 @@ public static partial class Gen5SpirvTranslator
|
|||||||
addressCursor += 4;
|
addressCursor += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
var coordinates = resource.Arrayed
|
var coordinates = BuildFloatCoordinates(image, addressCursor);
|
||||||
? BuildFloatArrayCoordinates(image, addressCursor)
|
|
||||||
: BuildFloatCoordinates(image, addressCursor);
|
|
||||||
var explicitLod = hasGradients || hasZeroLod || hasLod;
|
var explicitLod = hasGradients || hasZeroLod || hasLod;
|
||||||
var lod = hasZeroLod
|
var lod = hasZeroLod
|
||||||
? Float(0)
|
? Float(0)
|
||||||
: hasLod
|
: hasLod
|
||||||
? LoadImageFloatAddress(
|
? LoadImageFloatAddress(image, addressCursor + 2)
|
||||||
image,
|
|
||||||
addressCursor + (resource.Arrayed ? 3 : 2))
|
|
||||||
: lodOrBias;
|
: lodOrBias;
|
||||||
if (hasOffset)
|
if (hasOffset)
|
||||||
{
|
{
|
||||||
@@ -3627,9 +3619,7 @@ public static partial class Gen5SpirvTranslator
|
|||||||
addressCursor += ImageFullAddressSlots(image);
|
addressCursor += ImageFullAddressSlots(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
var coordinates = resource.Arrayed
|
var coordinates = BuildFloatCoordinates(image, addressCursor);
|
||||||
? BuildFloatArrayCoordinates(image, addressCursor)
|
|
||||||
: BuildFloatCoordinates(image, addressCursor);
|
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
@@ -3833,19 +3823,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
y);
|
y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint BuildFloatArrayCoordinates(Gen5ImageControl image, int start)
|
|
||||||
{
|
|
||||||
var x = LoadImageFloatAddress(image, start);
|
|
||||||
var y = LoadImageFloatAddress(image, start + 1);
|
|
||||||
var slice = LoadImageFloatAddress(image, start + 2);
|
|
||||||
return _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
_vec3Type,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
slice);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int ImageAddressRegister(
|
private static int ImageAddressRegister(
|
||||||
Gen5ImageControl image,
|
Gen5ImageControl image,
|
||||||
int component) => image.A16 ? component / 2 : component;
|
int component) => image.A16 ? component / 2 : component;
|
||||||
@@ -4163,20 +4140,9 @@ public static partial class Gen5SpirvTranslator
|
|||||||
signedLod);
|
signedLod);
|
||||||
var size = _module.AddInstruction(
|
var size = _module.AddInstruction(
|
||||||
SpirvOp.ImageQuerySizeLod,
|
SpirvOp.ImageQuerySizeLod,
|
||||||
resource.Arrayed ? _module.TypeVector(_intType, 3) : ivec2,
|
ivec2,
|
||||||
image,
|
image,
|
||||||
clampedLod);
|
clampedLod);
|
||||||
if (resource.Arrayed)
|
|
||||||
{
|
|
||||||
size = _module.AddInstruction(
|
|
||||||
SpirvOp.VectorShuffle,
|
|
||||||
ivec2,
|
|
||||||
size,
|
|
||||||
size,
|
|
||||||
0u,
|
|
||||||
1u);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sizeFloat = _module.AddInstruction(
|
var sizeFloat = _module.AddInstruction(
|
||||||
SpirvOp.ConvertSToF,
|
SpirvOp.ConvertSToF,
|
||||||
_vec2Type,
|
_vec2Type,
|
||||||
@@ -4190,34 +4156,11 @@ public static partial class Gen5SpirvTranslator
|
|||||||
_vec2Type,
|
_vec2Type,
|
||||||
offsetFloat,
|
offsetFloat,
|
||||||
sizeFloat);
|
sizeFloat);
|
||||||
if (!resource.Arrayed)
|
|
||||||
{
|
|
||||||
return _module.AddInstruction(
|
|
||||||
SpirvOp.FAdd,
|
|
||||||
_vec2Type,
|
|
||||||
coordinates,
|
|
||||||
normalizedOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
var offsetVec3 = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
_vec3Type,
|
|
||||||
_module.AddInstruction(
|
|
||||||
SpirvOp.CompositeExtract,
|
|
||||||
_floatType,
|
|
||||||
normalizedOffset,
|
|
||||||
0u),
|
|
||||||
_module.AddInstruction(
|
|
||||||
SpirvOp.CompositeExtract,
|
|
||||||
_floatType,
|
|
||||||
normalizedOffset,
|
|
||||||
1u),
|
|
||||||
Float(0));
|
|
||||||
return _module.AddInstruction(
|
return _module.AddInstruction(
|
||||||
SpirvOp.FAdd,
|
SpirvOp.FAdd,
|
||||||
_vec3Type,
|
_vec2Type,
|
||||||
coordinates,
|
coordinates,
|
||||||
offsetVec3);
|
normalizedOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryEmitExport(
|
private bool TryEmitExport(
|
||||||
@@ -5347,20 +5290,10 @@ public static partial class Gen5SpirvTranslator
|
|||||||
UInt(0x108));
|
UInt(0x108));
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wave-mask SGPR (VCC/EXEC) consumed as a per-lane predicate — the
|
|
||||||
// condition of VCndmask, a VCC/EXEC branch, or the derived _vcc/_exec
|
|
||||||
// bool — must be tested at the CURRENT lane's bit, exactly as the
|
|
||||||
// hardware does, not as "the 64-bit value is non-zero". The two coincide
|
|
||||||
// for comparison results (only the lane's own bit is ever set), so the
|
|
||||||
// single-lane path historically used a cheaper whole-word non-zero test.
|
|
||||||
// But bitwise-complement wave-mask idioms (S_NOT/S_ORN2/S_ANDN2/S_NAND/
|
|
||||||
// S_NOR on a 64-bit mask) set the unused upper 63 bits; a whole-word test
|
|
||||||
// then reports "lane active" even when this lane's bit is clear. Unity's
|
|
||||||
// PostProcessing NaN killer does exactly this (`anyNaN | ~allFinite`),
|
|
||||||
// which made every valid pixel read as NaN and get replaced with 0 —
|
|
||||||
// zeroing the whole scene before tonemap. Extract the lane bit always.
|
|
||||||
private uint IsWaveMaskActive(uint mask) =>
|
private uint IsWaveMaskActive(uint mask) =>
|
||||||
IsCurrentLaneSet(mask);
|
_subgroupInvocationIdInput == 0
|
||||||
|
? IsNotZero64(mask)
|
||||||
|
: IsCurrentLaneSet(mask);
|
||||||
|
|
||||||
private uint IsCurrentLaneSet(uint mask) =>
|
private uint IsCurrentLaneSet(uint mask) =>
|
||||||
IsNotZero64(
|
IsNotZero64(
|
||||||
|
|||||||
@@ -1192,10 +1192,8 @@ public static class Gen5ShaderTranslator
|
|||||||
|
|
||||||
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
||||||
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
||||||
// The mix ops (0x20/0x21/0x22) are V_MAD_MIX_* on gfx9 and V_FMA_MIX_*
|
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
|
||||||
// (fused) on the gfx10 the PS5 targets; both share these opcodes. Any
|
// fail loudly at emission rather than being silently mis-emitted.
|
||||||
// remaining packed opcode (integer, ...) stays opaque here and fails
|
|
||||||
// loudly at emission rather than being silently mis-emitted.
|
|
||||||
name = opcode switch
|
name = opcode switch
|
||||||
{
|
{
|
||||||
0x0E => "VPkFmaF16",
|
0x0E => "VPkFmaF16",
|
||||||
@@ -1203,9 +1201,6 @@ public static class Gen5ShaderTranslator
|
|||||||
0x10 => "VPkMulF16",
|
0x10 => "VPkMulF16",
|
||||||
0x11 => "VPkMinF16",
|
0x11 => "VPkMinF16",
|
||||||
0x12 => "VPkMaxF16",
|
0x12 => "VPkMaxF16",
|
||||||
0x20 => "VFmaMixF32",
|
|
||||||
0x21 => "VFmaMixloF16",
|
|
||||||
0x22 => "VFmaMixhiF16",
|
|
||||||
_ => $"Vop3pRaw{opcode:X2}",
|
_ => $"Vop3pRaw{opcode:X2}",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1612,11 +1607,6 @@ public static class Gen5ShaderTranslator
|
|||||||
binding.ResourceDescriptor.SequenceEqual(candidate.ResourceDescriptor));
|
binding.ResourceDescriptor.SequenceEqual(candidate.ResourceDescriptor));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsArrayedImageBinding(Gen5ImageBinding binding) =>
|
|
||||||
binding.Control.IsArray &&
|
|
||||||
(binding.Opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
|
||||||
binding.Opcode.StartsWith("ImageGather4", StringComparison.Ordinal));
|
|
||||||
|
|
||||||
public static bool IsDataShareAtomic(string name) => name switch
|
public static bool IsDataShareAtomic(string name) => name switch
|
||||||
{
|
{
|
||||||
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
|
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
// 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;
|
|
||||||
|
|
||||||
// Regression tests for the VOP3P mix ops V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16
|
|
||||||
// (opcodes 0x20 / 0x21 / 0x22). The decoder leaves any unlowered VOP3P opcode
|
|
||||||
// opaque (Vop3pRaw20/21/22); before these were lowered they hit the vector-ALU
|
|
||||||
// switch default and failed emission ("unsupported vector opcode"), which drops
|
|
||||||
// the whole shader. Unity HDR / tone-mapping / auto-exposure shaders use
|
|
||||||
// V_FMA_MIX_F32 and so failed to translate entirely.
|
|
||||||
//
|
|
||||||
// Each mix op computes a single f32 fma(a, b, c) where every source is read
|
|
||||||
// *independently* as either a full f32 register or one f16 half widened to f32,
|
|
||||||
// selected per operand by op_sel_hi (f16 when set) and op_sel (which half). The
|
|
||||||
// mix ops also repurpose the VOP3P neg_hi field as an absolute-value modifier.
|
|
||||||
public sealed class Gen5FmaMixSpirvTests
|
|
||||||
{
|
|
||||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
|
||||||
|
|
||||||
// GLSL.std.450 extended-instruction numbers used by the lowering.
|
|
||||||
private const uint GlslFma = 50;
|
|
||||||
private const uint GlslFAbs = 4;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FmaMixF32_TranslatesToFmaAndDoesNotDropShader()
|
|
||||||
{
|
|
||||||
// V_FMA_MIX_F32 v3, v0, v1, v2
|
|
||||||
// op_sel_hi = 0b011 -> src0/src1 read as f16, src2 as full f32
|
|
||||||
// op_sel = 0b010 -> src1 takes its high f16 half (src0 low half)
|
|
||||||
// neg_hi = 0b001 -> abs(src0)
|
|
||||||
// neg = 0b100 -> -src2
|
|
||||||
// Reaching TryCompileComputeShader == true already proves the shader is no
|
|
||||||
// longer dropped at the VOP3P default error path.
|
|
||||||
var spirv = Compile([0xCC201103u, 0x9C0A0300u]);
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
ContainsExtInst(spirv, GlslFma),
|
|
||||||
"V_FMA_MIX_F32 must lower to a GLSL.std.450 Fma");
|
|
||||||
Assert.True(
|
|
||||||
ContainsExtInst(spirv, GlslFAbs),
|
|
||||||
"the neg_hi modifier on a mix source must lower to an FAbs (abs-then-neg)");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FmaMixLoF16_TranslatesWithoutDroppingShader()
|
|
||||||
{
|
|
||||||
// V_FMA_MIXLO_F16 v3, v0, v1, v2 with op_sel_hi = 0b111 (all sources read
|
|
||||||
// as f16 low halves). The f32 fma result is narrowed to f16 and merged
|
|
||||||
// into the low 16 bits of vdst; the fma itself is still emitted.
|
|
||||||
var spirv = Compile([0xCC214003u, 0x1C0A0300u]);
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
ContainsExtInst(spirv, GlslFma),
|
|
||||||
"V_FMA_MIXLO_F16 must still lower its multiply-add to a GLSL.std.450 Fma");
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when the module contains an OpExtInst selecting the given GLSL.std.450
|
|
||||||
// instruction number.
|
|
||||||
private static bool ContainsExtInst(byte[] spirv, uint instruction)
|
|
||||||
{
|
|
||||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
|
||||||
{
|
|
||||||
// OpExtInst = 12: (opcode, resultType, resultId, set, instruction, ...).
|
|
||||||
if (op != 12 || wordCount < 5)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReadWord(spirv, offset + 16) == instruction)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
|
|
||||||
byte[] spirv)
|
|
||||||
{
|
|
||||||
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
|
||||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
|
||||||
{
|
|
||||||
var word = ReadWord(spirv, offset);
|
|
||||||
var wordCount = (int)(word >> 16);
|
|
||||||
if (wordCount <= 0)
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return ((ushort)word, wordCount, offset);
|
|
||||||
offset += wordCount * sizeof(uint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint ReadWord(byte[] spirv, int offset) =>
|
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
|
||||||
|
|
||||||
private static byte[] Compile(uint[] programWords)
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
|
||||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
||||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
|
||||||
var shaderRegisters = new Dictionary<uint, uint>
|
|
||||||
{
|
|
||||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 shader.Spirv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
// 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;
|
|
||||||
|
|
||||||
// Regression tests for how a VCC/EXEC wave mask consumed as a per-lane predicate
|
|
||||||
// is lowered to SPIR-V. A wave mask must be tested at the current lane's bit
|
|
||||||
// (mask & lane_bit) — exactly as the hardware evaluates the VCndmask condition or
|
|
||||||
// a VCC/EXEC branch — not with a whole-word "the 64-bit value is non-zero" test.
|
|
||||||
//
|
|
||||||
// The two agree for comparison results (only the lane's own bit is ever set), but
|
|
||||||
// diverge for the bitwise-complement wave-mask idioms (S_NOT / S_ORN2 / S_ANDN2 /
|
|
||||||
// S_NAND / S_NOR), which set the unused upper 63 bits. A whole-word test then
|
|
||||||
// reports the lane active even when its bit is clear. Unity's PostProcessing NaN
|
|
||||||
// killer combines its channels as `anyNaN | ~allFinite` (S_ORN2_B64); under the
|
|
||||||
// whole-word test every valid pixel read as NaN and was replaced with 0, zeroing
|
|
||||||
// the whole HDR scene before tone-mapping.
|
|
||||||
public sealed class Gen5WaveMaskSpirvTests
|
|
||||||
{
|
|
||||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void WaveMaskPredicate_IsTestedAtCurrentLaneBit()
|
|
||||||
{
|
|
||||||
// V_CMP_EQ_F32 vcc, v0, v1 writes VCC at run time, which re-materialises
|
|
||||||
// the per-lane _vcc predicate from the wave mask via IsWaveMaskActive.
|
|
||||||
var spirv = Compile([0x7C04_0300u]);
|
|
||||||
|
|
||||||
// The lane's bit in single-lane emulation is the 64-bit constant 1, so the
|
|
||||||
// predicate is `(mask & 1) != 0`. The whole-word bug emitted `mask != 0`
|
|
||||||
// with no such mask. Require the lane-bit AND to be present.
|
|
||||||
Assert.True(
|
|
||||||
ContainsLaneBitMaskedWaveTest(spirv),
|
|
||||||
"wave-mask predicate must be tested at the current lane bit "
|
|
||||||
+ "(mask & lane_bit), not as a whole-word non-zero test");
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when the module contains an OpBitwiseAnd whose operand is a 64-bit
|
|
||||||
// constant of value 1 — the current-lane bit that IsCurrentLaneSet masks the
|
|
||||||
// wave mask with before the non-zero test.
|
|
||||||
private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv)
|
|
||||||
{
|
|
||||||
var laneBitConstIds = new HashSet<uint>();
|
|
||||||
|
|
||||||
// Pass 1: collect 64-bit OpConstant result-ids whose value is 1.
|
|
||||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
|
||||||
{
|
|
||||||
// OpConstant = 43; a 64-bit constant occupies 5 words
|
|
||||||
// (opcode, resultType, resultId, valueLow, valueHigh).
|
|
||||||
if (op != 43 || wordCount != 5)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resultId = ReadWord(spirv, offset + 8);
|
|
||||||
var low = ReadWord(spirv, offset + 12);
|
|
||||||
var high = ReadWord(spirv, offset + 16);
|
|
||||||
if (low == 1 && high == 0)
|
|
||||||
{
|
|
||||||
laneBitConstIds.Add(resultId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: look for an OpBitwiseAnd that consumes one of those constants.
|
|
||||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
|
||||||
{
|
|
||||||
// OpBitwiseAnd = 199 (opcode, resultType, resultId, operand0, operand1).
|
|
||||||
if (op != 199 || wordCount != 5)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var operand0 = ReadWord(spirv, offset + 12);
|
|
||||||
var operand1 = ReadWord(spirv, offset + 16);
|
|
||||||
if (laneBitConstIds.Contains(operand0) || laneBitConstIds.Contains(operand1))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
|
|
||||||
byte[] spirv)
|
|
||||||
{
|
|
||||||
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
|
||||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
|
||||||
{
|
|
||||||
var word = ReadWord(spirv, offset);
|
|
||||||
var wordCount = (int)(word >> 16);
|
|
||||||
if (wordCount <= 0)
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return ((ushort)word, wordCount, offset);
|
|
||||||
offset += wordCount * sizeof(uint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint ReadWord(byte[] spirv, int offset) =>
|
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
|
||||||
|
|
||||||
private static byte[] Compile(uint[] programWords)
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
|
||||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
||||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
|
||||||
var shaderRegisters = new Dictionary<uint, uint>
|
|
||||||
{
|
|
||||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 shader.Spirv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -90,133 +90,6 @@ public sealed class AprStreamingContractTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong pathListAddress = memoryBase + 0x100;
|
|
||||||
const ulong pathAddress = memoryBase + 0x200;
|
|
||||||
const ulong idsAddress = memoryBase + 0x800;
|
|
||||||
const ulong sizesAddress = memoryBase + 0x880;
|
|
||||||
const ulong errorIndexAddress = memoryBase + 0x8F0;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
var missingHostPath = Path.Combine(
|
|
||||||
Path.GetTempPath(),
|
|
||||||
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
|
|
||||||
memory.WriteCString(pathAddress, missingHostPath);
|
|
||||||
WriteUInt64(memory, pathListAddress, pathAddress);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = pathListAddress;
|
|
||||||
context[CpuRegister.Rsi] = 1;
|
|
||||||
context[CpuRegister.Rdx] = idsAddress;
|
|
||||||
context[CpuRegister.Rcx] = sizesAddress;
|
|
||||||
context[CpuRegister.R8] = errorIndexAddress;
|
|
||||||
|
|
||||||
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
|
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
||||||
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress));
|
|
||||||
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress));
|
|
||||||
Assert.Equal(0u, ReadUInt32(memory, errorIndexAddress));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveFilepathsToIdsAndFileSizes_InvalidErrorIndex_ReturnsMemoryFault()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong pathListAddress = memoryBase + 0x100;
|
|
||||||
const ulong pathAddress = memoryBase + 0x200;
|
|
||||||
const ulong idsAddress = memoryBase + 0x800;
|
|
||||||
const ulong sizesAddress = memoryBase + 0x880;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
var missingHostPath = Path.Combine(
|
|
||||||
Path.GetTempPath(),
|
|
||||||
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
|
|
||||||
memory.WriteCString(pathAddress, missingHostPath);
|
|
||||||
WriteUInt64(memory, pathListAddress, pathAddress);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = pathListAddress;
|
|
||||||
context[CpuRegister.Rsi] = 1;
|
|
||||||
context[CpuRegister.Rdx] = idsAddress;
|
|
||||||
context[CpuRegister.Rcx] = sizesAddress;
|
|
||||||
context[CpuRegister.R8] = memoryBase + 0x5000;
|
|
||||||
|
|
||||||
Assert.Equal(
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
|
||||||
KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveFilepathsToIdsAndFileSizes_MissingMidBatch_StopsAtFailingEntry()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong pathListAddress = memoryBase + 0x100;
|
|
||||||
const ulong idsAddress = memoryBase + 0x800;
|
|
||||||
const ulong sizesAddress = memoryBase + 0x880;
|
|
||||||
const ulong errorIndexAddress = memoryBase + 0x8F0;
|
|
||||||
byte[] fileContents = [1, 2, 3, 4, 5];
|
|
||||||
var hostPath = Path.GetTempFileName();
|
|
||||||
var missingHostPath = Path.Combine(
|
|
||||||
Path.GetTempPath(),
|
|
||||||
$"sharpemu-apr-missing-{Guid.NewGuid():N}.bin");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.WriteAllBytes(hostPath, fileContents);
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x4000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
memory.WriteCString(memoryBase + 0x200, hostPath);
|
|
||||||
memory.WriteCString(memoryBase + 0x400, missingHostPath);
|
|
||||||
memory.WriteCString(memoryBase + 0x600, hostPath);
|
|
||||||
WriteUInt64(memory, pathListAddress, memoryBase + 0x200);
|
|
||||||
WriteUInt64(memory, pathListAddress + 8, memoryBase + 0x400);
|
|
||||||
WriteUInt64(memory, pathListAddress + 16, memoryBase + 0x600);
|
|
||||||
WriteUInt32(memory, idsAddress + 8, 0x1234_5678); // sentinel: entry 2 untouched
|
|
||||||
WriteUInt64(memory, sizesAddress + 16, 0xDEAD);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = pathListAddress;
|
|
||||||
context[CpuRegister.Rsi] = 3;
|
|
||||||
context[CpuRegister.Rdx] = idsAddress;
|
|
||||||
context[CpuRegister.Rcx] = sizesAddress;
|
|
||||||
context[CpuRegister.R8] = errorIndexAddress;
|
|
||||||
|
|
||||||
Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context));
|
|
||||||
Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress));
|
|
||||||
Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress));
|
|
||||||
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress + 4));
|
|
||||||
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress + 8));
|
|
||||||
Assert.Equal(1u, ReadUInt32(memory, errorIndexAddress));
|
|
||||||
Assert.Equal(0x1234_5678u, ReadUInt32(memory, idsAddress + 8));
|
|
||||||
Assert.Equal(0xDEADul, ReadUInt64(memory, sizesAddress + 16));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
File.Delete(hostPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
|
||||||
Assert.True(memory.TryRead(address, bytes));
|
|
||||||
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
|
||||||
Assert.True(memory.TryRead(address, bytes));
|
|
||||||
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
|
||||||
Assert.True(memory.TryWrite(address, bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||||
|
|||||||
@@ -40,65 +40,6 @@ public sealed class AvPlayerPathTests : IDisposable
|
|||||||
AssertPathIsInsideApp0(resolved);
|
AssertPathIsInsideApp0(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UnrealRelativeRawPathAnchorsAtApp0AndResolvesMedia()
|
|
||||||
{
|
|
||||||
var mediaPath = CreateFile("SampleProject/Content/Movies/Startup.mp4");
|
|
||||||
|
|
||||||
var resolved = AvPlayerExports.ResolveGuestPath(
|
|
||||||
"../../../SampleProject/Content/Movies/Startup.mp4");
|
|
||||||
|
|
||||||
Assert.NotNull(resolved);
|
|
||||||
Assert.Equal(File.ReadAllBytes(mediaPath), File.ReadAllBytes(resolved));
|
|
||||||
AssertPathIsInsideApp0(resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UnrealRelativeRawPathCannotEscapeApp0()
|
|
||||||
{
|
|
||||||
var outsidePath = Path.Combine(_tempRoot, "outside.mp4");
|
|
||||||
File.WriteAllBytes(outsidePath, [0x7F]);
|
|
||||||
CreateFile("outside.mp4");
|
|
||||||
|
|
||||||
Assert.Null(AvPlayerExports.ResolveGuestPath("../../../outside.mp4"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CurrentDirectoryRawPathResolvesInsideApp0()
|
|
||||||
{
|
|
||||||
var mediaPath = CreateFile("Movies/Intro.mp4");
|
|
||||||
|
|
||||||
var resolved = AvPlayerExports.ResolveGuestPath("./Movies/Intro.mp4");
|
|
||||||
|
|
||||||
Assert.NotNull(resolved);
|
|
||||||
Assert.Equal(Path.GetFullPath(mediaPath), resolved);
|
|
||||||
AssertPathIsInsideApp0(resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(false, "ffmpeg", "ffprobe")]
|
|
||||||
[InlineData(true, "ffmpeg.exe", "ffprobe.exe")]
|
|
||||||
public void MediaToolLookupUsesPlatformNames(
|
|
||||||
bool isWindows,
|
|
||||||
string ffmpegName,
|
|
||||||
string ffprobeName)
|
|
||||||
{
|
|
||||||
var toolDirectory = Path.Combine(_tempRoot, "Media Tools");
|
|
||||||
Directory.CreateDirectory(toolDirectory);
|
|
||||||
var ffmpeg = Path.Combine(toolDirectory, ffmpegName);
|
|
||||||
File.WriteAllBytes(ffmpeg, []);
|
|
||||||
|
|
||||||
var resolved = AvPlayerExports.FindFfmpeg(
|
|
||||||
configured: null,
|
|
||||||
searchPath: $"\"{toolDirectory}\"",
|
|
||||||
isWindows);
|
|
||||||
|
|
||||||
Assert.Equal(ffmpeg, resolved);
|
|
||||||
Assert.Equal(
|
|
||||||
Path.Combine(toolDirectory, ffprobeName),
|
|
||||||
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RelativeFileUriCannotEscapeApp0()
|
public void RelativeFileUriCannotEscapeApp0()
|
||||||
{
|
{
|
||||||
@@ -202,14 +143,12 @@ public sealed class AvPlayerPathTests : IDisposable
|
|||||||
|
|
||||||
private void AssertPathIsInsideApp0(string resolved)
|
private void AssertPathIsInsideApp0(string resolved)
|
||||||
{
|
{
|
||||||
var relative = Path.GetRelativePath(
|
var rootWithSeparator =
|
||||||
Path.GetFullPath(_app0Root),
|
Path.TrimEndingDirectorySeparator(Path.GetFullPath(_app0Root)) +
|
||||||
Path.GetFullPath(resolved));
|
Path.DirectorySeparatorChar;
|
||||||
Assert.False(Path.IsPathFullyQualified(relative));
|
Assert.StartsWith(
|
||||||
Assert.NotEqual("..", relative);
|
rootWithSeparator,
|
||||||
Assert.False(
|
Path.GetFullPath(resolved),
|
||||||
relative.StartsWith(
|
StringComparison.OrdinalIgnoreCase);
|
||||||
".." + Path.DirectorySeparatorChar,
|
|
||||||
StringComparison.Ordinal));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.AvPlayer;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.AvPlayer;
|
|
||||||
|
|
||||||
public sealed class AvPlayerStreamInfoTests
|
|
||||||
{
|
|
||||||
private const string StreamInfoExNid = "ctTAcF5DiKQ";
|
|
||||||
private const ulong BaseAddress = 0x1_0000_0000;
|
|
||||||
private const int MemorySize = 0x2000;
|
|
||||||
private const ulong InfoAddress = BaseAddress + 0x100;
|
|
||||||
private const ulong Handle = 0xA0_0000_0001;
|
|
||||||
private const ulong DurationMilliseconds = 0x0102_0304_0506_0708;
|
|
||||||
private const byte Sentinel = 0xAB;
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(false, 0u)]
|
|
||||||
[InlineData(true, 0u)]
|
|
||||||
[InlineData(false, 1u)]
|
|
||||||
[InlineData(true, 1u)]
|
|
||||||
public void GetStreamInfoFunctionsDoNotWritePastThe32ByteStructure(
|
|
||||||
bool useExtendedFunction,
|
|
||||||
uint streamIndex)
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Span<byte> window = stackalloc byte[40];
|
|
||||||
window.Fill(Sentinel);
|
|
||||||
Assert.True(memory.TryWrite(InfoAddress, window));
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = Handle;
|
|
||||||
context[CpuRegister.Rsi] = streamIndex;
|
|
||||||
context[CpuRegister.Rdx] = InfoAddress;
|
|
||||||
|
|
||||||
var resultCode = useExtendedFunction
|
|
||||||
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
|
|
||||||
: AvPlayerExports.AvPlayerGetStreamInfo(context);
|
|
||||||
Assert.Equal(0, resultCode);
|
|
||||||
|
|
||||||
Span<byte> result = stackalloc byte[40];
|
|
||||||
Assert.True(memory.TryRead(InfoAddress, result));
|
|
||||||
Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result));
|
|
||||||
if (streamIndex == 0)
|
|
||||||
{
|
|
||||||
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
|
|
||||||
Assert.Equal(720u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(result[8..]));
|
|
||||||
Assert.Equal(48_000u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..]));
|
|
||||||
}
|
|
||||||
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
|
|
||||||
|
|
||||||
for (var index = 32; index < result.Length; index++)
|
|
||||||
{
|
|
||||||
Assert.Equal(Sentinel, result[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(false)]
|
|
||||||
[InlineData(true)]
|
|
||||||
public void GetStreamInfoFunctionsRejectInvalidArguments(bool useExtendedFunction)
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context[CpuRegister.Rdi] = Handle;
|
|
||||||
context[CpuRegister.Rsi] = 2;
|
|
||||||
context[CpuRegister.Rdx] = InfoAddress;
|
|
||||||
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
|
|
||||||
|
|
||||||
context[CpuRegister.Rsi] = 0;
|
|
||||||
context[CpuRegister.Rdx] = 0;
|
|
||||||
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = Handle + 1;
|
|
||||||
context[CpuRegister.Rdx] = InfoAddress;
|
|
||||||
Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void StreamInfoExExportIsRegisteredForGen5Only()
|
|
||||||
{
|
|
||||||
var gen4Manager = new ModuleManager();
|
|
||||||
gen4Manager.RegisterExports(
|
|
||||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4));
|
|
||||||
Assert.False(gen4Manager.TryGetExport(StreamInfoExNid, out _));
|
|
||||||
|
|
||||||
var gen5Manager = new ModuleManager();
|
|
||||||
gen5Manager.RegisterExports(
|
|
||||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
|
||||||
Assert.True(gen5Manager.TryGetExport(StreamInfoExNid, out var export));
|
|
||||||
Assert.Equal("sceAvPlayerGetStreamInfoEx", export.Name);
|
|
||||||
Assert.Equal("libSceAvPlayer", export.LibraryName);
|
|
||||||
Assert.Equal(Generation.Gen5, export.Target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int InvokeGetStreamInfo(CpuContext context, bool useExtendedFunction) =>
|
|
||||||
useExtendedFunction
|
|
||||||
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
|
|
||||||
: AvPlayerExports.AvPlayerGetStreamInfo(context);
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// 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 EXTRQ/INSERTQ bit-field semantics used by the general SSE4a
|
|
||||||
// illegal-instruction software fallback (DirectExecutionBackend.Amd64Compat.cs). Expected values
|
|
||||||
// were computed from the AMD64 Architecture Programmer's Manual definitions and cross-checked
|
|
||||||
// with an independent Python re-implementation before being written here, so a regression in the
|
|
||||||
// ported bit math fails in this file without needing a live guest or a Windows host.
|
|
||||||
public sealed class Sse4aBitFieldEmulatorTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void ExtractBitField_ExtractsLowByte()
|
|
||||||
{
|
|
||||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 8, index: 0);
|
|
||||||
|
|
||||||
Assert.Equal(0xF0UL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ExtractBitField_ExtractsMidFieldAtNonZeroIndex()
|
|
||||||
{
|
|
||||||
// bits [31:16] of 0x1234_5678_9ABC_DEF0 == 0x9ABC
|
|
||||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 16, index: 16);
|
|
||||||
|
|
||||||
Assert.Equal(0x9ABCUL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ExtractBitField_LengthZeroMeansSixtyFour()
|
|
||||||
{
|
|
||||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0xFFFF_FFFF_FFFF_FFFF, length: 0, index: 0);
|
|
||||||
|
|
||||||
Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ExtractBitField_MasksImmediatesToLowSixBits()
|
|
||||||
{
|
|
||||||
// length=0x28 (40) and index=0 is exactly the idiom SharpEmu's load-time
|
|
||||||
// Sse4aExtrqBlendPatch already recognizes; the general emulator must agree with it.
|
|
||||||
var result = Sse4aBitFieldEmulator.ExtractBitField(0x0000_0000_0000_00FF, length: 0x28, index: 0);
|
|
||||||
|
|
||||||
Assert.Equal(0xFFUL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0x1234_5678_9ABC_DEF0UL)]
|
|
||||||
[InlineData(0x0000_0000_0000_0000UL)]
|
|
||||||
[InlineData(0xFFFF_FFFF_FFFF_FFFFUL)]
|
|
||||||
[InlineData(0x00FF_00FF_00FF_00FFUL)]
|
|
||||||
public void ExtractBitField_AgreesWithSse4aExtrqBlendPatchsByteFourRule(ulong value)
|
|
||||||
{
|
|
||||||
// Sse4aExtrqBlendPatch's own comment states that after "EXTRQ xmmN, 0x28, 0x00", dword
|
|
||||||
// lane 1 (bits 63:32) of the result equals byte 4 of the source zero-extended. The
|
|
||||||
// general emulator (used for every other EXTRQ occurrence) must produce a result
|
|
||||||
// consistent with that independently-reverse-engineered rule for the one idiom both
|
|
||||||
// paths can be checked against.
|
|
||||||
var extractedLow64 = Sse4aBitFieldEmulator.ExtractBitField(value, length: 0x28, index: 0);
|
|
||||||
var dword1 = (uint)(extractedLow64 >> 32);
|
|
||||||
var byteFourZeroExtended = (uint)((value >> 32) & 0xFF);
|
|
||||||
|
|
||||||
Assert.Equal(byteFourZeroExtended, dword1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ExtractBitField_RejectsUndefinedFieldPastRegisterEnd()
|
|
||||||
{
|
|
||||||
Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 8, index: 60));
|
|
||||||
Assert.Equal(0UL, Sse4aBitFieldEmulator.ExtractBitField(
|
|
||||||
0xFFFF_FFFF_FFFF_FFFF,
|
|
||||||
length: 8,
|
|
||||||
index: 60));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ExtractBitField_RejectsZeroLengthAtNonZeroIndex()
|
|
||||||
{
|
|
||||||
Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 0, index: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InsertBitField_InsertsFieldAtNonZeroIndexWithoutDisturbingOtherBits()
|
|
||||||
{
|
|
||||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destination: 0x0000_0000_0000_0000,
|
|
||||||
source: 0xFFFF_FFFF_FFFF_FFFF,
|
|
||||||
length: 8,
|
|
||||||
index: 8);
|
|
||||||
|
|
||||||
Assert.Equal(0x0000_0000_0000_FF00UL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InsertBitField_ClearsExactlyTheDestinationWindowBeforeInserting()
|
|
||||||
{
|
|
||||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destination: 0xFFFF_FFFF_FFFF_FFFF,
|
|
||||||
source: 0x0000_0000_0000_0000,
|
|
||||||
length: 16,
|
|
||||||
index: 16);
|
|
||||||
|
|
||||||
Assert.Equal(0xFFFF_FFFF_0000_FFFFUL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InsertBitField_InsertsLowByteAtIndexZero()
|
|
||||||
{
|
|
||||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destination: 0x1122_3344_5566_7788,
|
|
||||||
source: 0xAABB_CCDD_EEFF_0011,
|
|
||||||
length: 8,
|
|
||||||
index: 0);
|
|
||||||
|
|
||||||
Assert.Equal(0x1122_3344_5566_7711UL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InsertBitField_LengthZeroMeansSixtyFourAndOverwritesEverything()
|
|
||||||
{
|
|
||||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destination: 0x1111_1111_1111_1111,
|
|
||||||
source: 0xFFFF_FFFF_FFFF_FFFF,
|
|
||||||
length: 0,
|
|
||||||
index: 0);
|
|
||||||
|
|
||||||
Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InsertBitField_ZeroSourceFieldClearsOnlyItsOwnWindow()
|
|
||||||
{
|
|
||||||
// A zero-valued 12-bit field inserted at index 20 clears exactly bits [31:20]
|
|
||||||
// (0x234 -> 0x000) and leaves every bit outside that window untouched.
|
|
||||||
var result = Sse4aBitFieldEmulator.InsertBitField(
|
|
||||||
destination: 0xABCD_EF01_2345_6789,
|
|
||||||
source: 0,
|
|
||||||
length: 12,
|
|
||||||
index: 20);
|
|
||||||
|
|
||||||
Assert.Equal(0xABCD_EF01_0005_6789UL, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.Fiber;
|
|
||||||
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Fiber;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contract tests for the libSceFiber HLE exports. These pin the current
|
|
||||||
/// validation and layout behaviour of <see cref="FiberExports"/>; they do not
|
|
||||||
/// exercise a live guest thread scheduler.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FiberExportsTests
|
|
||||||
{
|
|
||||||
private const ulong Base = 0x3_0000_0000UL;
|
|
||||||
private const int RegionSize = 0x2000;
|
|
||||||
|
|
||||||
private const int ErrorNull = unchecked((int)0x80590001);
|
|
||||||
private const int ErrorAlignment = unchecked((int)0x80590002);
|
|
||||||
private const int ErrorRange = unchecked((int)0x80590003);
|
|
||||||
private const int ErrorInvalid = unchecked((int)0x80590004);
|
|
||||||
private const int ErrorPermission = unchecked((int)0x80590005);
|
|
||||||
|
|
||||||
private const uint SignatureStart = 0xDEF1649Cu;
|
|
||||||
private const uint SignatureEnd = 0xB37592A0u;
|
|
||||||
private const ulong StackSignature = 0x7149F2CA7149F2CAUL;
|
|
||||||
private const uint StateIdle = 2;
|
|
||||||
|
|
||||||
private const ulong FiberAddress = Base;
|
|
||||||
private const ulong NameAddress = Base + 0x200;
|
|
||||||
private const ulong ContextAddress = Base + 0x400;
|
|
||||||
private const ulong EntryAddress = 0x4_0000_1000UL;
|
|
||||||
private const ulong InfoAddress = Base + 0x800;
|
|
||||||
|
|
||||||
public FiberExportsTests()
|
|
||||||
{
|
|
||||||
FiberExports.ResetRuntimeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OptParamInitialize_NullParam_ReturnsNullError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = 0;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberOptParamInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorNull, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetSelf_NullOutAddress_ReturnsNullError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = 0;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberGetSelf(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorNull, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetSelf_OutsideFiberContext_ReturnsPermissionError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = Base + 0x100;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberGetSelf(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorPermission, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetInfo_NullInfo_ReturnsNullError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = 0;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberGetInfo(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorNull, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_NullFiber_ReturnsNullError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
WriteCString(memory, NameAddress, "F");
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = 0;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorNull, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_NullName_ReturnsNullError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = 0;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorNull, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_NullEntry_ReturnsNullError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
WriteCString(memory, NameAddress, "F");
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = 0;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorNull, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_MisalignedFiber_ReturnsAlignmentError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
WriteCString(memory, NameAddress, "F");
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress + 4;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorAlignment, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_TooSmallContextSize_ReturnsRangeError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
WriteCString(memory, NameAddress, "F");
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
context[CpuRegister.R8] = ContextAddress;
|
|
||||||
context[CpuRegister.R9] = 256;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorRange, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_ContextAddressWithoutSize_ReturnsInvalidError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
WriteCString(memory, NameAddress, "F");
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
context[CpuRegister.R8] = ContextAddress;
|
|
||||||
context[CpuRegister.R9] = 0;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorInvalid, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Initialize_Valid_WritesExpectedLayout()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
const string name = "TestFiber";
|
|
||||||
WriteCString(memory, NameAddress, name);
|
|
||||||
const ulong argOnInitialize = 0xDEADUL;
|
|
||||||
const ulong contextSize = 512UL;
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
context[CpuRegister.Rcx] = argOnInitialize;
|
|
||||||
context[CpuRegister.R8] = ContextAddress;
|
|
||||||
context[CpuRegister.R9] = contextSize;
|
|
||||||
// RSP defaults to 0 (unmapped); ReadStackArg64 falls back to 0 ->
|
|
||||||
// optParam = 0, buildVersion = 0. ApplyInitializationFlags(0, 0, false) == 0.
|
|
||||||
|
|
||||||
var result = FiberExports.FiberInitialize(context);
|
|
||||||
|
|
||||||
Assert.Equal(0, result);
|
|
||||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
||||||
|
|
||||||
Assert.Equal(SignatureStart, ReadUInt32(memory, FiberAddress + 0));
|
|
||||||
Assert.Equal(StateIdle, ReadUInt32(memory, FiberAddress + 4));
|
|
||||||
Assert.Equal(EntryAddress, ReadUInt64(memory, FiberAddress + 8));
|
|
||||||
Assert.Equal(argOnInitialize, ReadUInt64(memory, FiberAddress + 16));
|
|
||||||
Assert.Equal(ContextAddress, ReadUInt64(memory, FiberAddress + 24));
|
|
||||||
Assert.Equal(contextSize, ReadUInt64(memory, FiberAddress + 32));
|
|
||||||
AssertInlineName(memory, FiberAddress + 40, name);
|
|
||||||
Assert.Equal(0UL, ReadUInt64(memory, FiberAddress + 72));
|
|
||||||
Assert.Equal(0u, ReadUInt32(memory, FiberAddress + 80));
|
|
||||||
Assert.Equal(ContextAddress, ReadUInt64(memory, FiberAddress + 88));
|
|
||||||
Assert.Equal(ContextAddress + contextSize, ReadUInt64(memory, FiberAddress + 96));
|
|
||||||
Assert.Equal(SignatureEnd, ReadUInt32(memory, FiberAddress + 104));
|
|
||||||
Assert.Equal(StackSignature, ReadUInt64(memory, ContextAddress));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetInfo_AfterInitialize_RoundTripsFields()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
const string name = "RoundTrip";
|
|
||||||
WriteCString(memory, NameAddress, name);
|
|
||||||
const ulong argOnInitialize = 0xCAFEUL;
|
|
||||||
const ulong contextSize = 512UL;
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
context[CpuRegister.Rcx] = argOnInitialize;
|
|
||||||
context[CpuRegister.R8] = ContextAddress;
|
|
||||||
context[CpuRegister.R9] = contextSize;
|
|
||||||
|
|
||||||
Assert.Equal(0, FiberExports.FiberInitialize(context));
|
|
||||||
|
|
||||||
WriteUInt64(memory, InfoAddress, 128);
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = InfoAddress;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberGetInfo(context);
|
|
||||||
|
|
||||||
Assert.Equal(0, result);
|
|
||||||
Assert.Equal(EntryAddress, ReadUInt64(memory, InfoAddress + 8));
|
|
||||||
Assert.Equal(argOnInitialize, ReadUInt64(memory, InfoAddress + 16));
|
|
||||||
Assert.Equal(ContextAddress, ReadUInt64(memory, InfoAddress + 24));
|
|
||||||
Assert.Equal(contextSize, ReadUInt64(memory, InfoAddress + 32));
|
|
||||||
AssertInlineName(memory, InfoAddress + 40, name);
|
|
||||||
Assert.Equal(ulong.MaxValue, ReadUInt64(memory, InfoAddress + 72));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetInfo_WrongSize_ReturnsInvalidError()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
WriteCString(memory, NameAddress, "F");
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = NameAddress;
|
|
||||||
context[CpuRegister.Rdx] = EntryAddress;
|
|
||||||
context[CpuRegister.R8] = ContextAddress;
|
|
||||||
context[CpuRegister.R9] = 512;
|
|
||||||
|
|
||||||
Assert.Equal(0, FiberExports.FiberInitialize(context));
|
|
||||||
|
|
||||||
WriteUInt64(memory, InfoAddress, 64);
|
|
||||||
context[CpuRegister.Rdi] = FiberAddress;
|
|
||||||
context[CpuRegister.Rsi] = InfoAddress;
|
|
||||||
|
|
||||||
var result = FiberExports.FiberGetInfo(context);
|
|
||||||
|
|
||||||
Assert.Equal(ErrorInvalid, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteCString(FakeCpuMemory memory, ulong address, string text)
|
|
||||||
{
|
|
||||||
memory.WriteCString(address, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
|
||||||
Assert.True(memory.TryWrite(address, buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
|
||||||
Assert.True(memory.TryRead(address, buffer));
|
|
||||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
|
||||||
Assert.True(memory.TryRead(address, buffer));
|
|
||||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssertInlineName(FakeCpuMemory memory, ulong address, string expected)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[32];
|
|
||||||
Assert.True(memory.TryRead(address, buffer));
|
|
||||||
var length = buffer.IndexOf((byte)0);
|
|
||||||
if (length < 0)
|
|
||||||
{
|
|
||||||
length = buffer.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Equal(expected, System.Text.Encoding.UTF8.GetString(buffer[..length]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.GUI;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.GUI;
|
|
||||||
|
|
||||||
public sealed class GuiSettingsTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_AllPropertiesNull_FallsBackToDefaults()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{
|
|
||||||
"LogLevel": null,
|
|
||||||
"GameFolders": null,
|
|
||||||
"ExcludedGames": null,
|
|
||||||
"EnvironmentToggles": null,
|
|
||||||
"Language": null,
|
|
||||||
"DiscordClientId": null
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var settings = GuiSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.Equal("Info", settings.LogLevel);
|
|
||||||
Assert.Equal("en", settings.Language);
|
|
||||||
Assert.Equal("1525606762248540221", settings.DiscordClientId);
|
|
||||||
Assert.Empty(settings.GameFolders);
|
|
||||||
Assert.Empty(settings.ExcludedGames);
|
|
||||||
Assert.Empty(settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_ValidValues_ArePreserved()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{
|
|
||||||
"LogLevel": "Debug",
|
|
||||||
"GameFolders": ["C:\\Games"],
|
|
||||||
"ExcludedGames": ["C:\\Games\\skip.bin"],
|
|
||||||
"EnvironmentToggles": ["SHARPEMU_TRACE"],
|
|
||||||
"Language": "pt-BR",
|
|
||||||
"DiscordClientId": "999"
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var settings = GuiSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.Equal("Debug", settings.LogLevel);
|
|
||||||
Assert.Equal("pt-BR", settings.Language);
|
|
||||||
Assert.Equal("999", settings.DiscordClientId);
|
|
||||||
Assert.Equal(["C:\\Games"], settings.GameFolders);
|
|
||||||
Assert.Equal(["C:\\Games\\skip.bin"], settings.ExcludedGames);
|
|
||||||
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// An empty Discord client ID intentionally disables Rich Presence.
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_EmptyDiscordClientId_IsPreservedNotNormalized()
|
|
||||||
{
|
|
||||||
const string json = """{ "DiscordClientId": "" }""";
|
|
||||||
|
|
||||||
var settings = GuiSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.Equal(string.Empty, settings.DiscordClientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_NullOrEmptyListEntries_AreFilteredOut()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{
|
|
||||||
"GameFolders": ["C:\\Games", null, ""],
|
|
||||||
"ExcludedGames": [null],
|
|
||||||
"EnvironmentToggles": [null, "SHARPEMU_TRACE", ""]
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var settings = GuiSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.Equal(["C:\\Games"], settings.GameFolders);
|
|
||||||
Assert.Empty(settings.ExcludedGames);
|
|
||||||
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_EmptyObject_UsesConstructorDefaults()
|
|
||||||
{
|
|
||||||
var settings = GuiSettings.NormalizeFromJson("{}");
|
|
||||||
|
|
||||||
Assert.Equal("Info", settings.LogLevel);
|
|
||||||
Assert.Equal("en", settings.Language);
|
|
||||||
Assert.Equal("1525606762248540221", settings.DiscordClientId);
|
|
||||||
Assert.Empty(settings.GameFolders);
|
|
||||||
Assert.Empty(settings.ExcludedGames);
|
|
||||||
Assert.Empty(settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.GUI;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.GUI;
|
|
||||||
|
|
||||||
public sealed class PerGameSettingsTests
|
|
||||||
{
|
|
||||||
// Invalid entries must not reach Environment.SetEnvironmentVariable.
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_NullOrEmptyToggleEntries_AreFilteredOut()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{ "EnvironmentToggles": [null, "SHARPEMU_TRACE", ""] }
|
|
||||||
""";
|
|
||||||
|
|
||||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.NotNull(settings);
|
|
||||||
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A null list means that the global setting should be inherited.
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_NullToggleList_StaysNull()
|
|
||||||
{
|
|
||||||
const string json = """{ "EnvironmentToggles": null }""";
|
|
||||||
|
|
||||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.NotNull(settings);
|
|
||||||
Assert.Null(settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_EmptyToggleList_StaysEmpty()
|
|
||||||
{
|
|
||||||
const string json = """{ "EnvironmentToggles": [] }""";
|
|
||||||
|
|
||||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.NotNull(settings);
|
|
||||||
Assert.Empty(Assert.IsType<List<string>>(settings.EnvironmentToggles));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeFromJson_ValidToggles_ArePreserved()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{ "EnvironmentToggles": ["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"] }
|
|
||||||
""";
|
|
||||||
|
|
||||||
var settings = PerGameSettings.NormalizeFromJson(json);
|
|
||||||
|
|
||||||
Assert.NotNull(settings);
|
|
||||||
Assert.Equal(["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"], settings.EnvironmentToggles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.HLE;
|
|
||||||
|
|
||||||
public sealed class GuestWriteWatchTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0x0000001000000001, "torn")]
|
|
||||||
[InlineData(0x0000FFFF00000001, "torn")]
|
|
||||||
[InlineData(0x0000000008000000, "shift")]
|
|
||||||
[InlineData(0x0000000080015F00, "shift")]
|
|
||||||
[InlineData(0x0000000007FFFFFF, null)]
|
|
||||||
[InlineData(0x0000000009000000, null)]
|
|
||||||
[InlineData(0x000000003F800000, null)]
|
|
||||||
[InlineData(0x0000000080015F01, null)]
|
|
||||||
[InlineData(0x0001000000000001, null)]
|
|
||||||
public void ClassifyBulkValue_RecognizesCorruptionSignatures(ulong value, string? expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, GuestWriteWatch.ClassifyBulkValue(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0, 0)]
|
|
||||||
[InlineData(1, 7)]
|
|
||||||
[InlineData(3, 5)]
|
|
||||||
[InlineData(7, 1)]
|
|
||||||
[InlineData(8, 0)]
|
|
||||||
public void FirstAlignedOffset_ReturnsTheNextEightByteBoundary(ulong address, int expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, GuestWriteWatch.FirstAlignedOffset(address));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0x1000, 8, 0x1000, true)]
|
|
||||||
[InlineData(0x0FFF, 1, 0x1000, false)]
|
|
||||||
[InlineData(0x0FFF, 2, 0x1000, true)]
|
|
||||||
[InlineData(0x1008, 1, 0x1000, false)]
|
|
||||||
[InlineData(ulong.MaxValue - 3, 4, ulong.MaxValue - 1, true)]
|
|
||||||
[InlineData(ulong.MaxValue, 1, ulong.MaxValue, true)]
|
|
||||||
[InlineData(0x1000, 0, 0x1000, false)]
|
|
||||||
public void Overlaps_HandlesBoundariesWithoutOverflow(
|
|
||||||
ulong address,
|
|
||||||
int length,
|
|
||||||
ulong slot,
|
|
||||||
bool expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, GuestWriteWatch.Overlaps(address, length, slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0x10000, 0xF2, true)]
|
|
||||||
[InlineData(0x10001, 0xF2, false)]
|
|
||||||
[InlineData(0x10000, 0xF1, false)]
|
|
||||||
public void IsPoolMapping_RequiresTheExpectedSizeAndProtection(
|
|
||||||
ulong length,
|
|
||||||
int protection,
|
|
||||||
bool expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, GuestWriteWatch.IsPoolMapping(length, protection));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(null, 0)]
|
|
||||||
[InlineData("", 0)]
|
|
||||||
[InlineData("not-hex", 0)]
|
|
||||||
[InlineData("80", 0x80)]
|
|
||||||
[InlineData("0x80", 0x80)]
|
|
||||||
[InlineData(" 0X801DB3BBB ", 0x801DB3BBB)]
|
|
||||||
public void Parse_HandlesHexadecimalWatchValues(string? text, ulong expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, GuestWriteWatch.Parse(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,90 +41,6 @@ public sealed class KernelMemoryCompatExportsTests
|
|||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PosixOpen_MissingFileReturnsMinusOne()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong pathAddress = memoryBase + 0x100;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/il2cpp.usym");
|
|
||||||
context[CpuRegister.Rdi] = pathAddress;
|
|
||||||
context[CpuRegister.Rsi] = 0; // O_RDONLY
|
|
||||||
|
|
||||||
var result = KernelMemoryCompatExports.PosixOpen(context);
|
|
||||||
|
|
||||||
// A libc open() failure must be -1, not the raw 0x8002xxxx sentinel the
|
|
||||||
// guest would otherwise store as a valid fd and later dereference.
|
|
||||||
Assert.Equal(-1, result);
|
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PosixFstat_BadDescriptorReturnsMinusOne()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong statAddress = memoryBase + 0x400;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
context[CpuRegister.Rdi] = 0x80020002; // the not-found sentinel misused as an fd
|
|
||||||
context[CpuRegister.Rsi] = statAddress;
|
|
||||||
|
|
||||||
var result = KernelMemoryCompatExports.PosixFstat(context);
|
|
||||||
|
|
||||||
Assert.Equal(-1, result);
|
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PosixClose_BadDescriptorReturnsMinusOne()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
|
|
||||||
|
|
||||||
var result = KernelMemoryCompatExports.PosixClose(context);
|
|
||||||
|
|
||||||
Assert.Equal(-1, result);
|
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PosixRead_BadDescriptorReturnsMinusOne()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong bufferAddress = memoryBase + 0x200;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
|
|
||||||
context[CpuRegister.Rsi] = bufferAddress;
|
|
||||||
context[CpuRegister.Rdx] = 0x40;
|
|
||||||
|
|
||||||
var result = KernelMemoryCompatExports.PosixRead(context);
|
|
||||||
|
|
||||||
Assert.Equal(-1, result);
|
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PosixWrite_BadDescriptorReturnsMinusOne()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
|
||||||
const ulong bufferAddress = memoryBase + 0x200;
|
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
memory.WriteCString(bufferAddress, "payload");
|
|
||||||
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
|
|
||||||
context[CpuRegister.Rsi] = bufferAddress;
|
|
||||||
context[CpuRegister.Rdx] = 0x7;
|
|
||||||
|
|
||||||
var result = KernelMemoryCompatExports.PosixWrite(context);
|
|
||||||
|
|
||||||
Assert.Equal(-1, result);
|
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
|
public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,11 +37,10 @@ public sealed class KernelSocketCompatExportsTests
|
|||||||
KernelMemoryCompatExports.PosixClose(context));
|
KernelMemoryCompatExports.PosixClose(context));
|
||||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||||
|
|
||||||
// A second close of an already-closed fd fails per the POSIX ABI:
|
|
||||||
// -1 with errno set, not the raw Orbis NOT_FOUND sentinel.
|
|
||||||
context[CpuRegister.Rdi] = unchecked((ulong)guestFd);
|
context[CpuRegister.Rdi] = unchecked((ulong)guestFd);
|
||||||
Assert.Equal(-1, KernelMemoryCompatExports.PosixClose(context));
|
Assert.Equal(
|
||||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND,
|
||||||
|
KernelMemoryCompatExports.PosixClose(context));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Memory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The write generation lets GPU caches detect guest CPU rewrites even after
|
|
||||||
/// another cache owner consumed the (single) dirty flag: the generation is
|
|
||||||
/// monotonic and survives consume/re-arm cycles and range replacement. These
|
|
||||||
/// invariants back the presenter's stale-upload detection for CPU-rewritten
|
|
||||||
/// images (video planes, streamed font atlases).
|
|
||||||
/// </summary>
|
|
||||||
public sealed unsafe class GuestImageWriteTrackerTests
|
|
||||||
{
|
|
||||||
// The tracker aligns to the guest's 4 KiB pages; the mprotect underneath
|
|
||||||
// operates on host pages, which are 16 KiB on Apple Silicon (the emulator
|
|
||||||
// itself always runs with 4 KiB host pages under Rosetta, but this test
|
|
||||||
// host may not). Align the allocation to the largest host page size so
|
|
||||||
// the kernel's rounding stays inside memory this test owns instead of
|
|
||||||
// spilling onto neighbouring heap pages.
|
|
||||||
private const nuint TrackedByteCount = 4096;
|
|
||||||
private const nuint HostPageAlignment = 16384;
|
|
||||||
|
|
||||||
private static ulong AllocateTrackedPages(out void* allocation)
|
|
||||||
{
|
|
||||||
allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment);
|
|
||||||
return (ulong)allocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerationSurvivesDirtyConsume()
|
|
||||||
{
|
|
||||||
if (!GuestImageWriteTracker.Enabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var address = AllocateTrackedPages(out var allocation);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Track(address, TrackedByteCount);
|
|
||||||
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
|
|
||||||
Assert.Equal(0, generation);
|
|
||||||
|
|
||||||
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
|
||||||
Assert.True(GuestImageWriteTracker.ConsumeDirty(address));
|
|
||||||
|
|
||||||
// Consuming the dirty flag must not roll back the generation:
|
|
||||||
// that is exactly what lets a second cache owner still observe
|
|
||||||
// the rewrite after the first owner consumed the flag.
|
|
||||||
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out generation));
|
|
||||||
Assert.Equal(1, generation);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Untrack(address);
|
|
||||||
NativeMemory.Free(allocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerationIncrementsOncePerArmedLifetime()
|
|
||||||
{
|
|
||||||
if (!GuestImageWriteTracker.Enabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var address = AllocateTrackedPages(out var allocation);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Track(address, TrackedByteCount);
|
|
||||||
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
|
||||||
// The first fault disarmed the range; later writes are free-running
|
|
||||||
// and must not inflate the generation until the owner re-arms.
|
|
||||||
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
|
||||||
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
|
|
||||||
Assert.Equal(1, generation);
|
|
||||||
|
|
||||||
GuestImageWriteTracker.Rearm(address);
|
|
||||||
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
|
||||||
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out generation));
|
|
||||||
Assert.Equal(2, generation);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Untrack(address);
|
|
||||||
NativeMemory.Free(allocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerationCarriesAcrossRangeReplacement()
|
|
||||||
{
|
|
||||||
if (!GuestImageWriteTracker.Enabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var address = AllocateTrackedPages(out var allocation);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Track(address, TrackedByteCount);
|
|
||||||
Assert.True(GuestImageWriteTracker.TryHandleWriteFault(address));
|
|
||||||
|
|
||||||
// Re-registering the same allocation with a different size retires
|
|
||||||
// the range object (the signal handler may still see the old
|
|
||||||
// snapshot) but must carry the generation, otherwise a resize
|
|
||||||
// would hide the rewrite from cache owners.
|
|
||||||
GuestImageWriteTracker.Track(address, 2 * TrackedByteCount);
|
|
||||||
Assert.True(GuestImageWriteTracker.TryGetWriteGeneration(address, out var generation));
|
|
||||||
Assert.Equal(1, generation);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
GuestImageWriteTracker.Untrack(address);
|
|
||||||
NativeMemory.Free(allocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UntrackedAddressHasNoGeneration()
|
|
||||||
{
|
|
||||||
if (!GuestImageWriteTracker.Enabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.False(GuestImageWriteTracker.TryGetWriteGeneration(0xDEAD_0000_0000UL, out _));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -121,113 +121,6 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses);
|
Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange()
|
|
||||||
{
|
|
||||||
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
|
||||||
const ulong rangeSize = 0x40_0000;
|
|
||||||
const ulong occupiedSize = 0x4_0000;
|
|
||||||
using var host = new PartialOverlapHostMemory(rangeBase, occupiedSize, rangeSize);
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
Assert.True(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false));
|
|
||||||
|
|
||||||
// Only the free tail is allocated; the already-occupied head is untouched.
|
|
||||||
Assert.Equal(
|
|
||||||
[(rangeBase + occupiedSize, rangeSize - occupiedSize)],
|
|
||||||
host.AllocationCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TryBackFixedRangeReturnsFalseWhenRangeIsFullyOccupied()
|
|
||||||
{
|
|
||||||
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
|
||||||
const ulong rangeSize = 0x40_0000;
|
|
||||||
using var host = new PartialOverlapHostMemory(rangeBase, rangeSize, rangeSize);
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
Assert.False(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false));
|
|
||||||
Assert.Empty(host.AllocationCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class PartialOverlapHostMemory : IHostMemory, IDisposable
|
|
||||||
{
|
|
||||||
private readonly ulong _rangeBase;
|
|
||||||
private readonly ulong _occupiedEnd;
|
|
||||||
private readonly ulong _rangeEnd;
|
|
||||||
|
|
||||||
public PartialOverlapHostMemory(ulong rangeBase, ulong occupiedSize, ulong rangeSize)
|
|
||||||
{
|
|
||||||
_rangeBase = rangeBase;
|
|
||||||
_occupiedEnd = rangeBase + occupiedSize;
|
|
||||||
_rangeEnd = rangeBase + rangeSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<(ulong Address, ulong Size)> AllocationCalls { get; } = [];
|
|
||||||
|
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
|
||||||
{
|
|
||||||
AllocationCalls.Add((desiredAddress, size));
|
|
||||||
return desiredAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
|
||||||
|
|
||||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
|
||||||
|
|
||||||
public bool Free(ulong address) => true;
|
|
||||||
|
|
||||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
||||||
{
|
|
||||||
rawOldProtection = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
||||||
{
|
|
||||||
rawOldProtection = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Query(ulong address, out HostRegionInfo info)
|
|
||||||
{
|
|
||||||
// Report contiguous runs of same-state pages, exactly as VirtualQuery
|
|
||||||
// does: the occupied head first, then the free tail.
|
|
||||||
if (address < _occupiedEnd)
|
|
||||||
{
|
|
||||||
info = new HostRegionInfo(
|
|
||||||
address,
|
|
||||||
_rangeBase,
|
|
||||||
_occupiedEnd - address,
|
|
||||||
HostRegionState.Committed,
|
|
||||||
RawState: 0x1000,
|
|
||||||
HostPageProtection.ReadWrite,
|
|
||||||
RawProtection: 0x04,
|
|
||||||
RawAllocationProtection: 0x04);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
info = new HostRegionInfo(
|
|
||||||
address,
|
|
||||||
AllocationBase: 0,
|
|
||||||
_rangeEnd - address,
|
|
||||||
HostRegionState.Free,
|
|
||||||
RawState: 0x10000,
|
|
||||||
HostPageProtection.NoAccess,
|
|
||||||
RawProtection: 0x01,
|
|
||||||
RawAllocationProtection: 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FlushInstructionCache(ulong address, ulong size)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeHostMemory : IHostMemory
|
private sealed class FakeHostMemory : IHostMemory
|
||||||
{
|
{
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
|||||||
@@ -39,60 +39,6 @@ public sealed class PhysicalVirtualMemoryTests
|
|||||||
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RepeatedLazyReadUsesCommittedRangeCache()
|
|
||||||
{
|
|
||||||
using var host = new LazyZeroedHostMemory();
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
||||||
host.CommitCalls.Clear();
|
|
||||||
|
|
||||||
Span<byte> buffer = stackalloc byte[16];
|
|
||||||
Assert.True(memory.TryRead(address + 0x100, buffer));
|
|
||||||
var queryCallsAfterFirstRead = host.QueryCalls;
|
|
||||||
Assert.True(memory.TryRead(address + 0x108, buffer[..8]));
|
|
||||||
|
|
||||||
Assert.Equal(queryCallsAfterFirstRead, host.QueryCalls);
|
|
||||||
Assert.Single(host.CommitCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TryCopyHandlesOverlappingIdentityMappedRanges()
|
|
||||||
{
|
|
||||||
using var host = new LazyZeroedHostMemory();
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
||||||
Assert.True(memory.TryWrite(address, new byte[] { 1, 2, 3, 4, 5, 6 }));
|
|
||||||
|
|
||||||
Assert.True(memory.TryCopy(address + 2, address, 4));
|
|
||||||
|
|
||||||
Span<byte> result = stackalloc byte[6];
|
|
||||||
Assert.True(memory.TryRead(address, result));
|
|
||||||
Assert.Equal(new byte[] { 1, 2, 1, 2, 3, 4 }, result.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RepeatedTryCopyKeepsSourceAndDestinationCommitRangesCached()
|
|
||||||
{
|
|
||||||
using var host = new LazyZeroedHostMemory();
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
||||||
var source = address + 0x100;
|
|
||||||
var destination = address + 0x1100;
|
|
||||||
Assert.True(memory.TryWrite(source, new byte[] { 1, 2, 3, 4 }));
|
|
||||||
Assert.True(memory.TryWrite(destination, new byte[4]));
|
|
||||||
|
|
||||||
host.CommitCalls.Clear();
|
|
||||||
Assert.True(memory.TryCopy(destination, source, 4));
|
|
||||||
var queryCallsAfterFirstCopy = host.QueryCalls;
|
|
||||||
Assert.True(memory.TryCopy(destination, source, 4));
|
|
||||||
|
|
||||||
Assert.Equal(queryCallsAfterFirstCopy, host.QueryCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Reserve-only region: GetPointer commits the page before returning it,
|
// 2. Reserve-only region: GetPointer commits the page before returning it,
|
||||||
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
|
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -179,14 +125,12 @@ public sealed class PhysicalVirtualMemoryTests
|
|||||||
|
|
||||||
public LazyZeroedHostMemory()
|
public LazyZeroedHostMemory()
|
||||||
{
|
{
|
||||||
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x3000);
|
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
|
||||||
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
|
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
||||||
|
|
||||||
public int QueryCalls { get; private set; }
|
|
||||||
|
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
||||||
|
|
||||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
||||||
@@ -218,7 +162,6 @@ public sealed class PhysicalVirtualMemoryTests
|
|||||||
|
|
||||||
public bool Query(ulong address, out HostRegionInfo info)
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
{
|
{
|
||||||
QueryCalls++;
|
|
||||||
var pageAddress = address & ~0xFFFUL;
|
var pageAddress = address & ~0xFFFUL;
|
||||||
info = new HostRegionInfo(
|
info = new HostRegionInfo(
|
||||||
pageAddress,
|
pageAddress,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace SharpEmu.Libs.Tests.Pthread;
|
|||||||
public sealed class PthreadMutexSemanticsTests
|
public sealed class PthreadMutexSemanticsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AdaptiveMutex_SelfLockIsIdempotent()
|
public void AdaptiveMutex_SelfLockUsesCompatibilityRecursion()
|
||||||
{
|
{
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
const ulong memoryBase = 0x1_0000_0000;
|
||||||
const ulong mutexAddress = memoryBase + 0x100;
|
const ulong mutexAddress = memoryBase + 0x100;
|
||||||
@@ -22,180 +22,7 @@ public sealed class PthreadMutexSemanticsTests
|
|||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
||||||
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AdaptiveMutex_GuestTrackedSelfLockReturnsDeadlockAndSingleUnlockReleases()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0001_0000;
|
|
||||||
const ulong mutexAddress = memoryBase + 0x100;
|
|
||||||
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
Assert.True(context.TryWriteUInt64(mutexAddress, 1)); // Static adaptive initializer.
|
|
||||||
context[CpuRegister.Rdi] = mutexAddress;
|
|
||||||
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
|
||||||
|
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
|
||||||
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
|
|
||||||
Assert.Equal(
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK,
|
|
||||||
KernelPthreadCompatExports.PthreadMutexLock(context));
|
|
||||||
|
|
||||||
Assert.True(context.TryWriteUInt64(mutexAddress + 8, 0));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RecursiveMutex_GuestTrackedSelfLockKeepsRecursiveSemantics()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x1_0002_0000;
|
|
||||||
const ulong attrAddress = memoryBase + 0x100;
|
|
||||||
const ulong mutexAddress = memoryBase + 0x200;
|
|
||||||
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = attrAddress;
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context));
|
|
||||||
context[CpuRegister.Rsi] = 2;
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrSettype(context));
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = mutexAddress;
|
|
||||||
context[CpuRegister.Rsi] = attrAddress;
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
|
||||||
|
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
|
||||||
Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
|
||||||
Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ContendedMutex_HandsOffOneHostWaiterAtATime()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x2_0000_0000;
|
|
||||||
const ulong mutexAddress = memoryBase + 0x100;
|
|
||||||
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
|
|
||||||
var ownerContext = new CpuContext(memory, Generation.Gen5);
|
|
||||||
Assert.True(ownerContext.TryWriteUInt64(mutexAddress, 1));
|
|
||||||
ownerContext[CpuRegister.Rdi] = mutexAddress;
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(ownerContext));
|
|
||||||
|
|
||||||
using var waitersStarted = new CountdownEvent(2);
|
|
||||||
using var firstAcquired = new ManualResetEventSlim(false);
|
|
||||||
using var secondAcquired = new ManualResetEventSlim(false);
|
|
||||||
using var releaseFirst = new ManualResetEventSlim(false);
|
|
||||||
var acquisitionCount = 0;
|
|
||||||
|
|
||||||
Task<(int LockResult, int UnlockResult)> StartWaiter() =>
|
|
||||||
Task.Factory.StartNew(
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var waiterContext = new CpuContext(memory, Generation.Gen5);
|
|
||||||
waiterContext[CpuRegister.Rdi] = mutexAddress;
|
|
||||||
waitersStarted.Signal();
|
|
||||||
var lockResult = KernelPthreadCompatExports.PthreadMutexLock(waiterContext);
|
|
||||||
if (lockResult != 0)
|
|
||||||
{
|
|
||||||
return (lockResult, int.MinValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Interlocked.Increment(ref acquisitionCount) == 1)
|
|
||||||
{
|
|
||||||
firstAcquired.Set();
|
|
||||||
releaseFirst.Wait(TimeSpan.FromSeconds(5));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
secondAcquired.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
var unlockResult = KernelPthreadCompatExports.PthreadMutexUnlock(waiterContext);
|
|
||||||
return (lockResult, unlockResult);
|
|
||||||
},
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
|
|
||||||
var firstWaiter = StartWaiter();
|
|
||||||
var secondWaiter = StartWaiter();
|
|
||||||
Assert.True(waitersStarted.Wait(TimeSpan.FromSeconds(5)));
|
|
||||||
Thread.Sleep(50);
|
|
||||||
Assert.Equal(0, Volatile.Read(ref acquisitionCount));
|
|
||||||
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(ownerContext));
|
|
||||||
Assert.True(firstAcquired.Wait(TimeSpan.FromSeconds(5)));
|
|
||||||
Assert.Equal(1, Volatile.Read(ref acquisitionCount));
|
|
||||||
releaseFirst.Set();
|
|
||||||
Assert.True(secondAcquired.Wait(TimeSpan.FromSeconds(5)));
|
|
||||||
|
|
||||||
var results = await Task.WhenAll(firstWaiter, secondWaiter).WaitAsync(TimeSpan.FromSeconds(5));
|
|
||||||
Assert.All(results, result => Assert.Equal((0, 0), result));
|
|
||||||
Assert.Equal(2, Volatile.Read(ref acquisitionCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ContendedMutex_PreservesMutualExclusionUnderLoad()
|
|
||||||
{
|
|
||||||
const ulong memoryBase = 0x3_0000_0000;
|
|
||||||
const ulong mutexAddress = memoryBase + 0x100;
|
|
||||||
const int workerCount = 4;
|
|
||||||
const int iterationsPerWorker = 250;
|
|
||||||
var memory = new AllocatingCpuMemory(memoryBase, 0x4000);
|
|
||||||
var initializationContext = new CpuContext(memory, Generation.Gen5);
|
|
||||||
Assert.True(initializationContext.TryWriteUInt64(mutexAddress, 1));
|
|
||||||
initializationContext[CpuRegister.Rdi] = mutexAddress;
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(initializationContext));
|
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(initializationContext));
|
|
||||||
|
|
||||||
using var start = new ManualResetEventSlim(false);
|
|
||||||
var insideCriticalSection = 0;
|
|
||||||
var mutualExclusionViolations = 0;
|
|
||||||
var protectedCounter = 0;
|
|
||||||
var workers = Enumerable.Range(0, workerCount)
|
|
||||||
.Select(_ => Task.Factory.StartNew(
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
context[CpuRegister.Rdi] = mutexAddress;
|
|
||||||
start.Wait();
|
|
||||||
for (var iteration = 0; iteration < iterationsPerWorker; iteration++)
|
|
||||||
{
|
|
||||||
if (KernelPthreadCompatExports.PthreadMutexLock(context) != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("pthread mutex lock failed during contention stress.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Interlocked.Increment(ref insideCriticalSection) != 1)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref mutualExclusionViolations);
|
|
||||||
}
|
|
||||||
|
|
||||||
protectedCounter++;
|
|
||||||
Thread.SpinWait(20);
|
|
||||||
Interlocked.Decrement(ref insideCriticalSection);
|
|
||||||
|
|
||||||
if (KernelPthreadCompatExports.PthreadMutexUnlock(context) != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("pthread mutex unlock failed during contention stress.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
start.Set();
|
|
||||||
await Task.WhenAll(workers).WaitAsync(TimeSpan.FromSeconds(10));
|
|
||||||
Assert.Equal(0, Volatile.Read(ref mutualExclusionViolations));
|
|
||||||
Assert.Equal(workerCount * iterationsPerWorker, protectedCounter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class AllocatingCpuMemory : ICpuMemory, IGuestMemoryAllocator
|
private sealed class AllocatingCpuMemory : ICpuMemory, IGuestMemoryAllocator
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.Kernel;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Pthread;
|
|
||||||
|
|
||||||
public sealed class PthreadSemaphoreSemanticsTests
|
|
||||||
{
|
|
||||||
private const ulong MemoryBase = 0x1_0000_0000;
|
|
||||||
private const ulong SemaphoreAddress = MemoryBase + 0x100;
|
|
||||||
private const ulong ValueAddress = MemoryBase + 0x200;
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("C36iRE0F5sE", "scePthreadSemWait")]
|
|
||||||
[InlineData("aishVAiFaYM", "scePthreadSemPost")]
|
|
||||||
[InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")]
|
|
||||||
[InlineData("GEnUkDZoUwY", "scePthreadSemInit")]
|
|
||||||
[InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")]
|
|
||||||
public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName)
|
|
||||||
{
|
|
||||||
var manager = new ModuleManager();
|
|
||||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(
|
|
||||||
Generation.Gen4 | Generation.Gen5));
|
|
||||||
|
|
||||||
Assert.True(manager.TryGetExport(nid, out var export));
|
|
||||||
Assert.Equal(exportName, export.Name);
|
|
||||||
Assert.Equal("libKernel", export.LibraryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PthreadSemPostAndWaitShareSemaphoreState()
|
|
||||||
{
|
|
||||||
var context = CreateContext();
|
|
||||||
InitializeSemaphore(context, initialCount: 0);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemPost(context));
|
|
||||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
||||||
Assert.Equal(1, ReadSemaphoreValue(context));
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemWait(context));
|
|
||||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
||||||
Assert.Equal(0, ReadSemaphoreValue(context));
|
|
||||||
|
|
||||||
DestroySemaphore(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PthreadSemTryWaitConsumesAvailableTokenAndReturnsTryAgainWhenEmpty()
|
|
||||||
{
|
|
||||||
var context = CreateContext();
|
|
||||||
InitializeSemaphore(context, initialCount: 1);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTryWait(context));
|
|
||||||
Assert.Equal(0, ReadSemaphoreValue(context));
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN,
|
|
||||||
KernelSemaphoreCompatExports.PthreadSemTryWait(context));
|
|
||||||
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN),
|
|
||||||
context[CpuRegister.Rax]);
|
|
||||||
|
|
||||||
DestroySemaphore(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PthreadSemInitRejectsNonPrivateFlag()
|
|
||||||
{
|
|
||||||
var context = CreateContext();
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
context[CpuRegister.Rsi] = 1;
|
|
||||||
context[CpuRegister.Rdx] = 0;
|
|
||||||
context[CpuRegister.Rcx] = 0;
|
|
||||||
|
|
||||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
|
||||||
KernelSemaphoreCompatExports.PthreadSemInit(context));
|
|
||||||
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT),
|
|
||||||
context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PthreadSemDestroyClearsSemaphoreAndRejectsSecondDestroy()
|
|
||||||
{
|
|
||||||
var context = CreateContext();
|
|
||||||
InitializeSemaphore(context, initialCount: 0);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemDestroy(context));
|
|
||||||
Assert.True(context.TryReadUInt32(SemaphoreAddress, out var handle));
|
|
||||||
Assert.Equal(0U, handle);
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
|
||||||
KernelSemaphoreCompatExports.PthreadSemDestroy(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(true)]
|
|
||||||
[InlineData(false)]
|
|
||||||
public void PthreadSemOperationRejectsInvalidSemaphore(bool post)
|
|
||||||
{
|
|
||||||
var context = CreateContext();
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
|
|
||||||
var result = post
|
|
||||||
? KernelSemaphoreCompatExports.PthreadSemPost(context)
|
|
||||||
: KernelSemaphoreCompatExports.PthreadSemWait(context);
|
|
||||||
|
|
||||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result);
|
|
||||||
Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT),
|
|
||||||
context[CpuRegister.Rax]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CpuContext CreateContext() => new(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
|
||||||
|
|
||||||
private static void InitializeSemaphore(CpuContext context, uint initialCount)
|
|
||||||
{
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
context[CpuRegister.Rsi] = 0;
|
|
||||||
context[CpuRegister.Rdx] = initialCount;
|
|
||||||
context[CpuRegister.Rcx] = 0;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemInit(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int ReadSemaphoreValue(CpuContext context)
|
|
||||||
{
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
context[CpuRegister.Rsi] = ValueAddress;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PosixSemGetValue(context));
|
|
||||||
Assert.True(context.TryReadUInt32(ValueAddress, out var value));
|
|
||||||
return unchecked((int)value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DestroySemaphore(CpuContext context)
|
|
||||||
{
|
|
||||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
|
||||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemDestroy(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,9 +31,6 @@ public sealed class SaveDataExportsTests : IDisposable
|
|||||||
private const ulong SyncParam = Base + 0xC80;
|
private const ulong SyncParam = Base + 0xC80;
|
||||||
private const ulong SetupParam = Base + 0xD00;
|
private const ulong SetupParam = Base + 0xD00;
|
||||||
private const ulong SetupResult = Base + 0xD80;
|
private const ulong SetupResult = Base + 0xD80;
|
||||||
private const ulong TransactionOut = Base + 0xE00;
|
|
||||||
private const ulong StaleR8 = Base + 0xE08;
|
|
||||||
private const ulong StaleR9 = Base + 0xE10;
|
|
||||||
|
|
||||||
private const int NoEvent = unchecked((int)0x809F0008);
|
private const int NoEvent = unchecked((int)0x809F0008);
|
||||||
private const int ParameterError = unchecked((int)0x809F0000);
|
private const int ParameterError = unchecked((int)0x809F0000);
|
||||||
@@ -228,64 +225,4 @@ public sealed class SaveDataExportsTests : IDisposable
|
|||||||
unchecked((int)0x809F0008),
|
unchecked((int)0x809F0008),
|
||||||
SaveDataExports.SaveDataDelete(Reg(rdi: DeleteParam)));
|
SaveDataExports.SaveDataDelete(Reg(rdi: DeleteParam)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CreateTransactionResource_WithoutOutPointer_DoesNotProbeStaleRegisters()
|
|
||||||
{
|
|
||||||
const uint sentinel = 0xA5A5A5A5;
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(TransactionOut, sentinel));
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(StaleR9, sentinel));
|
|
||||||
|
|
||||||
var ctx = Reg(rdi: UserId, rdx: 0, rcx: TransactionOut);
|
|
||||||
ctx[CpuRegister.R8] = StaleR8;
|
|
||||||
ctx[CpuRegister.R9] = StaleR9;
|
|
||||||
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataCreateTransactionResource(ctx));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var rcxValue));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(StaleR8, out var r8Value));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(StaleR9, out var r9Value));
|
|
||||||
Assert.Equal(sentinel, rcxValue);
|
|
||||||
Assert.Equal(sentinel, r8Value);
|
|
||||||
Assert.Equal(sentinel, r9Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CreateTransactionResource_WithOutPointerFlag_WritesOnlyRcx()
|
|
||||||
{
|
|
||||||
const uint sentinel = 0xA5A5A5A5;
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(TransactionOut, 0));
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(StaleR9, sentinel));
|
|
||||||
|
|
||||||
var ctx = Reg(rdi: UserId, rdx: 1, rcx: TransactionOut);
|
|
||||||
ctx[CpuRegister.R8] = StaleR8;
|
|
||||||
ctx[CpuRegister.R9] = StaleR9;
|
|
||||||
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataCreateTransactionResource(ctx));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var resource));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(StaleR8, out var r8Value));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(StaleR9, out var r9Value));
|
|
||||||
Assert.NotEqual(0u, resource);
|
|
||||||
Assert.Equal(sentinel, r8Value);
|
|
||||||
Assert.Equal(sentinel, r9Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CreateTransactionResource_WithLegacyOutPointer_WritesOnlyRdx()
|
|
||||||
{
|
|
||||||
const uint sentinel = 0xA5A5A5A5;
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(TransactionOut, 0));
|
|
||||||
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
|
|
||||||
|
|
||||||
Assert.Equal(
|
|
||||||
0,
|
|
||||||
SaveDataExports.SaveDataCreateTransactionResource(
|
|
||||||
Reg(rdi: UserId, rdx: TransactionOut, rcx: StaleR8)));
|
|
||||||
|
|
||||||
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var resource));
|
|
||||||
Assert.True(_ctx.TryReadUInt32(StaleR8, out var rcxValue));
|
|
||||||
Assert.NotEqual(0u, resource);
|
|
||||||
Assert.Equal(sentinel, rcxValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.Libs.VideoOut;
|
|
||||||
using Silk.NET.Vulkan;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
|
||||||
|
|
||||||
public sealed class VulkanPresentEncodeFormatTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Format.B8G8R8A8Unorm, Format.B8G8R8A8Srgb)]
|
|
||||||
[InlineData(Format.R8G8B8A8Unorm, Format.R8G8B8A8Srgb)]
|
|
||||||
public void UnormSwapchainFormatsHaveSrgbCounterparts(
|
|
||||||
Format swapchainFormat,
|
|
||||||
Format expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
expected,
|
|
||||||
VulkanVideoPresenter.GetSrgbCounterpart(swapchainFormat));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
// Already-sRGB swapchains encode on store; the direct blit stays.
|
|
||||||
[InlineData(Format.B8G8R8A8Srgb)]
|
|
||||||
[InlineData(Format.R8G8B8A8Srgb)]
|
|
||||||
// No same-class sRGB counterpart exists; the raw blit must remain.
|
|
||||||
[InlineData(Format.A2B10G10R10UnormPack32)]
|
|
||||||
[InlineData(Format.R16G16B16A16Sfloat)]
|
|
||||||
[InlineData(Format.R5G6B5UnormPack16)]
|
|
||||||
[InlineData(Format.Undefined)]
|
|
||||||
public void OtherSwapchainFormatsKeepTheDirectBlit(Format swapchainFormat)
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
Format.Undefined,
|
|
||||||
VulkanVideoPresenter.GetSrgbCounterpart(swapchainFormat));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Format.R16G16B16A16Sfloat)]
|
|
||||||
[InlineData(Format.R32G32B32A32Sfloat)]
|
|
||||||
public void FloatFlipSourcesNeedLinearToSrgbEncode(Format sourceFormat)
|
|
||||||
{
|
|
||||||
Assert.True(VulkanVideoPresenter.IsLinearFloatPresentSource(sourceFormat));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Format.B8G8R8A8Unorm)]
|
|
||||||
[InlineData(Format.R8G8B8A8Unorm)]
|
|
||||||
[InlineData(Format.B8G8R8A8Srgb)]
|
|
||||||
[InlineData(Format.A2B10G10R10UnormPack32)]
|
|
||||||
[InlineData(Format.B10G11R11UfloatPack32)]
|
|
||||||
[InlineData(Format.Undefined)]
|
|
||||||
public void NonFloatFlipSourcesKeepTheDirectBlit(Format sourceFormat)
|
|
||||||
{
|
|
||||||
Assert.False(VulkanVideoPresenter.IsLinearFloatPresentSource(sourceFormat));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,8 +17,6 @@
|
|||||||
// multiply-add would give 0x7A6A instead
|
// multiply-add would give 0x7A6A instead
|
||||||
// [6] the same fma with the addend negated -> 0x7A6A packed (just below the
|
// [6] the same fma with the addend negated -> 0x7A6A packed (just below the
|
||||||
// same midpoint), pinning the opposite rounding direction
|
// same midpoint), pinning the opposite rounding direction
|
||||||
// [7] the pinned fma with the clamp modifier -> 0x3C00 packed (both lanes
|
|
||||||
// exceed 1.0, so each saturates to 1.0)
|
|
||||||
// Every other word of the buffer must still hold the sentinel afterwards.
|
// Every other word of the buffer must still hold the sentinel afterwards.
|
||||||
//
|
//
|
||||||
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
||||||
@@ -47,9 +45,6 @@ var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
|
|||||||
// addend decides the rounding direction under a single fused rounding.
|
// addend decides the rounding direction under a single fused rounding.
|
||||||
const uint ExpectedPkFma = 0x7A6B_7A6B;
|
const uint ExpectedPkFma = 0x7A6B_7A6B;
|
||||||
const uint ExpectedPkFmaNeg = 0x7A6A_7A6A;
|
const uint ExpectedPkFmaNeg = 0x7A6A_7A6A;
|
||||||
// Both lanes of the pinned fma are ~52560, far above 1.0, so the clamp modifier
|
|
||||||
// saturates each to 1.0 (0x3C00 in f16).
|
|
||||||
const uint ExpectedPkFmaClamp = 0x3C00_3C00;
|
|
||||||
|
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
@@ -370,7 +365,6 @@ unsafe
|
|||||||
("store after exec restore (offset 16)", words[4], expectedRestored),
|
("store after exec restore (offset 16)", words[4], expectedRestored),
|
||||||
("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma),
|
("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma),
|
||||||
("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg),
|
("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg),
|
||||||
("v_pk_fma_f16 clamp saturates to 1.0", words[7], ExpectedPkFmaClamp),
|
|
||||||
};
|
};
|
||||||
var failures = 0;
|
var failures = 0;
|
||||||
foreach (var (name, actual, expected) in results)
|
foreach (var (name, actual, expected) in results)
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ const ulong ProgramAddress = 0x100000;
|
|||||||
0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1
|
0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1
|
||||||
0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1
|
0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1
|
||||||
0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2
|
0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2
|
||||||
0xCC0FC009, 0x18020000, // v_pk_add_f16 v9, v0, v0 clamp (2.5+2.5=5 -> saturates to 1.0)
|
|
||||||
0xCC0EC40A, 0x1C0A0300, // v_pk_fma_f16 v10, v0, v1, v2 clamp
|
|
||||||
0xBF810000, // s_endpgm
|
0xBF810000, // s_endpgm
|
||||||
]),
|
]),
|
||||||
("mrt", true, [
|
("mrt", true, [
|
||||||
@@ -160,10 +158,8 @@ const ulong ProgramAddress = 0x100000;
|
|||||||
0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed)
|
0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed)
|
||||||
0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9
|
0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9
|
||||||
0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9
|
0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9
|
||||||
0xCC0EC00C, 0x1C261107, // v_pk_fma_f16 v12, v7, v8, v9 clamp (>=1 -> saturates to 1.0)
|
|
||||||
0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20
|
0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20
|
||||||
0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24
|
0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24
|
||||||
0xE070001C, 0x80020C00, // buffer_store_dword v12, off, s[8:11], 0 offset:28
|
|
||||||
0xBF810000, // s_endpgm
|
0xBF810000, // s_endpgm
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user