mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 640878adfa | |||
| faf3a397a8 | |||
| 488b285ecb | |||
| 1a4a2902d4 | |||
| f544146d6d | |||
| dbd74654c4 | |||
| 28485b60e2 | |||
| 6b1abc1a38 | |||
| 7494792249 | |||
| 3585519007 | |||
| 8ee0fe8e44 | |||
| 0755ca15f7 | |||
| 74c70cc2c1 | |||
| 73e0ebf446 | |||
| 459ae7e3f7 | |||
| 8f405caebe | |||
| dabf723b3e | |||
| 2129a12684 | |||
| 2db1fae282 | |||
| 33f96252da | |||
| f7981a7ed7 | |||
| e10efa3ae1 | |||
| 16a2131b67 | |||
| 9883a9445d | |||
| f2d9051358 | |||
| 53d414e096 | |||
| 52d2874fa8 | |||
| c5a82c1065 | |||
| a8be1daca4 | |||
| 26dbad8ac8 | |||
| 2196a1f786 | |||
| 8db2c0622d | |||
| 53da00fa89 | |||
| b4b95014f1 | |||
| 7b91c964fc | |||
| 9bacb883f1 | |||
| 864cbb0fa0 | |||
| ad5a7d3799 |
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -7,6 +7,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
- "**/*.png"
|
- "**/*.png"
|
||||||
@@ -53,6 +55,11 @@ jobs:
|
|||||||
release_tag="v${version}"
|
release_tag="v${version}"
|
||||||
release_name="SharpEmu v${version}"
|
release_name="SharpEmu v${version}"
|
||||||
|
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "${GITHUB_REF_NAME}" != "${release_tag}" ]; then
|
||||||
|
echo "Release tag ${GITHUB_REF_NAME} does not match project version ${release_tag}." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "short-sha=${short_sha}"
|
echo "short-sha=${short_sha}"
|
||||||
echo "safe-ref=${safe_ref}"
|
echo "safe-ref=${safe_ref}"
|
||||||
@@ -118,7 +125,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||||
|
|
||||||
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64-${{ needs.init.outputs.short-sha }}.zip"
|
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||||
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||||
if (Test-Path $archivePath) {
|
if (Test-Path $archivePath) {
|
||||||
Remove-Item $archivePath -Force
|
Remove-Item $archivePath -Force
|
||||||
@@ -130,7 +137,7 @@ jobs:
|
|||||||
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.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64-${{ needs.init.outputs.short-sha }}.zip
|
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-posix:
|
build-posix:
|
||||||
@@ -187,14 +194,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p "$RELEASE_DIR"
|
mkdir -p "$RELEASE_DIR"
|
||||||
# tar keeps the executable bit, which zip would drop.
|
# tar keeps the executable bit, which zip would drop.
|
||||||
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz" \
|
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||||
-C "$PUBLISH_DIR" .
|
-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.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
|
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -203,7 +210,9 @@ jobs:
|
|||||||
- init
|
- init
|
||||||
- build
|
- build
|
||||||
- build-posix
|
- build-posix
|
||||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
# Versioned releases are immutable and tag-driven. Branch and manual runs
|
||||||
|
# still produce Actions artifacts without modifying a published release.
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -213,7 +222,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Create or update release
|
- name: Create release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_REPO: ${{ github.repository }}
|
GH_REPO: ${{ github.repository }}
|
||||||
@@ -231,11 +240,11 @@ jobs:
|
|||||||
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
|
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
|
||||||
|
|
||||||
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||||
gh release upload "${RELEASE_TAG}" "${assets[@]}" --clobber
|
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
||||||
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
exit 1
|
||||||
else
|
|
||||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
|
||||||
--title "${RELEASE_NAME}" \
|
|
||||||
--notes "${notes}" \
|
|
||||||
--target "${GITHUB_SHA}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||||
|
--verify-tag \
|
||||||
|
--title "${RELEASE_NAME}" \
|
||||||
|
--notes "${notes}"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<SharpEmuVersion>0.0.1</SharpEmuVersion>
|
<SharpEmuVersion>0.0.2</SharpEmuVersion>
|
||||||
<Version>$(SharpEmuVersion)</Version>
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ This project is developed purely for research and educational purposes. There ar
|
|||||||
SharpEmu focuses exclusively on the PlayStation 5.
|
SharpEmu focuses exclusively on the PlayStation 5.
|
||||||
Our goal is **not** to emulate PS4 games, as there is already an excellent emulator dedicated to that platform: **ShadPS4**.
|
Our goal is **not** to emulate PS4 games, as there is already an excellent emulator dedicated to that platform: **ShadPS4**.
|
||||||
|
|
||||||
|
## Games Tested
|
||||||
|
|
||||||
|
| Demons Souls Remake | Dreaming Sarah |
|
||||||
|
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
| Void Terrarium | Dead Cells |
|
||||||
|
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
|
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
|
||||||
@@ -88,25 +98,6 @@ chmod +x ./SharpEmu
|
|||||||
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||||
release includes the MoltenVK Vulkan implementation.
|
release includes the MoltenVK Vulkan implementation.
|
||||||
|
|
||||||
## Games Tested
|
|
||||||
|
|
||||||
* **Demon's Souls Remake**
|
|
||||||
* [Demon's Souls [PPSA01341]](https://github.com/sharpemu/sharpemu/issues/2)
|
|
||||||
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this.
|
|
||||||

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

|
|
||||||
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> This project does **not** support or condone piracy.
|
> This project does **not** support or condone piracy.
|
||||||
> All games used during development and testing are dumped from consoles that we personally own.
|
> All games used during development and testing are dumped from consoles that we personally own.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 653 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,46 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Bink 2 bridge
|
||||||
|
|
||||||
|
Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
|
||||||
|
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||||
|
decoder cannot observe or replace those frames.
|
||||||
|
|
||||||
|
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
|
||||||
|
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||||
|
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||||
|
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||||
|
|
||||||
|
Without an adapter, Bink movies are skipped by default: their open call returns
|
||||||
|
not-found so games that mark cinematics as optional progress to their next
|
||||||
|
state instead of waiting on an empty Bink GPU texture.
|
||||||
|
|
||||||
|
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
||||||
|
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||||
|
only; it does not decode the movie or alter its game logic. Set
|
||||||
|
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||||
|
|
||||||
|
## Supplying the adapter
|
||||||
|
|
||||||
|
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
|
||||||
|
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
|
||||||
|
The adapter deliberately contains only a three-function C ABI so the managed
|
||||||
|
emulator never depends on RAD's private binary ABI.
|
||||||
|
|
||||||
|
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||||
|
executable, or point to it explicitly:
|
||||||
|
|
||||||
|
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||||
|
./SharpEmu /path/to/eboot.bin
|
||||||
|
|
||||||
|
The expected exports are sharpemu_bink2_open_utf8,
|
||||||
|
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||||
|
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||||
|
frame. The managed side validates dimensions and retains ownership of the
|
||||||
|
destination buffer.
|
||||||
|
|
||||||
|
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||||
|
and retains the existing guest rendering path.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*
|
||||||
|
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
|
||||||
|
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "bink.h"
|
||||||
|
|
||||||
|
typedef struct sharpemu_bink2_info {
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint32_t frames_per_second_numerator;
|
||||||
|
uint32_t frames_per_second_denominator;
|
||||||
|
} sharpemu_bink2_info;
|
||||||
|
|
||||||
|
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
|
||||||
|
HBINK bink;
|
||||||
|
if (!path || !movie || !info) return 0;
|
||||||
|
|
||||||
|
bink = BinkOpen(path, 0);
|
||||||
|
if (!bink) return 0;
|
||||||
|
|
||||||
|
*movie = bink;
|
||||||
|
info->width = bink->Width;
|
||||||
|
info->height = bink->Height;
|
||||||
|
info->frames_per_second_numerator = bink->FrameRate;
|
||||||
|
info->frames_per_second_denominator = bink->FrameRateDiv;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
|
||||||
|
uint32_t stride, uint32_t destination_bytes) {
|
||||||
|
uint64_t needed;
|
||||||
|
if (!movie || !destination || stride < movie->Width * 4) return 0;
|
||||||
|
needed = (uint64_t)stride * movie->Height;
|
||||||
|
if (needed > destination_bytes) return 0;
|
||||||
|
|
||||||
|
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
|
||||||
|
if (BinkWait(movie)) return 0;
|
||||||
|
BinkDoFrame(movie);
|
||||||
|
BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA);
|
||||||
|
BinkNextFrame(movie);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sharpemu_bink2_close(HBINK movie) {
|
||||||
|
if (movie) BinkClose(movie);
|
||||||
|
}
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
|
||||||
|
VERSION_ELEMENT_PATTERN = re.compile(
|
||||||
|
r"(<SharpEmuVersion>)([^<]+)(</SharpEmuVersion>)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(
|
||||||
|
*args: str,
|
||||||
|
cwd: Path,
|
||||||
|
capture_output: bool = False,
|
||||||
|
) -> str:
|
||||||
|
command = ["git", *args]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=cwd,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=capture_output,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise ReleaseError("Git was not found in PATH.") from None
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
stderr = error.stderr.strip() if error.stderr else ""
|
||||||
|
detail = f"\n{stderr}" if stderr else ""
|
||||||
|
|
||||||
|
raise ReleaseError(
|
||||||
|
f"Git command failed: {' '.join(command)}{detail}"
|
||||||
|
) from error
|
||||||
|
|
||||||
|
return result.stdout.strip() if capture_output else ""
|
||||||
|
|
||||||
|
|
||||||
|
def find_repository_root(script_path: Path) -> Path:
|
||||||
|
root = run_git(
|
||||||
|
"rev-parse",
|
||||||
|
"--show-toplevel",
|
||||||
|
cwd=script_path.resolve().parent,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Path(root)
|
||||||
|
|
||||||
|
|
||||||
|
def get_status(repository_root: Path) -> str:
|
||||||
|
return run_git(
|
||||||
|
"status",
|
||||||
|
"--porcelain",
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_clean_worktree(repository_root: Path) -> None:
|
||||||
|
status = get_status(repository_root)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
raise ReleaseError(
|
||||||
|
"The working tree is not clean.\n\n"
|
||||||
|
f"{status}\n\n"
|
||||||
|
"Commit, stash, or remove these changes first."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_branch(repository_root: Path) -> str:
|
||||||
|
branch = run_git(
|
||||||
|
"branch",
|
||||||
|
"--show-current",
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not branch:
|
||||||
|
raise ReleaseError(
|
||||||
|
"HEAD is detached. Switch to a branch before continuing."
|
||||||
|
)
|
||||||
|
|
||||||
|
return branch
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_branch_does_not_exist(
|
||||||
|
repository_root: Path,
|
||||||
|
branch: str,
|
||||||
|
remote: str,
|
||||||
|
) -> None:
|
||||||
|
local_branch = run_git(
|
||||||
|
"branch",
|
||||||
|
"--list",
|
||||||
|
branch,
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if local_branch:
|
||||||
|
raise ReleaseError(f"Branch {branch} already exists locally.")
|
||||||
|
|
||||||
|
remote_branch = run_git(
|
||||||
|
"ls-remote",
|
||||||
|
"--heads",
|
||||||
|
remote,
|
||||||
|
branch,
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if remote_branch:
|
||||||
|
raise ReleaseError(
|
||||||
|
f"Branch {branch} already exists on {remote}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tag_does_not_exist(
|
||||||
|
repository_root: Path,
|
||||||
|
tag: str,
|
||||||
|
remote: str,
|
||||||
|
) -> None:
|
||||||
|
local_tag = run_git(
|
||||||
|
"tag",
|
||||||
|
"--list",
|
||||||
|
tag,
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if local_tag:
|
||||||
|
raise ReleaseError(f"Tag {tag} already exists locally.")
|
||||||
|
|
||||||
|
remote_tag = run_git(
|
||||||
|
"ls-remote",
|
||||||
|
"--tags",
|
||||||
|
remote,
|
||||||
|
f"refs/tags/{tag}",
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if remote_tag:
|
||||||
|
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
|
||||||
|
|
||||||
|
|
||||||
|
def read_version(props_path: Path) -> str:
|
||||||
|
if not props_path.exists():
|
||||||
|
raise ReleaseError(f"Version file not found: {props_path}")
|
||||||
|
|
||||||
|
content = props_path.read_text(encoding="utf-8")
|
||||||
|
match = VERSION_ELEMENT_PATTERN.search(content)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
raise ReleaseError(
|
||||||
|
f"SharpEmuVersion was not found in {props_path.name}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return match.group(2).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def update_version(props_path: Path, version: str) -> str:
|
||||||
|
content = props_path.read_text(encoding="utf-8")
|
||||||
|
current_version = read_version(props_path)
|
||||||
|
|
||||||
|
if current_version == version:
|
||||||
|
raise ReleaseError(
|
||||||
|
f"SharpEmuVersion is already set to {version}."
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_content, replacement_count = VERSION_ELEMENT_PATTERN.subn(
|
||||||
|
rf"\g<1>{version}\g<3>",
|
||||||
|
content,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if replacement_count != 1:
|
||||||
|
raise ReleaseError(
|
||||||
|
"Expected exactly one SharpEmuVersion element."
|
||||||
|
)
|
||||||
|
|
||||||
|
props_path.write_text(
|
||||||
|
updated_content,
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
return current_version
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_release(
|
||||||
|
repository_root: Path,
|
||||||
|
props_path: Path,
|
||||||
|
version: str,
|
||||||
|
remote: str,
|
||||||
|
) -> None:
|
||||||
|
ensure_clean_worktree(repository_root)
|
||||||
|
|
||||||
|
current_branch = get_current_branch(repository_root)
|
||||||
|
|
||||||
|
if current_branch != "main":
|
||||||
|
raise ReleaseError(
|
||||||
|
f"Prepare must be run from main, not {current_branch}."
|
||||||
|
)
|
||||||
|
|
||||||
|
run_git(
|
||||||
|
"pull",
|
||||||
|
"--ff-only",
|
||||||
|
remote,
|
||||||
|
"main",
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
branch = f"release/{version}"
|
||||||
|
ensure_branch_does_not_exist(
|
||||||
|
repository_root,
|
||||||
|
branch,
|
||||||
|
remote,
|
||||||
|
)
|
||||||
|
|
||||||
|
previous_version = read_version(props_path)
|
||||||
|
|
||||||
|
run_git(
|
||||||
|
"switch",
|
||||||
|
"-c",
|
||||||
|
branch,
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
update_version(props_path, version)
|
||||||
|
|
||||||
|
relative_props_path = props_path.relative_to(repository_root)
|
||||||
|
|
||||||
|
run_git(
|
||||||
|
"add",
|
||||||
|
relative_props_path.as_posix(),
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
run_git(
|
||||||
|
"commit",
|
||||||
|
"-m",
|
||||||
|
f"chore: bump version to {version}",
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
run_git(
|
||||||
|
"push",
|
||||||
|
"-u",
|
||||||
|
remote,
|
||||||
|
branch,
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
print(
|
||||||
|
"\nPrepare failed. The release branch may still exist locally.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Prepared release {previous_version} -> {version}")
|
||||||
|
print(f"Branch pushed: {branch}")
|
||||||
|
print()
|
||||||
|
print("Open a pull request from:")
|
||||||
|
print(f" {branch}")
|
||||||
|
print("into:")
|
||||||
|
print(" main")
|
||||||
|
print()
|
||||||
|
print("After merging the PR, run:")
|
||||||
|
print(f" python scripts/release.py tag {version}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_release_tag(
|
||||||
|
repository_root: Path,
|
||||||
|
props_path: Path,
|
||||||
|
version: str,
|
||||||
|
remote: str,
|
||||||
|
) -> None:
|
||||||
|
ensure_clean_worktree(repository_root)
|
||||||
|
|
||||||
|
current_branch = get_current_branch(repository_root)
|
||||||
|
|
||||||
|
if current_branch != "main":
|
||||||
|
raise ReleaseError(
|
||||||
|
f"Tagging must be run from main, not {current_branch}."
|
||||||
|
)
|
||||||
|
|
||||||
|
run_git(
|
||||||
|
"pull",
|
||||||
|
"--ff-only",
|
||||||
|
remote,
|
||||||
|
"main",
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
current_version = read_version(props_path)
|
||||||
|
|
||||||
|
if current_version != version:
|
||||||
|
raise ReleaseError(
|
||||||
|
"Version mismatch:\n"
|
||||||
|
f" Directory.Build.props: {current_version}\n"
|
||||||
|
f" Requested tag: {version}"
|
||||||
|
)
|
||||||
|
|
||||||
|
tag = f"v{version}"
|
||||||
|
|
||||||
|
ensure_tag_does_not_exist(
|
||||||
|
repository_root,
|
||||||
|
tag,
|
||||||
|
remote,
|
||||||
|
)
|
||||||
|
|
||||||
|
run_git(
|
||||||
|
"tag",
|
||||||
|
"-a",
|
||||||
|
tag,
|
||||||
|
"-m",
|
||||||
|
f"SharpEmu {version}",
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
run_git(
|
||||||
|
"push",
|
||||||
|
remote,
|
||||||
|
tag,
|
||||||
|
cwd=repository_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Successfully pushed tag {tag}.")
|
||||||
|
print("The release workflow should start automatically.")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Prepare or tag a SharpEmu release."
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
dest="command",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for command in ("prepare", "tag"):
|
||||||
|
subparser = subparsers.add_parser(command)
|
||||||
|
subparser.add_argument(
|
||||||
|
"version",
|
||||||
|
help="Version without the v prefix, e.g. 0.0.2-beta.2.",
|
||||||
|
)
|
||||||
|
subparser.add_argument(
|
||||||
|
"--remote",
|
||||||
|
default="origin",
|
||||||
|
help="Git remote. Default: origin.",
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
|
if not VERSION_PATTERN.fullmatch(arguments.version):
|
||||||
|
parser.error(
|
||||||
|
"Version must look like 0.0.2, "
|
||||||
|
"0.0.2-beta.2, or 0.0.2-rc.1."
|
||||||
|
)
|
||||||
|
|
||||||
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
arguments = parse_arguments()
|
||||||
|
|
||||||
|
try:
|
||||||
|
repository_root = find_repository_root(Path(__file__))
|
||||||
|
props_path = repository_root / "Directory.Build.props"
|
||||||
|
|
||||||
|
if arguments.command == "prepare":
|
||||||
|
prepare_release(
|
||||||
|
repository_root,
|
||||||
|
props_path,
|
||||||
|
arguments.version,
|
||||||
|
arguments.remote,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
create_release_tag(
|
||||||
|
repository_root,
|
||||||
|
props_path,
|
||||||
|
arguments.version,
|
||||||
|
arguments.remote,
|
||||||
|
)
|
||||||
|
except ReleaseError as error:
|
||||||
|
print(f"Error: {error}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -45,6 +45,11 @@ internal static partial class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// Avoid blocking full collections while guest and render threads are
|
||||||
|
// running, and establish the GC mode before the runtime reserves the
|
||||||
|
// fixed guest address-space window.
|
||||||
|
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Run(args);
|
return Run(args);
|
||||||
@@ -58,6 +63,11 @@ internal static partial class Program
|
|||||||
|
|
||||||
private static int Run(string[] args)
|
private static int Run(string[] args)
|
||||||
{
|
{
|
||||||
|
if (Updater.TryApply(args, out var updateExitCode))
|
||||||
|
{
|
||||||
|
return updateExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
if (args.Length == 0 && !isMitigatedChild)
|
if (args.Length == 0 && !isMitigatedChild)
|
||||||
{
|
{
|
||||||
@@ -84,6 +94,7 @@ internal static partial class Program
|
|||||||
{
|
{
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
|
ConfigureMoltenVkDefaults();
|
||||||
PreloadMacVulkanLoader();
|
PreloadMacVulkanLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +162,26 @@ internal static partial class Program
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies MoltenVK performance defaults before the Vulkan loader is
|
||||||
|
/// loaded. Existing user-provided values always take precedence.
|
||||||
|
/// </summary>
|
||||||
|
private static void ConfigureMoltenVkDefaults()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0);
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION", "1", 0);
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0);
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_RESUME_LOST_DEVICE", "1", 0);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Failed to set MoltenVK defaults: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
||||||
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
||||||
@@ -1319,4 +1350,7 @@ internal static partial class Program
|
|||||||
uint creationDisposition,
|
uint creationDisposition,
|
||||||
uint flagsAndAttributes,
|
uint flagsAndAttributes,
|
||||||
nint templateFile);
|
nint templateFile);
|
||||||
|
|
||||||
|
[DllImport("libSystem", EntryPoint = "setenv")]
|
||||||
|
private static extern int MacSetEnv(string name, string value, int overwrite);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<!-- Server GC reserves one large heap range per logical processor. On
|
||||||
|
high-core-count hosts those ranges can overlap fixed PS5 image bases
|
||||||
|
before the guest address space is established. Workstation GC avoids
|
||||||
|
that collision and keeps the emulator's required mappings available. -->
|
||||||
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<TieredPGO>true</TieredPGO>
|
<TieredPGO>true</TieredPGO>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
|
||||||
|
|
||||||
private enum EntryFrameKind
|
private enum EntryFrameKind
|
||||||
{
|
{
|
||||||
ProcessEntry,
|
ProcessEntry,
|
||||||
@@ -31,9 +27,9 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||||
private const ulong TlsSize = 0x0001_0000UL;
|
private const ulong TlsSize = 0x0001_0000UL;
|
||||||
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
|
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
|
||||||
// amd64 variant II); libc.prx alone reaches beyond -0x1700, so give the
|
// amd64 variant II). Keep every host in sync with GuestTlsTemplate's
|
||||||
// prefix a full 64KB on POSIX. Windows keeps its historical 4KB prefix.
|
// startup reservation; PS5 modules routinely reach beyond one host page.
|
||||||
private static readonly ulong TlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
|
private const ulong TlsPrefixSize = GuestTlsTemplate.StartupStaticTlsReservation;
|
||||||
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
|
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
|
||||||
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
|
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
|
||||||
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
|
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
|
||||||
@@ -49,19 +45,16 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
];
|
];
|
||||||
private readonly IVirtualMemory _virtualMemory;
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
private readonly IModuleManager _moduleManager;
|
private readonly IModuleManager _moduleManager;
|
||||||
private readonly IHostPlatform? _hostPlatform;
|
|
||||||
private INativeCpuBackend? _nativeCpuBackend;
|
private INativeCpuBackend? _nativeCpuBackend;
|
||||||
|
|
||||||
public CpuDispatcher(
|
public CpuDispatcher(
|
||||||
IVirtualMemory virtualMemory,
|
IVirtualMemory virtualMemory,
|
||||||
IModuleManager moduleManager,
|
IModuleManager moduleManager,
|
||||||
INativeCpuBackend? nativeCpuBackend = null,
|
INativeCpuBackend? nativeCpuBackend = null)
|
||||||
IHostPlatform? hostPlatform = null)
|
|
||||||
{
|
{
|
||||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
_nativeCpuBackend = nativeCpuBackend;
|
_nativeCpuBackend = nativeCpuBackend;
|
||||||
_hostPlatform = hostPlatform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong? LastEntryPoint { get; private set; }
|
public ulong? LastEntryPoint { get; private set; }
|
||||||
@@ -94,8 +87,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string processImageName = "eboot.bin",
|
string processImageName = "eboot.bin",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchEntry START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -103,7 +96,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +110,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string moduleName = "module",
|
string moduleName = "module",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -132,7 +126,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +141,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
CpuExecutionOptions executionOptions = default,
|
CpuExecutionOptions executionOptions = default,
|
||||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||||
{
|
{
|
||||||
Log.Debug("DispatchEntryCore STARTING...");
|
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
LastEntryPoint = entryPoint;
|
LastEntryPoint = entryPoint;
|
||||||
LastTrapInfo = null;
|
LastTrapInfo = null;
|
||||||
@@ -277,7 +272,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
entryFrameDiagnostic,
|
entryFrameDiagnostic,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
"CpuEngine: native-only");
|
"CpuEngine: native-only");
|
||||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||||
if (_nativeCpuBackend.TryExecute(
|
if (_nativeCpuBackend.TryExecute(
|
||||||
context,
|
context,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
@@ -318,7 +313,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
LastMilestoneLog,
|
LastMilestoneLog,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
$"CpuEngine native-only failed: {backendError}");
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
Log.Error($"Native backend FAILED: {backendError}");
|
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||||
return FailEarly(
|
return FailEarly(
|
||||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
CpuExitReason.NativeBackendUnavailable);
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
@@ -377,11 +372,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
|
|
||||||
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
||||||
{
|
{
|
||||||
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
|
if (!context.TryWriteUInt64(tlsBase - 0xF0, 0) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
|
!context.TryWriteUInt64(tlsBase + 0x00, tlsBase) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
|
!context.TryWriteUInt64(tlsBase + 0x10, tlsBase) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
|
!context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBA00UL) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
|
!context.TryWriteUInt64(tlsBase + 0x60, tlsBase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed the static TLS block below the thread pointer with the main
|
||||||
|
// module's initialized thread-locals (variant II layout).
|
||||||
|
SharpEmu.HLE.GuestTlsTemplate.SeedThreadBlock(context, tlsBase);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
||||||
@@ -407,35 +410,54 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
ulong programExitHandlerAddress)
|
ulong programExitHandlerAddress)
|
||||||
{
|
{
|
||||||
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
|
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
|
||||||
var encodedNameLength = Encoding.UTF8.GetByteCount(imageName);
|
var arguments = new List<string>(3) { imageName };
|
||||||
Span<byte> argv0Buffer = encodedNameLength + 1 <= 512
|
var configuredArguments = Environment.GetEnvironmentVariable("SHARPEMU_GUEST_ARGS");
|
||||||
? stackalloc byte[encodedNameLength + 1]
|
if (!string.IsNullOrWhiteSpace(configuredArguments))
|
||||||
: new byte[encodedNameLength + 1];
|
|
||||||
if (Encoding.UTF8.GetBytes(imageName.AsSpan(), argv0Buffer) != encodedNameLength)
|
|
||||||
{
|
{
|
||||||
return false;
|
// The PS5 entry-parameter ABI exposes three inline argv pointers.
|
||||||
|
// Two compatibility arguments are therefore safe without changing
|
||||||
|
// the fixed 0x20-byte structure expected by existing titles.
|
||||||
|
var compatibilityArguments = configuredArguments.Split(
|
||||||
|
(char[]?)null,
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
arguments.AddRange(compatibilityArguments.Take(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
argv0Buffer[encodedNameLength] = 0;
|
|
||||||
var cursor = context[CpuRegister.Rsp];
|
var cursor = context[CpuRegister.Rsp];
|
||||||
|
var argumentAddresses = new ulong[arguments.Count];
|
||||||
var argv0Address = AlignDown(cursor - (ulong)argv0Buffer.Length, 16);
|
for (var index = arguments.Count - 1; index >= 0; index--)
|
||||||
if (!context.Memory.TryWrite(argv0Address, argv0Buffer))
|
|
||||||
{
|
{
|
||||||
return false;
|
var encoded = Encoding.UTF8.GetBytes(arguments[index] + '\0');
|
||||||
|
cursor = AlignDown(cursor - (ulong)encoded.Length, 16);
|
||||||
|
if (!context.Memory.TryWrite(cursor, encoded))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
argumentAddresses[index] = cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ulong entryParamsSize = 0x20;
|
const ulong entryParamsSize = 0x20;
|
||||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
var entryParamsAddress = AlignDown(cursor - entryParamsSize, 16);
|
||||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, (uint)arguments.Count) ||
|
||||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x08, argumentAddresses[0]) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
!context.TryWriteUInt64(
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
entryParamsAddress + 0x10,
|
||||||
|
argumentAddresses.Length > 1 ? argumentAddresses[1] : 0) ||
|
||||||
|
!context.TryWriteUInt64(
|
||||||
|
entryParamsAddress + 0x18,
|
||||||
|
argumentAddresses.Length > 2 ? argumentAddresses[2] : 0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arguments.Count > 1)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[DISPATCHER] Guest arguments: {string.Join(' ', arguments.Skip(1))}");
|
||||||
|
}
|
||||||
|
|
||||||
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
||||||
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
||||||
{
|
{
|
||||||
@@ -468,6 +490,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
return value & ~(alignment - 1);
|
return value & ~(alignment - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return context.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildEntryFrameDiagnostic(
|
private static string BuildEntryFrameDiagnostic(
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Emulation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register
|
||||||
|
/// bit-manipulation instructions.
|
||||||
|
///
|
||||||
|
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's
|
||||||
|
/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises
|
||||||
|
/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the
|
||||||
|
/// register-only arithmetic so the exception handler can finish the instruction in software and
|
||||||
|
/// resume, instead of aborting the title.
|
||||||
|
///
|
||||||
|
/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the
|
||||||
|
/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the
|
||||||
|
/// backend adapter. Each method returns the result already masked to the operand width and, where
|
||||||
|
/// the instruction is defined to touch flags, updates <paramref name="eflags"/> in place. Flags
|
||||||
|
/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays
|
||||||
|
/// deterministic across hosts.
|
||||||
|
/// </summary>
|
||||||
|
public static class BmiInstructionEmulator
|
||||||
|
{
|
||||||
|
private const uint FlagCarry = 1u << 0;
|
||||||
|
private const uint FlagZero = 1u << 6;
|
||||||
|
private const uint FlagSign = 1u << 7;
|
||||||
|
private const uint FlagOverflow = 1u << 11;
|
||||||
|
|
||||||
|
private static ulong WidthMask(GprOperandSize size) =>
|
||||||
|
size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL;
|
||||||
|
|
||||||
|
private static int WidthBits(GprOperandSize size) => (int)size;
|
||||||
|
|
||||||
|
private static bool SignSet(ulong value, GprOperandSize size) =>
|
||||||
|
size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0;
|
||||||
|
|
||||||
|
private static uint WithFlag(uint eflags, uint flag, bool set) =>
|
||||||
|
set ? eflags | flag : eflags & ~flag;
|
||||||
|
|
||||||
|
// Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow
|
||||||
|
// the result, and the caller supplies CF because each instruction defines it differently.
|
||||||
|
private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry)
|
||||||
|
{
|
||||||
|
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||||
|
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||||
|
eflags = WithFlag(eflags, FlagSign, SignSet(result, size));
|
||||||
|
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||||
|
return eflags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>ANDN: <c>dest = (~src1) & src2</c>. CF and OF are cleared.</summary>
|
||||||
|
public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var result = (~src1 & src2) & WidthMask(size);
|
||||||
|
eflags = ApplyLogicFlags(eflags, result, size, carry: false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>BLSI: isolate the lowest set bit, <c>dest = (-src) & src</c>. CF = (src != 0).</summary>
|
||||||
|
public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var s = src & mask;
|
||||||
|
var result = ((0UL - s) & s) & mask;
|
||||||
|
eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>BLSMSK: mask up to and including the lowest set bit, <c>dest = (src - 1) ^ src</c>. CF = (src == 0).</summary>
|
||||||
|
public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var s = src & mask;
|
||||||
|
var result = ((s - 1) ^ s) & mask;
|
||||||
|
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>BLSR: reset the lowest set bit, <c>dest = (src - 1) & src</c>. CF = (src == 0).</summary>
|
||||||
|
public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var s = src & mask;
|
||||||
|
var result = ((s - 1) & s) & mask;
|
||||||
|
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BEXTR: extract <c>len</c> bits of <paramref name="src"/> starting at bit <c>start</c>, where
|
||||||
|
/// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var start = (int)(control & 0xFF);
|
||||||
|
var length = (int)((control >> 8) & 0xFF);
|
||||||
|
|
||||||
|
ulong result;
|
||||||
|
if (start >= bits)
|
||||||
|
{
|
||||||
|
result = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var shifted = (src & WidthMask(size)) >> start;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
result = 0;
|
||||||
|
}
|
||||||
|
else if (length >= bits)
|
||||||
|
{
|
||||||
|
result = shifted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = shifted & ((1UL << length) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result &= WidthMask(size);
|
||||||
|
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||||
|
eflags = WithFlag(eflags, FlagCarry, false);
|
||||||
|
eflags = WithFlag(eflags, FlagOverflow, false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BZHI: zero the bits of <paramref name="src"/> from bit position <c>index[7:0]</c> upward.
|
||||||
|
/// CF is set when the requested position is at or beyond the operand width.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var s = src & mask;
|
||||||
|
var n = (int)(index & 0xFF);
|
||||||
|
|
||||||
|
ulong result;
|
||||||
|
bool carry;
|
||||||
|
if (n >= bits)
|
||||||
|
{
|
||||||
|
result = s;
|
||||||
|
carry = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = s & ((1UL << n) - 1);
|
||||||
|
carry = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
eflags = ApplyLogicFlags(eflags, result, size, carry);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||||
|
public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var s = src & WidthMask(size);
|
||||||
|
|
||||||
|
ulong result;
|
||||||
|
bool carry;
|
||||||
|
if (s == 0)
|
||||||
|
{
|
||||||
|
result = (ulong)bits;
|
||||||
|
carry = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = (ulong)(size == GprOperandSize.Bits64
|
||||||
|
? BitOperations.TrailingZeroCount(s)
|
||||||
|
: BitOperations.TrailingZeroCount((uint)s));
|
||||||
|
carry = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||||
|
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set.</summary>
|
||||||
|
public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var s = src & WidthMask(size);
|
||||||
|
|
||||||
|
ulong result;
|
||||||
|
bool carry;
|
||||||
|
if (s == 0)
|
||||||
|
{
|
||||||
|
result = (ulong)bits;
|
||||||
|
carry = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = (ulong)(size == GprOperandSize.Bits64
|
||||||
|
? BitOperations.LeadingZeroCount(s)
|
||||||
|
: BitOperations.LeadingZeroCount((uint)s));
|
||||||
|
carry = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
eflags = WithFlag(eflags, FlagCarry, carry);
|
||||||
|
eflags = WithFlag(eflags, FlagZero, result == 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>RORX: rotate <paramref name="src"/> right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||||
|
public static ulong Rorx(ulong src, int count, GprOperandSize size)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var s = src & mask;
|
||||||
|
var rotate = count & (bits - 1);
|
||||||
|
if (rotate == 0)
|
||||||
|
{
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((s >> rotate) | (s << (bits - rotate))) & mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SARX: arithmetic shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||||
|
public static ulong Sarx(ulong src, int count, GprOperandSize size)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var shift = count & (bits - 1);
|
||||||
|
if (size == GprOperandSize.Bits64)
|
||||||
|
{
|
||||||
|
return (ulong)((long)src >> shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ulong)(uint)((int)(uint)src >> shift) & mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SHLX: logical shift left by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||||
|
public static ulong Shlx(ulong src, int count, GprOperandSize size)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var shift = count & (bits - 1);
|
||||||
|
return (src << shift) & mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SHRX: logical shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
|
||||||
|
public static ulong Shrx(ulong src, int count, GprOperandSize size)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var mask = WidthMask(size);
|
||||||
|
var s = src & mask;
|
||||||
|
var shift = count & (bits - 1);
|
||||||
|
return s >> shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>PDEP: deposit contiguous low bits of <paramref name="src"/> into the positions selected by <paramref name="mask"/>. No flags.</summary>
|
||||||
|
public static ulong Pdep(ulong src, ulong mask, GprOperandSize size)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var selector = mask & WidthMask(size);
|
||||||
|
ulong result = 0;
|
||||||
|
var bit = 0;
|
||||||
|
for (var i = 0; i < bits; i++)
|
||||||
|
{
|
||||||
|
var position = 1UL << i;
|
||||||
|
if ((selector & position) != 0)
|
||||||
|
{
|
||||||
|
if (((src >> bit) & 1UL) != 0)
|
||||||
|
{
|
||||||
|
result |= position;
|
||||||
|
}
|
||||||
|
|
||||||
|
bit++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result & WidthMask(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>PEXT: gather the bits of <paramref name="src"/> selected by <paramref name="mask"/> into contiguous low bits. No flags.</summary>
|
||||||
|
public static ulong Pext(ulong src, ulong mask, GprOperandSize size)
|
||||||
|
{
|
||||||
|
var bits = WidthBits(size);
|
||||||
|
var selector = mask & WidthMask(size);
|
||||||
|
ulong result = 0;
|
||||||
|
var bit = 0;
|
||||||
|
for (var i = 0; i < bits; i++)
|
||||||
|
{
|
||||||
|
if ((selector & (1UL << i)) != 0)
|
||||||
|
{
|
||||||
|
if (((src >> i) & 1UL) != 0)
|
||||||
|
{
|
||||||
|
result |= 1UL << bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
bit++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result & WidthMask(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Emulation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Operand width for the emulated general-purpose-register instructions. The numeric value is the
|
||||||
|
/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result.
|
||||||
|
/// </summary>
|
||||||
|
public enum GprOperandSize
|
||||||
|
{
|
||||||
|
Bits32 = 32,
|
||||||
|
Bits64 = 64,
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -17,6 +17,55 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
||||||
|
|
||||||
|
private static readonly bool _perfHleHistogram =
|
||||||
|
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE"), "1", System.StringComparison.Ordinal);
|
||||||
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, long> _perfHleCounts = new();
|
||||||
|
private static long _perfHleTotal;
|
||||||
|
private static long _perfHleDispatchTicks;
|
||||||
|
|
||||||
|
private static void RecordPerfHleDispatchTime(long ticks)
|
||||||
|
{
|
||||||
|
var total = System.Threading.Interlocked.Add(ref _perfHleDispatchTicks, ticks);
|
||||||
|
var calls = System.Threading.Interlocked.Read(ref _perfHleTotal);
|
||||||
|
if (calls > 0 && calls % 500000 == 0)
|
||||||
|
{
|
||||||
|
var avgUs = (double)total / System.Diagnostics.Stopwatch.Frequency * 1_000_000.0 / calls;
|
||||||
|
System.Console.Error.WriteLine($"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us total_managed_s={(double)total / System.Diagnostics.Stopwatch.Frequency:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly bool _perfHleNoDict =
|
||||||
|
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE_NODICT"), "1", System.StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static void RecordPerfHleCall(string name)
|
||||||
|
{
|
||||||
|
var total = System.Threading.Interlocked.Increment(ref _perfHleTotal);
|
||||||
|
if (!_perfHleNoDict)
|
||||||
|
{
|
||||||
|
_perfHleCounts.AddOrUpdate(name, 1, static (_, v) => v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total % 500000 == 0 && !_perfHleNoDict)
|
||||||
|
{
|
||||||
|
// Snapshot via foreach (a safe moving enumerator) before sorting.
|
||||||
|
// LINQ over a ConcurrentDictionary uses ICollection.CopyTo, which
|
||||||
|
// throws ArgumentException if another thread adds a key between the
|
||||||
|
// Count read and the copy — that exception was being swallowed into
|
||||||
|
// a CPU_TRAP return and crashing the guest.
|
||||||
|
var snapshot = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, long>>(_perfHleCounts.Count + 16);
|
||||||
|
foreach (var kvp in _perfHleCounts)
|
||||||
|
{
|
||||||
|
snapshot.Add(kvp);
|
||||||
|
}
|
||||||
|
|
||||||
|
var top = snapshot
|
||||||
|
.OrderByDescending(kvp => kvp.Value)
|
||||||
|
.Take(20)
|
||||||
|
.Select(kvp => $"{kvp.Key}={kvp.Value}");
|
||||||
|
System.Console.Error.WriteLine($"[PERF][HLE] total={total} top: {string.Join(", ", top)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RecordRecentImportTrace(
|
private void RecordRecentImportTrace(
|
||||||
long dispatchIndex,
|
long dispatchIndex,
|
||||||
string nid,
|
string nid,
|
||||||
@@ -25,101 +74,40 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong arg1,
|
ulong arg1,
|
||||||
ulong arg2)
|
ulong arg2)
|
||||||
{
|
{
|
||||||
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
var trace = _recentImportTrace;
|
||||||
|
trace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||||
dispatchIndex,
|
dispatchIndex,
|
||||||
nid,
|
nid,
|
||||||
returnRip,
|
returnRip,
|
||||||
arg0,
|
arg0,
|
||||||
arg1,
|
arg1,
|
||||||
arg2);
|
arg2,
|
||||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
if (_recentImportTraceCount < _recentImportTrace.Length)
|
Environment.CurrentManagedThreadId);
|
||||||
|
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % trace.Length;
|
||||||
|
if (_recentImportTraceCount < trace.Length)
|
||||||
{
|
{
|
||||||
_recentImportTraceCount++;
|
_recentImportTraceCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecordDeferredBootstrapTrace(
|
|
||||||
long dispatchIndex,
|
|
||||||
ulong op,
|
|
||||||
ulong symbolPointer,
|
|
||||||
ulong outputPointer,
|
|
||||||
ulong returnRip)
|
|
||||||
{
|
|
||||||
lock (_deferredBootstrapTraceGate)
|
|
||||||
{
|
|
||||||
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
|
|
||||||
dispatchIndex,
|
|
||||||
op,
|
|
||||||
symbolPointer,
|
|
||||||
outputPointer,
|
|
||||||
returnRip);
|
|
||||||
_deferredBootstrapTraceWriteIndex =
|
|
||||||
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
|
|
||||||
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
|
|
||||||
{
|
|
||||||
_deferredBootstrapTraceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrainDeferredBootstrapTraces()
|
|
||||||
{
|
|
||||||
if (!_logBootstrap)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeferredBootstrapTraceEntry[] pending;
|
|
||||||
lock (_deferredBootstrapTraceGate)
|
|
||||||
{
|
|
||||||
if (_deferredBootstrapTraceCount == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
|
|
||||||
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
|
|
||||||
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
|
|
||||||
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
|
|
||||||
{
|
|
||||||
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
|
|
||||||
}
|
|
||||||
|
|
||||||
_deferredBootstrapTraceCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in pending)
|
|
||||||
{
|
|
||||||
var symbolText = "<unreadable>";
|
|
||||||
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
|
|
||||||
{
|
|
||||||
symbolText = sym;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
|
|
||||||
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
|
|
||||||
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DumpRecentImportTrace()
|
private void DumpRecentImportTrace()
|
||||||
{
|
{
|
||||||
if (_recentImportTraceCount == 0)
|
var trace = _recentImportTrace;
|
||||||
|
if (trace is null || _recentImportTraceCount == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
Log.Info($" Recent import calls for managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ({_recentImportTraceCount}):");
|
||||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + trace.Length) % trace.Length;
|
||||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||||
{
|
{
|
||||||
int num2 = (num + i) % _recentImportTrace.Length;
|
int num2 = (num + i) % trace.Length;
|
||||||
var entry = _recentImportTrace[num2];
|
var entry = trace[num2];
|
||||||
if (!string.IsNullOrEmpty(entry.Nid))
|
if (!string.IsNullOrEmpty(entry.Nid))
|
||||||
{
|
{
|
||||||
Log.Info(
|
Log.Info(
|
||||||
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
$" #{entry.DispatchIndex} managed={entry.ManagedThreadId} guest=0x{entry.GuestThreadHandle:X16} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,9 +123,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int num2 = 0;
|
int num2 = 0;
|
||||||
List<ulong> list = new List<ulong>(16);
|
List<ulong> list = new List<ulong>(16);
|
||||||
ulong num3 = scanStart;
|
ulong num3 = scanStart;
|
||||||
var hostMemory = ResolveDiagnosticsHostMemory();
|
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
||||||
HostRegionInfo lpBuffer;
|
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||||
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
|
||||||
{
|
{
|
||||||
ulong baseAddress = lpBuffer.BaseAddress;
|
ulong baseAddress = lpBuffer.BaseAddress;
|
||||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||||
@@ -147,7 +134,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
ulong value = Math.Max(num3, baseAddress);
|
ulong value = Math.Max(num3, baseAddress);
|
||||||
ulong num5 = Math.Min(num4, scanEnd);
|
ulong num5 = Math.Min(num4, scanEnd);
|
||||||
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
|
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
||||||
{
|
{
|
||||||
ulong num6 = AlignUp(value, 8uL);
|
ulong num6 = AlignUp(value, 8uL);
|
||||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||||
@@ -186,6 +173,50 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const int preludeSize = 192;
|
||||||
|
Span<byte> prelude = stackalloc byte[preludeSize];
|
||||||
|
if (returnRip >= preludeSize && cpuContext.Memory.TryRead(returnRip - preludeSize, prelude))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} pre-return bytes @0x{returnRip - preludeSize:X16}: " +
|
||||||
|
BitConverter.ToString(prelude.ToArray()).Replace("-", " "));
|
||||||
|
|
||||||
|
List<DecodedInst>? bestCallChain = null;
|
||||||
|
var preludeAddress = returnRip - preludeSize;
|
||||||
|
for (var startOffset = 0; startOffset < preludeSize; startOffset++)
|
||||||
|
{
|
||||||
|
var cursor = preludeAddress + (ulong)startOffset;
|
||||||
|
var candidate = new List<DecodedInst>();
|
||||||
|
while (cursor < returnRip && candidate.Count < 96 &&
|
||||||
|
IcedDecoder.TryReadGuestBytes(cpuContext.Memory, cursor, 15, out var instructionBytes) &&
|
||||||
|
IcedDecoder.TryDecode(cursor, instructionBytes, out var instruction) &&
|
||||||
|
instruction.Length > 0 &&
|
||||||
|
cursor + (ulong)instruction.Length <= returnRip)
|
||||||
|
{
|
||||||
|
candidate.Add(instruction);
|
||||||
|
cursor += (ulong)instruction.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor == returnRip &&
|
||||||
|
candidate.Count > 0 &&
|
||||||
|
string.Equals(candidate[^1].Mnemonic, "Call", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
(bestCallChain is null || candidate.Count > bestCallChain.Count))
|
||||||
|
{
|
||||||
|
bestCallChain = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestCallChain is not null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} pre-return disassembly:");
|
||||||
|
foreach (var instruction in bestCallChain.TakeLast(32))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] 0x{instruction.Rip:X16}: {instruction.Text} " +
|
||||||
|
$"bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Span<byte> destination = stackalloc byte[128];
|
Span<byte> destination = stackalloc byte[128];
|
||||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||||
{
|
{
|
||||||
@@ -237,15 +268,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong callRip = returnRip + (ulong)i;
|
ulong callRip = returnRip + (ulong)i;
|
||||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||||
var importEntries = _importEntries;
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
|
||||||
{
|
{
|
||||||
if (importEntries[importIndex].Address != target)
|
if (_importEntries[importIndex].Address != target)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string nid = importEntries[importIndex].Nid;
|
string nid = _importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Log.Debug(
|
||||||
@@ -272,14 +302,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Log.Debug(
|
Log.Debug(
|
||||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
{
|
{
|
||||||
if (importEntries[importIndex].Address != slotTarget)
|
if (_importEntries[importIndex].Address != slotTarget)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string nid = importEntries[importIndex].Nid;
|
string nid = _importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Log.Debug(
|
||||||
@@ -303,6 +333,28 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
|
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong ParseOptionalHexAddress(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = value.Trim();
|
||||||
|
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
text = text[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ulong.TryParse(
|
||||||
|
text,
|
||||||
|
System.Globalization.NumberStyles.HexNumber,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var address)
|
||||||
|
? address
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsPlausibleReturnAddress(ulong address)
|
private static bool IsPlausibleReturnAddress(ulong address)
|
||||||
{
|
{
|
||||||
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
|
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
|
||||||
@@ -352,7 +404,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -361,7 +413,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -393,12 +445,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||||
if (executable)
|
if (executable)
|
||||||
{
|
{
|
||||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
@@ -417,14 +469,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return (value + num) & ~num;
|
return (value + num) & ~num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostics helpers are static (reachable from static handler paths), so
|
|
||||||
// they use the platform injected into the backend active on this thread and
|
|
||||||
// fall back to the process-wide singleton only when no run is bound.
|
|
||||||
private static IHostMemory ResolveDiagnosticsHostMemory()
|
|
||||||
{
|
|
||||||
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsReadableProtection(uint protect)
|
private static bool IsReadableProtection(uint protect)
|
||||||
{
|
{
|
||||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Disasm;
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
using SharpEmu.Core.Cpu.Native.Windows;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -19,6 +17,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
||||||
private static int _lazyCommitTraceCount;
|
private static int _lazyCommitTraceCount;
|
||||||
|
private static int _guestAllocatorHoleRecoveries;
|
||||||
|
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||||
|
|
||||||
private unsafe void SetupExceptionHandler()
|
private unsafe void SetupExceptionHandler()
|
||||||
{
|
{
|
||||||
@@ -30,12 +30,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
||||||
if (_rawExceptionHandlerStub == 0)
|
if (_rawExceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||||
}
|
}
|
||||||
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -45,22 +45,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
_handlerDelegate = VectoredHandler;
|
_handlerDelegate = VectoredHandler;
|
||||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||||
if (_exceptionHandlerStub == 0)
|
if (_exceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||||
}
|
}
|
||||||
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||||
if (_unhandledFilterStub == 0)
|
if (_unhandledFilterStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||||
}
|
}
|
||||||
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
@@ -68,8 +68,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
||||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||||
@@ -108,19 +108,28 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||||
|
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
|
||||||
// the managed import gateway means the VEH->managed reentry happened from
|
|
||||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
|
||||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
|
||||||
{
|
{
|
||||||
TraceThreadMode(
|
return -1;
|
||||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
}
|
||||||
|
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (exceptionCode == 3221225477u &&
|
||||||
|
TryRecoverGuestAllocatorHole(exceptionRecord, contextRecord, rip))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (exceptionCode == StatusIllegalInstruction &&
|
||||||
|
TryRecoverIllegalInstruction(contextRecord, rip))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -135,10 +144,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case WindowsFaultCodes.AccessViolation:
|
case 3221225477u:
|
||||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||||
break;
|
break;
|
||||||
case WindowsFaultCodes.FastFail:
|
case 3221226505u:
|
||||||
{
|
{
|
||||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
@@ -148,27 +157,56 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
ulong rax = ReadCtxU64(contextRecord, 120);
|
||||||
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
ulong rbx = ReadCtxU64(contextRecord, 144);
|
||||||
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
ulong rcx = ReadCtxU64(contextRecord, 128);
|
||||||
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
ulong rdx = ReadCtxU64(contextRecord, 136);
|
||||||
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
ulong rsi = ReadCtxU64(contextRecord, 168);
|
||||||
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
ulong rdi = ReadCtxU64(contextRecord, 176);
|
||||||
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
ulong rbp = ReadCtxU64(contextRecord, 160);
|
||||||
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
ulong r8 = ReadCtxU64(contextRecord, 184);
|
||||||
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
ulong r9 = ReadCtxU64(contextRecord, 192);
|
||||||
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
ulong r10 = ReadCtxU64(contextRecord, 200);
|
||||||
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
ulong r11 = ReadCtxU64(contextRecord, 208);
|
||||||
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
ulong r12 = ReadCtxU64(contextRecord, 216);
|
||||||
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
ulong r13 = ReadCtxU64(contextRecord, 224);
|
||||||
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
ulong r14 = ReadCtxU64(contextRecord, 232);
|
||||||
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
ulong r15 = ReadCtxU64(contextRecord, 240);
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Host thread: managed={Environment.CurrentManagedThreadId} " +
|
||||||
|
$"name='{Thread.CurrentThread.Name ?? "<unnamed>"}'");
|
||||||
|
if (_activeGuestThreadState is { } activeGuestThread)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Guest thread: handle=0x{activeGuestThread.ThreadHandle:X16} " +
|
||||||
|
$"name='{activeGuestThread.Name}' state={activeGuestThread.State} " +
|
||||||
|
$"last_import={activeGuestThread.LastImportNid ?? "<none>"} " +
|
||||||
|
$"last_ret=0x{activeGuestThread.LastReturnRip:X16}");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Last import registers: " +
|
||||||
|
$"rax=0x{Volatile.Read(ref activeGuestThread.LastImportRax):X16} " +
|
||||||
|
$"result_valid={Volatile.Read(ref activeGuestThread.LastImportResultValid) != 0} " +
|
||||||
|
$"rdi=0x{activeGuestThread.LastImportRdi:X16} " +
|
||||||
|
$"rsi=0x{activeGuestThread.LastImportRsi:X16} " +
|
||||||
|
$"rdx=0x{activeGuestThread.LastImportRdx:X16} " +
|
||||||
|
$"rcx=0x{activeGuestThread.LastImportRcx:X16} " +
|
||||||
|
$"r8=0x{activeGuestThread.LastImportR8:X16} " +
|
||||||
|
$"r9=0x{activeGuestThread.LastImportR9:X16}");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Last import stack args: " +
|
||||||
|
$"0=0x{activeGuestThread.LastImportStack0:X16} " +
|
||||||
|
$"1=0x{activeGuestThread.LastImportStack1:X16} " +
|
||||||
|
$"2=0x{activeGuestThread.LastImportStack2:X16} " +
|
||||||
|
$"3=0x{activeGuestThread.LastImportStack3:X16} " +
|
||||||
|
$"4=0x{activeGuestThread.LastImportStack4:X16} " +
|
||||||
|
$"5=0x{activeGuestThread.LastImportStack5:X16}");
|
||||||
|
}
|
||||||
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
||||||
@@ -193,7 +231,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong accessType = 0;
|
ulong accessType = 0;
|
||||||
ulong target = 0;
|
ulong target = 0;
|
||||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
||||||
{
|
{
|
||||||
accessType = *exceptionRecord->ExceptionInformation;
|
accessType = *exceptionRecord->ExceptionInformation;
|
||||||
target = exceptionRecord->ExceptionInformation[1];
|
target = exceptionRecord->ExceptionInformation[1];
|
||||||
@@ -206,9 +244,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
};
|
};
|
||||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||||
if (_hostMemory.Query(target, out var mbi))
|
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -225,13 +263,46 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_FAULT_STACK_WINDOW"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Full fault stack window (RSP-0x300..RSP+0x100):");
|
||||||
|
var windowStart = rsp >= 0x300 ? rsp - 0x300 : 0;
|
||||||
|
for (var stackAddr = windowStart; stackAddr < rsp + 0x100; stackAddr += 8)
|
||||||
|
{
|
||||||
|
if (!TryReadHostQword(stackAddr, out var value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = unchecked((long)(stackAddr - rsp));
|
||||||
|
var relativeText = relative >= 0
|
||||||
|
? $"+0x{relative:X}"
|
||||||
|
: $"-0x{-relative:X}";
|
||||||
|
var symbolText = TryFormatNearestRuntimeSymbol(value, out var stackSymbol)
|
||||||
|
? $" [{stackSymbol}]"
|
||||||
|
: string.Empty;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] [rsp{relativeText}] " +
|
||||||
|
$"@0x{stackAddr:X16} = 0x{value:X16}{symbolText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpPointerWindow("fault-register-rbx", rbx, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-rsi", rsi, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-rdi", rdi, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-r13", r13, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-r14", r14, 0x60);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
|
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
|
||||||
ulong frame = rbp;
|
ulong frame = rbp;
|
||||||
for (int i = 0; i < 12; i++)
|
for (int i = 0; i < 12; i++)
|
||||||
{
|
{
|
||||||
if (frame < 140733193388032L || frame > 140737488355327L)
|
if (frame < 0x10000)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -256,7 +327,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case WindowsFaultCodes.AccessViolation:
|
case 3221225477u:
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||||
@@ -290,22 +361,57 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||||
}
|
}
|
||||||
|
for (var stackIndex = 0; stackIndex < 16; stackIndex++)
|
||||||
|
{
|
||||||
|
byte[] stackSlot = new byte[8];
|
||||||
|
if (!TryReadHostBytes(rsp + (ulong)(stackIndex * 8), stackSlot))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var candidate = BitConverter.ToUInt64(stackSlot);
|
||||||
|
if (candidate < _entryPoint || candidate >= _entryPoint + 0x10000000 || candidate < 24)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte[] callSiteWindow = new byte[48];
|
||||||
|
if (TryReadHostBytes(candidate - 24, callSiteWindow))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
|
||||||
|
BitConverter.ToString(callSiteWindow).Replace("-", " "));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||||
}
|
}
|
||||||
DumpRecentImportTrace();
|
DumpRecentImportTrace();
|
||||||
DumpGuestDisasmDiagnostics(rip, rbp);
|
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||||
|
DumpGuestRegisterWindowDiagnostics(
|
||||||
|
rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp,
|
||||||
|
r8, r9, r10, r11, r12, r13, r14, r15);
|
||||||
DumpGuestReferenceDiagnostics();
|
DumpGuestReferenceDiagnostics();
|
||||||
DumpGuestPointerWindowDiagnostics();
|
DumpGuestPointerWindowDiagnostics();
|
||||||
break;
|
break;
|
||||||
case WindowsFaultCodes.Breakpoint:
|
case 2147483651u:
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||||
break;
|
break;
|
||||||
case WindowsFaultCodes.IllegalInstruction:
|
case 1073741845u:
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Abort (SIGABRT)");
|
||||||
|
DumpRecentImportTrace();
|
||||||
|
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||||
|
break;
|
||||||
|
case 3221225501u:
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||||
|
byte[] illegalCode = new byte[16];
|
||||||
|
if (TryReadHostBytes(rip, illegalCode))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(illegalCode).Replace("-", " "));
|
||||||
|
}
|
||||||
|
DumpRecentImportTrace();
|
||||||
|
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +425,108 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryRecoverAuxiliaryThreadExecuteFault(
|
||||||
|
EXCEPTION_RECORD* exceptionRecord,
|
||||||
|
void* contextRecord,
|
||||||
|
ulong rip)
|
||||||
|
{
|
||||||
|
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
||||||
|
rip >= 0x0000000800000000UL ||
|
||||||
|
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostExit = ActiveEntryReturnSentinelRip;
|
||||||
|
if (hostExit < 0x10000)
|
||||||
|
{
|
||||||
|
hostExit = unchecked((ulong)_guestReturnStub);
|
||||||
|
}
|
||||||
|
if (hostExit < 0x10000)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Could not recover auxiliary TBB execute fault: target=0x{rip:X16} " +
|
||||||
|
$"active_exit=0x{ActiveEntryReturnSentinelRip:X16} guest_return_stub=0x{unchecked((ulong)_guestReturnStub):X16}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||||
|
WriteCtxU64(contextRecord, 120, 0);
|
||||||
|
WriteCtxU64(contextRecord, 248, hostExit);
|
||||||
|
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||||
|
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||||
|
{
|
||||||
|
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] opcode = new byte[2];
|
||||||
|
if (!TryReadHostBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Interlocked.Increment(ref _ignoredGuestInt41Count);
|
||||||
|
WriteCtxU64(contextRecord, 248, rip + 2);
|
||||||
|
if (count <= 16 || count % 65536 == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (SHARPEMU_IGNORE_INT41=1)");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static bool TryRecoverGuestAllocatorHole(
|
||||||
|
EXCEPTION_RECORD* exceptionRecord,
|
||||||
|
void* contextRecord,
|
||||||
|
ulong rip)
|
||||||
|
{
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal) ||
|
||||||
|
exceptionRecord->NumberParameters < 2 ||
|
||||||
|
exceptionRecord->ExceptionInformation[0] != 0 ||
|
||||||
|
exceptionRecord->ExceptionInformation[1] != 8 ||
|
||||||
|
ReadCtxU64(contextRecord, CTX_RDI) != 0 ||
|
||||||
|
rip < 0x10000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demon's Souls occasionally leaves an empty payload in a locked pool
|
||||||
|
// tree node. The allocator dereferences payload+8 before reaching its
|
||||||
|
// existing empty-pool fallback. Match the instruction stream instead of
|
||||||
|
// a title-specific absolute address, then resume at that fallback so the
|
||||||
|
// lock is released and the allocator can try its next backing pool.
|
||||||
|
const ulong allocatorHoleSignature = 0x634CFF568D08778BUL;
|
||||||
|
if (*(ulong*)rip != allocatorHoleSignature || *((byte*)rip + 8) != 0xF2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ulong emptyPoolFallbackDelta = 0x8E;
|
||||||
|
WriteCtxU64(contextRecord, CTX_RIP, rip + emptyPoolFallbackDelta);
|
||||||
|
var recovery = Interlocked.Increment(ref _guestAllocatorHoleRecoveries);
|
||||||
|
if (recovery <= 16 || (recovery & (recovery - 1)) == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Guest allocator empty-node adapter recovery #{recovery}: " +
|
||||||
|
$"rip=0x{rip:X16} -> 0x{rip + emptyPoolFallbackDelta:X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsBenignHostDebugException(uint exceptionCode)
|
private static bool IsBenignHostDebugException(uint exceptionCode)
|
||||||
{
|
{
|
||||||
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
||||||
@@ -337,8 +545,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||||
void* contextRecord = pointers->ContextRecord;
|
void* contextRecord = pointers->ContextRecord;
|
||||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
||||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
||||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -398,7 +606,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp)
|
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp, ulong rsp)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -410,12 +618,20 @@ public sealed partial class DirectExecutionBackend
|
|||||||
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
|
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimized guest code frequently omits frame pointers. The return
|
||||||
|
// address at RSP is then more useful than an RBP walk and identifies the
|
||||||
|
// exact call site that supplied the faulting arguments.
|
||||||
|
if (TryReadHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
|
||||||
|
{
|
||||||
|
DumpGuestInstructionStream("stack-return-prelude", stackReturn - 0x60, 40);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ulong frame = rbp;
|
ulong frame = rbp;
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
if (frame < 140733193388032L || frame > 140737488355327L)
|
if (frame < 0x10000)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -484,7 +700,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong address = scanBase;
|
ulong address = scanBase;
|
||||||
while (address < scanEnd)
|
while (address < scanEnd)
|
||||||
{
|
{
|
||||||
if (!_hostMemory.Query(address, out var mbi))
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -496,9 +712,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == HostRegionState.Committed &&
|
if (mbi.State == MEM_COMMIT &&
|
||||||
IsReadableProtection(mbi.RawProtection) &&
|
IsReadableProtection(mbi.Protect) &&
|
||||||
IsExecutableProtection(mbi.RawProtection))
|
IsExecutableProtection(mbi.Protect))
|
||||||
{
|
{
|
||||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||||
}
|
}
|
||||||
@@ -559,6 +775,55 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DumpGuestRegisterWindowDiagnostics(
|
||||||
|
ulong rax,
|
||||||
|
ulong rbx,
|
||||||
|
ulong rcx,
|
||||||
|
ulong rdx,
|
||||||
|
ulong rsi,
|
||||||
|
ulong rdi,
|
||||||
|
ulong rbp,
|
||||||
|
ulong rsp,
|
||||||
|
ulong r8,
|
||||||
|
ulong r9,
|
||||||
|
ulong r10,
|
||||||
|
ulong r11,
|
||||||
|
ulong r12,
|
||||||
|
ulong r13,
|
||||||
|
ulong r14,
|
||||||
|
ulong r15)
|
||||||
|
{
|
||||||
|
if (!string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_REGISTER_WINDOWS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A register can be the only surviving reference to the object or
|
||||||
|
// argument array that caused a native guest fault. Capture a compact
|
||||||
|
// window while the process is alive so the post-mortem log can
|
||||||
|
// distinguish an absent object from a partially initialized one.
|
||||||
|
var registers = new (string Name, ulong Value)[]
|
||||||
|
{
|
||||||
|
("rax", rax), ("rbx", rbx), ("rcx", rcx), ("rdx", rdx),
|
||||||
|
("rsi", rsi), ("rdi", rdi), ("rbp", rbp), ("rsp", rsp),
|
||||||
|
("r8", r8), ("r9", r9), ("r10", r10), ("r11", r11),
|
||||||
|
("r12", r12), ("r13", r13), ("r14", r14), ("r15", r15),
|
||||||
|
};
|
||||||
|
var seen = new HashSet<ulong>();
|
||||||
|
foreach (var (name, value) in registers)
|
||||||
|
{
|
||||||
|
if (value < 0x10000 || !seen.Add(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpPointerWindow($"register-{name}", value, 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ScanExecutableRegionForTargetReferences(
|
private void ScanExecutableRegionForTargetReferences(
|
||||||
ulong regionBase,
|
ulong regionBase,
|
||||||
ulong regionEnd,
|
ulong regionEnd,
|
||||||
@@ -803,13 +1068,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_hostMemory.Query(address, out var mbi))
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||||
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -848,7 +1113,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool TryReadHostBytes(ulong address, byte[] buffer)
|
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||||
{
|
{
|
||||||
if (address < 65536)
|
if (address < 65536)
|
||||||
{
|
{
|
||||||
@@ -861,9 +1126,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong end = address + (ulong)buffer.Length;
|
ulong end = address + (ulong)buffer.Length;
|
||||||
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
||||||
{
|
{
|
||||||
if (!_hostMemory.Query(page, out var mbi) ||
|
if (VirtualQuery((void*)page, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||||
mbi.State != HostRegionState.Committed ||
|
mbi.State != MEM_COMMIT ||
|
||||||
!IsReadableProtection(mbi.RawProtection))
|
!IsReadableProtection(mbi.Protect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -976,25 +1241,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!_hostMemory.Query(faultAddress, out var mbi))
|
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||||
{
|
{
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1003,10 +1268,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
ulong committedSize = 0;
|
ulong committedSize = 0;
|
||||||
|
|
||||||
if (mbi.State == HostRegionState.Free)
|
if (mbi.State == 65536)
|
||||||
{
|
{
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||||
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = windowBase;
|
committedBase = windowBase;
|
||||||
@@ -1015,7 +1280,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = largeBase;
|
||||||
@@ -1026,13 +1291,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1045,7 +1310,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1053,13 +1318,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State != HostRegionState.Reserved)
|
if (mbi.State != 8192)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = commitWindowBase;
|
committedBase = commitWindowBase;
|
||||||
@@ -1068,7 +1333,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = largeCommitBase;
|
||||||
@@ -1079,19 +1344,19 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
committedSize = 8192uL;
|
committedSize = 8192uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1104,7 +1369,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1145,33 +1410,31 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The commit protection is one of the two raw values ResolveLazyCommitProtection
|
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||||
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
|
|
||||||
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
|
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||||
{
|
{
|
||||||
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
if (!TryReserveRange(reserveAddress, reserveSize))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
return TryCommitRange(commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
|
using Iced.Intel;
|
||||||
|
using SharpEmu.Core.Cpu.Emulation;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
|
||||||
|
//
|
||||||
|
// Guest code runs natively, so the guest and host share the same virtual address space and the
|
||||||
|
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
|
||||||
|
// one of these extensions it raises #UD instead of executing the opcode; without this the title
|
||||||
|
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
|
||||||
|
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
|
||||||
|
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
|
||||||
|
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
|
||||||
|
// does not fully model.
|
||||||
|
public sealed partial class DirectExecutionBackend
|
||||||
|
{
|
||||||
|
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
|
||||||
|
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
|
||||||
|
private const int CTX_EFLAGS = 68;
|
||||||
|
|
||||||
|
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
|
||||||
|
private const uint StatusIllegalInstruction = 0xC000001Du;
|
||||||
|
|
||||||
|
private const int MaxInstructionBytes = 15;
|
||||||
|
|
||||||
|
// Instruction-window sizes tried in turn so a fault near a page boundary still decodes.
|
||||||
|
private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 };
|
||||||
|
|
||||||
|
private static int _bmiSoftwareFallbackAnnounced;
|
||||||
|
private static long _bmiInstructionsEmulated;
|
||||||
|
|
||||||
|
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
|
||||||
|
{
|
||||||
|
if (!TryReadFaultingInstruction(rip, out var instruction))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.Op0Kind != OpKind.Register ||
|
||||||
|
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteCtxU64(contextRecord, destOffset, result);
|
||||||
|
if (flagsChanged)
|
||||||
|
{
|
||||||
|
WriteCtxU32(contextRecord, CTX_EFLAGS, eflags);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _bmiInstructionsEmulated);
|
||||||
|
if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " +
|
||||||
|
"emulating those instructions in software.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryEvaluate(
|
||||||
|
void* contextRecord,
|
||||||
|
in Instruction instruction,
|
||||||
|
GprOperandSize size,
|
||||||
|
out ulong result,
|
||||||
|
out bool flagsChanged,
|
||||||
|
out uint eflags)
|
||||||
|
{
|
||||||
|
result = 0;
|
||||||
|
flagsChanged = false;
|
||||||
|
eflags = ReadCtxU32(contextRecord, CTX_EFLAGS);
|
||||||
|
|
||||||
|
switch (instruction.Mnemonic)
|
||||||
|
{
|
||||||
|
case Mnemonic.Andn:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) ||
|
||||||
|
!TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags);
|
||||||
|
flagsChanged = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Blsi:
|
||||||
|
case Mnemonic.Blsmsk:
|
||||||
|
case Mnemonic.Blsr:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = instruction.Mnemonic switch
|
||||||
|
{
|
||||||
|
Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags),
|
||||||
|
Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags),
|
||||||
|
_ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags),
|
||||||
|
};
|
||||||
|
flagsChanged = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Bextr:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) ||
|
||||||
|
!TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags);
|
||||||
|
flagsChanged = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Bzhi:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) ||
|
||||||
|
!TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags);
|
||||||
|
flagsChanged = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Tzcnt:
|
||||||
|
case Mnemonic.Lzcnt:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = instruction.Mnemonic == Mnemonic.Tzcnt
|
||||||
|
? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags)
|
||||||
|
: BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags);
|
||||||
|
flagsChanged = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Rorx:
|
||||||
|
if (instruction.Op2Kind != OpKind.Immediate8 ||
|
||||||
|
!TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Sarx:
|
||||||
|
case Mnemonic.Shlx:
|
||||||
|
case Mnemonic.Shrx:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) ||
|
||||||
|
!TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = instruction.Mnemonic switch
|
||||||
|
{
|
||||||
|
Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size),
|
||||||
|
Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size),
|
||||||
|
_ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size),
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Mnemonic.Pdep:
|
||||||
|
case Mnemonic.Pext:
|
||||||
|
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) ||
|
||||||
|
!TryReadOperand(contextRecord, in instruction, 2, size, out var packMask))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = instruction.Mnemonic == Mnemonic.Pdep
|
||||||
|
? BmiInstructionEmulator.Pdep(packSrc, packMask, size)
|
||||||
|
: BmiInstructionEmulator.Pext(packSrc, packMask, size);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
|
||||||
|
{
|
||||||
|
// Try the full instruction window first, then shrink so a fault near the end of a mapped
|
||||||
|
// page (where fewer than 15 bytes are readable) still decodes.
|
||||||
|
foreach (var attempt in DecodeWindowSizes)
|
||||||
|
{
|
||||||
|
var buffer = new byte[attempt];
|
||||||
|
if (!TryReadHostBytes(rip, buffer))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
|
||||||
|
decoder.IP = rip;
|
||||||
|
decoder.Decode(out instruction);
|
||||||
|
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instruction = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryReadOperand(
|
||||||
|
void* contextRecord,
|
||||||
|
in Instruction instruction,
|
||||||
|
int operandIndex,
|
||||||
|
GprOperandSize size,
|
||||||
|
out ulong value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
switch (instruction.GetOpKind(operandIndex))
|
||||||
|
{
|
||||||
|
case OpKind.Register:
|
||||||
|
if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw = ReadCtxU64(contextRecord, offset);
|
||||||
|
value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case OpKind.Memory:
|
||||||
|
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
|
||||||
|
var buffer = new byte[byteCount];
|
||||||
|
if (!TryReadHostBytes(address, buffer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = byteCount == 8
|
||||||
|
? BinaryPrimitives.ReadUInt64LittleEndian(buffer)
|
||||||
|
: BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
|
||||||
|
// FS/GS-relative operands need the guest segment base, which is not modelled here.
|
||||||
|
if (instruction.SegmentPrefix != Register.None)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.IsIPRelativeMemoryOperand)
|
||||||
|
{
|
||||||
|
address = instruction.IPRelativeMemoryAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var effective = instruction.MemoryDisplacement64;
|
||||||
|
if (instruction.MemoryBase != Register.None)
|
||||||
|
{
|
||||||
|
if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
effective += ReadCtxU64(contextRecord, baseOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.MemoryIndex != Register.None)
|
||||||
|
{
|
||||||
|
if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
address = effective;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies.
|
||||||
|
private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size)
|
||||||
|
{
|
||||||
|
switch (register)
|
||||||
|
{
|
||||||
|
case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true;
|
||||||
|
case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true;
|
||||||
|
default:
|
||||||
|
if (TryGetGpr64Offset(register, out offset))
|
||||||
|
{
|
||||||
|
size = GprOperandSize.Bits64;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = GprOperandSize.Bits32;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetGpr64Offset(Register register, out int offset)
|
||||||
|
{
|
||||||
|
switch (register)
|
||||||
|
{
|
||||||
|
case Register.RAX: offset = CTX_RAX; return true;
|
||||||
|
case Register.RCX: offset = CTX_RCX; return true;
|
||||||
|
case Register.RDX: offset = CTX_RDX; return true;
|
||||||
|
case Register.RBX: offset = CTX_RBX; return true;
|
||||||
|
case Register.RSP: offset = CTX_RSP; return true;
|
||||||
|
case Register.RBP: offset = CTX_RBP; return true;
|
||||||
|
case Register.RSI: offset = CTX_RSI; return true;
|
||||||
|
case Register.RDI: offset = CTX_RDI; return true;
|
||||||
|
case Register.R8: offset = CTX_R8; return true;
|
||||||
|
case Register.R9: offset = CTX_R9; return true;
|
||||||
|
case Register.R10: offset = CTX_R10; return true;
|
||||||
|
case Register.R11: offset = CTX_R11; return true;
|
||||||
|
case Register.R12: offset = CTX_R12; return true;
|
||||||
|
case Register.R13: offset = CTX_R13; return true;
|
||||||
|
case Register.R14: offset = CTX_R14; return true;
|
||||||
|
case Register.R15: offset = CTX_R15; return true;
|
||||||
|
default: offset = 0; return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,6 @@ using System.Collections.Generic;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.HLE.Host.Posix;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -37,6 +35,20 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private bool _nativeWorkersDisposed;
|
private bool _nativeWorkersDisposed;
|
||||||
private int _nativeWorkerCreationFailedLogged;
|
private int _nativeWorkerCreationFailedLogged;
|
||||||
|
|
||||||
|
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern nint CreateThread(
|
||||||
|
nint lpThreadAttributes,
|
||||||
|
nuint dwStackSize,
|
||||||
|
nint lpStartAddress,
|
||||||
|
nint lpParameter,
|
||||||
|
uint dwCreationFlags,
|
||||||
|
out uint lpThreadId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||||
// thread; falls back to the historical inline calli (guest frames above this
|
// thread; falls back to the historical inline calli (guest frames above this
|
||||||
// thread's managed frames) when workers are disabled or unavailable.
|
// thread's managed frames) when workers are disabled or unavailable.
|
||||||
@@ -49,7 +61,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var worker = RentNativeGuestExecutor();
|
var worker = RentNativeGuestExecutor();
|
||||||
if (worker is null)
|
if (worker is null)
|
||||||
{
|
{
|
||||||
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
return CallNativeEntry(entryStub);
|
return CallNativeEntry(entryStub);
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -80,7 +92,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||||
{
|
{
|
||||||
if (NativeGuestWorkersDisabled)
|
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
||||||
|
// kernel32!CreateThread. POSIX hosts use the established inline entry
|
||||||
|
// path until the worker loop has a pthread/eventfd implementation.
|
||||||
|
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -234,7 +249,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||||
{
|
{
|
||||||
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
if (!EnsureKernel32Exports())
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -247,27 +262,32 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
private static bool EnsureKernel32Exports()
|
||||||
{
|
{
|
||||||
if (_exitThreadAddress != 0)
|
if (_exitThreadAddress != 0)
|
||||||
{
|
{
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||||
}
|
}
|
||||||
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||||
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
if (kernel32 == 0)
|
||||||
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
||||||
|
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
||||||
|
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Initialize()
|
private bool Initialize()
|
||||||
{
|
{
|
||||||
_selfHandle = GCHandle.Alloc(this);
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
||||||
if (_controlBlock == null)
|
if (_controlBlock == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
||||||
if (_loopStub == null)
|
if (_loopStub == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -375,15 +395,17 @@ public sealed partial class DirectExecutionBackend
|
|||||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||||
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
_threadHandle = CreateThread(
|
||||||
(nint)_loopStub,
|
|
||||||
0,
|
0,
|
||||||
WorkerStackReservation,
|
WorkerStackReservation,
|
||||||
|
(nint)_loopStub,
|
||||||
|
0,
|
||||||
|
StackSizeParamIsAReservation,
|
||||||
out _nativeThreadId);
|
out _nativeThreadId);
|
||||||
if (_threadHandle == 0)
|
if (_threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -511,7 +533,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||||
_prevState = _activeGuestThreadState;
|
_prevState = _activeGuestThreadState;
|
||||||
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
||||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||||
_entered = true;
|
_entered = true;
|
||||||
_activeExecutionBackend = backend;
|
_activeExecutionBackend = backend;
|
||||||
@@ -523,11 +545,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_activeGuestThreadYieldReason = null;
|
_activeGuestThreadYieldReason = null;
|
||||||
_activeGuestThreadState = _runState;
|
_activeGuestThreadState = _runState;
|
||||||
backend.BindTlsBase(_runContext!);
|
backend.BindTlsBase(_runContext!);
|
||||||
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||||
if (_runState is { } state)
|
if (_runState is { } state)
|
||||||
{
|
{
|
||||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||||
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||||
}
|
}
|
||||||
if (_runAffinityMask != 0)
|
if (_runAffinityMask != 0)
|
||||||
{
|
{
|
||||||
@@ -557,7 +579,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||||
}
|
}
|
||||||
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||||
_activeExecutionBackend = _prevBackend;
|
_activeExecutionBackend = _prevBackend;
|
||||||
_activeCpuContext = _prevContext;
|
_activeCpuContext = _prevContext;
|
||||||
@@ -594,8 +616,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var exited = _threadHandle == 0;
|
var exited = _threadHandle == 0;
|
||||||
if (_threadHandle != 0)
|
if (_threadHandle != 0)
|
||||||
{
|
{
|
||||||
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
||||||
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
CloseHandle(_threadHandle);
|
||||||
_threadHandle = 0;
|
_threadHandle = 0;
|
||||||
}
|
}
|
||||||
if (!exited)
|
if (!exited)
|
||||||
@@ -609,12 +631,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (_loopStub != null)
|
if (_loopStub != null)
|
||||||
{
|
{
|
||||||
_backend._hostMemory.Free((ulong)_loopStub);
|
VirtualFree(_loopStub, 0u, 32768u);
|
||||||
_loopStub = null;
|
_loopStub = null;
|
||||||
}
|
}
|
||||||
if (_controlBlock != null)
|
if (_controlBlock != null)
|
||||||
{
|
{
|
||||||
_backend._hostMemory.Free((ulong)_controlBlock);
|
VirtualFree(_controlBlock, 0u, 32768u);
|
||||||
_controlBlock = null;
|
_controlBlock = null;
|
||||||
}
|
}
|
||||||
if (_selfHandle.IsAllocated)
|
if (_selfHandle.IsAllocated)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Native.Windows;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
// runtime keeps turning its own faults into managed exceptions.
|
// runtime keeps turning its own faults into managed exceptions.
|
||||||
|
|
||||||
private const int PosixSigIll = 4;
|
private const int PosixSigIll = 4;
|
||||||
|
private const int PosixSigTrap = 5;
|
||||||
|
private const int PosixSigAbort = 6;
|
||||||
private const int PosixSigSegv = 11;
|
private const int PosixSigSegv = 11;
|
||||||
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
||||||
|
|
||||||
@@ -62,6 +64,9 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
private static bool _posixSignalWarmup;
|
private static bool _posixSignalWarmup;
|
||||||
private static readonly nint[] _posixPreviousActions = new nint[32];
|
private static readonly nint[] _posixPreviousActions = new nint[32];
|
||||||
private static int _posixSignalTraceCount;
|
private static int _posixSignalTraceCount;
|
||||||
|
private static long _perfSignalCount;
|
||||||
|
private static readonly bool _perfSignalCounter =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PERF_MEM"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static int _posixSignalHandlerDepth;
|
private static int _posixSignalHandlerDepth;
|
||||||
@@ -88,10 +93,13 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
WarmUpPosixSignalPath();
|
WarmUpPosixSignalPath();
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
|
||||||
|
|
||||||
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
||||||
!InstallPosixSignalHandler(PosixSigBus) ||
|
!InstallPosixSignalHandler(PosixSigBus) ||
|
||||||
!InstallPosixSignalHandler(PosixSigIll))
|
!InstallPosixSignalHandler(PosixSigIll) ||
|
||||||
|
!InstallPosixSignalHandler(PosixSigTrap) ||
|
||||||
|
!InstallPosixSignalHandler(PosixSigAbort))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
||||||
}
|
}
|
||||||
@@ -132,8 +140,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
// action and sigaction(0, ...) fails with EINVAL).
|
// action and sigaction(0, ...) fails with EINVAL).
|
||||||
EXCEPTION_RECORD record = default;
|
EXCEPTION_RECORD record = default;
|
||||||
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
|
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
|
||||||
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
byte* contextRecord = stackalloc byte[Win64ContextSize];
|
||||||
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
new Span<byte>(contextRecord, Win64ContextSize).Clear();
|
||||||
EXCEPTION_POINTERS pointers;
|
EXCEPTION_POINTERS pointers;
|
||||||
pointers.ExceptionRecord = &record;
|
pointers.ExceptionRecord = &record;
|
||||||
pointers.ContextRecord = contextRecord;
|
pointers.ContextRecord = contextRecord;
|
||||||
@@ -190,8 +198,27 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
_posixSignalHandlerDepth++;
|
_posixSignalHandlerDepth++;
|
||||||
|
if (_perfSignalCounter)
|
||||||
|
{
|
||||||
|
var n = Interlocked.Increment(ref _perfSignalCount);
|
||||||
|
if (n % 100000 == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[PERF][MEM] posix_faults={n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Guest-image write tracking runs first: it only needs the fault
|
||||||
|
// address (safe for host and guest threads alike) and must resume
|
||||||
|
// the faulting write immediately after restoring write access.
|
||||||
|
if (signal != PosixSigIll &&
|
||||||
|
siginfo != 0 &&
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
|
||||||
|
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -217,8 +244,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
byte* contextRecord = stackalloc byte[Win64ContextSize];
|
||||||
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
new Span<byte>(contextRecord, Win64ContextSize).Clear();
|
||||||
int[] offsets = PosixRegisterOffsets;
|
int[] offsets = PosixRegisterOffsets;
|
||||||
for (int i = 0; i < offsets.Length; i++)
|
for (int i = 0; i < offsets.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -231,6 +258,14 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
record.ExceptionCode = 3221225501u;
|
record.ExceptionCode = 3221225501u;
|
||||||
}
|
}
|
||||||
|
else if (signal == PosixSigTrap)
|
||||||
|
{
|
||||||
|
record.ExceptionCode = 2147483651u;
|
||||||
|
}
|
||||||
|
else if (signal == PosixSigAbort)
|
||||||
|
{
|
||||||
|
record.ExceptionCode = 1073741845u;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,425 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POSIX replacements for the kernel32 helpers the native backend embeds in
|
||||||
|
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
|
||||||
|
/// convention the emitted call sites were written for (first argument in
|
||||||
|
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
|
||||||
|
/// emission code stays identical across platforms.
|
||||||
|
/// </summary>
|
||||||
|
internal static unsafe class PosixHostStubs
|
||||||
|
{
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static bool _initialized;
|
||||||
|
private static nint _tlsGetValueStub;
|
||||||
|
private static nint _queryPerformanceCounterStub;
|
||||||
|
private static nint _switchToThreadStub;
|
||||||
|
private static nint _sleepStub;
|
||||||
|
|
||||||
|
public static nint TlsGetValueStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _tlsGetValueStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint QueryPerformanceCounterStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SwitchToThreadStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _switchToThreadStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SleepStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _sleepStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint CreateWorkerEvent()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return dispatch_semaphore_create(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var semaphore = Marshal.AllocHGlobal(64);
|
||||||
|
if (sem_init(semaphore, 0, 0) != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(semaphore);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return semaphore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SignalWorkerEvent(nint handle)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
_ = dispatch_semaphore_signal(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sem_post(handle) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
if (timeoutMilliseconds < 0)
|
||||||
|
{
|
||||||
|
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
|
||||||
|
return dispatch_semaphore_wait(handle, deadline) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutMilliseconds < 0)
|
||||||
|
{
|
||||||
|
while (sem_wait(handle) != 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
|
||||||
|
while (sem_trywait(handle) != 0)
|
||||||
|
{
|
||||||
|
if (Environment.TickCount64 >= deadlineTicks)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DestroyWorkerEvent(nint handle)
|
||||||
|
{
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
dispatch_release(handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = sem_destroy(handle);
|
||||||
|
Marshal.FreeHGlobal(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.</summary>
|
||||||
|
public static uint TlsAlloc()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
nuint key;
|
||||||
|
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint key32;
|
||||||
|
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TlsFree(uint key)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_key_delete_mac((nuint)key) == 0
|
||||||
|
: pthread_key_delete_linux(key) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TlsSetValue(uint key, nint value)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_setspecific_mac((nuint)key, value) == 0
|
||||||
|
: pthread_setspecific_linux(key, value) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint TlsGetValue(uint key)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_getspecific_mac((nuint)key)
|
||||||
|
: pthread_getspecific_linux(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).</summary>
|
||||||
|
public static uint GetCurrentThreadId()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
ulong tid;
|
||||||
|
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unchecked((uint)gettid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
|
||||||
|
/// thunk that accepts up to four integer arguments in the Win64 ABI the
|
||||||
|
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
|
||||||
|
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
|
||||||
|
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
|
||||||
|
/// the stack 16-byte aligned for the call, and forwards the rax result.
|
||||||
|
/// </summary>
|
||||||
|
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
|
||||||
|
{
|
||||||
|
var page = (byte*)HostMemory.Alloc(
|
||||||
|
null,
|
||||||
|
4096,
|
||||||
|
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||||
|
HostMemory.PAGE_EXECUTE_READWRITE);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
|
||||||
|
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
|
||||||
|
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
|
||||||
|
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
|
||||||
|
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
|
||||||
|
}
|
||||||
|
|
||||||
|
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
||||||
|
return (nint)page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildStubs();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildStubs()
|
||||||
|
{
|
||||||
|
var page = (byte*)HostMemory.Alloc(
|
||||||
|
null,
|
||||||
|
4096,
|
||||||
|
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||||
|
HostMemory.PAGE_EXECUTE_READWRITE);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
|
||||||
|
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
|
||||||
|
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
|
||||||
|
_sleepStub = EmitSleep(page, ref offset);
|
||||||
|
|
||||||
|
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
|
||||||
|
}
|
||||||
|
|
||||||
|
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitTlsGetValue(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// On macOS x86-64 pthread keys index the gs-based thread specific
|
||||||
|
// data array directly, so TlsGetValue(index in ecx) collapses to a
|
||||||
|
// single load that clobbers nothing but RAX.
|
||||||
|
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||||
|
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux: call pthread_getspecific, preserving the registers that are
|
||||||
|
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
|
||||||
|
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||||
|
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
|
||||||
|
// consumers only need a monotonically increasing counter, which rdtsc
|
||||||
|
// provides without leaving Win64-safe registers.
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
|
||||||
|
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
|
||||||
|
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSwitchToThread(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
var schedYield = ResolveLibcExport("sched_yield");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSleep(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||||
|
var usleep = ResolveLibcExport("usleep");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||||
|
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||||
|
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||||
|
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||||
|
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||||
|
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint ResolveLibcExport(string name)
|
||||||
|
{
|
||||||
|
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
|
||||||
|
return NativeLibrary.GetExport(libc, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Emit(byte* page, ref int offset, params byte[] bytes)
|
||||||
|
{
|
||||||
|
foreach (var value in bytes)
|
||||||
|
{
|
||||||
|
page[offset++] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
|
||||||
|
{
|
||||||
|
Emit(page, ref offset, 0x48, 0xB8);
|
||||||
|
*(long*)(page + offset) = value;
|
||||||
|
offset += sizeof(long);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||||
|
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||||
|
private static extern int pthread_key_create_linux(uint* key, nint destructor);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||||
|
private static extern int pthread_key_delete_mac(nuint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||||
|
private static extern int pthread_key_delete_linux(uint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||||
|
private static extern int pthread_setspecific_mac(nuint key, nint value);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||||
|
private static extern int pthread_setspecific_linux(uint key, nint value);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||||
|
private static extern nint pthread_getspecific_mac(nuint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||||
|
private static extern nint pthread_getspecific_linux(uint key);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int gettid();
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_create(long value);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_signal(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern void dispatch_release(nint handle);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_init(nint semaphore, int shared, uint value);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_post(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_wait(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_trywait(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_destroy(nint semaphore);
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -12,15 +11,17 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
private readonly List<nint> _allocatedStubs = new();
|
private readonly List<nint> _allocatedStubs = new();
|
||||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||||
private readonly IHostMemory _hostMemory;
|
|
||||||
private byte* _pltMemory;
|
private byte* _pltMemory;
|
||||||
private int _pltOffset;
|
private int _pltOffset;
|
||||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||||
|
|
||||||
public StubManager(IHostMemory? hostMemory = null)
|
public StubManager()
|
||||||
{
|
{
|
||||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
_pltMemory = (byte*)VirtualAlloc(
|
||||||
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
null,
|
||||||
|
(nuint)PltMemorySize,
|
||||||
|
AllocationType.Reserve | AllocationType.Commit,
|
||||||
|
MemoryProtection.ExecuteReadWrite);
|
||||||
|
|
||||||
if (_pltMemory == null)
|
if (_pltMemory == null)
|
||||||
{
|
{
|
||||||
@@ -184,7 +185,7 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
{
|
{
|
||||||
if (_pltMemory != null)
|
if (_pltMemory != null)
|
||||||
{
|
{
|
||||||
_hostMemory.Free((ulong)_pltMemory);
|
VirtualFree(_pltMemory, 0, FreeType.Release);
|
||||||
_pltMemory = null;
|
_pltMemory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,5 +194,29 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
_stubAddresses.Clear();
|
_stubAddresses.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect) =>
|
||||||
|
HostMemory.Alloc(lpAddress, dwSize, (uint)flAllocationType, (uint)flProtect);
|
||||||
|
|
||||||
|
private static bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType) =>
|
||||||
|
HostMemory.Free(lpAddress, dwSize, (uint)dwFreeType);
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum AllocationType : uint
|
||||||
|
{
|
||||||
|
Commit = 0x1000,
|
||||||
|
Reserve = 0x2000,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum MemoryProtection : uint
|
||||||
|
{
|
||||||
|
ExecuteReadWrite = 0x40,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum FreeType : uint
|
||||||
|
{
|
||||||
|
Release = 0x8000,
|
||||||
|
}
|
||||||
|
|
||||||
public delegate void ImportHandler(CpuContext context);
|
public delegate void ImportHandler(CpuContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public enum ProgramHeaderType : uint
|
|||||||
|
|
||||||
Tls = 7,
|
Tls = 7,
|
||||||
|
|
||||||
|
GnuEhFrame = 0x6474E550,
|
||||||
|
|
||||||
SceRela = 0x60000000,
|
SceRela = 0x60000000,
|
||||||
|
|
||||||
SceProcParam = 0x61000001,
|
SceProcParam = 0x61000001,
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ public sealed class SelfImage
|
|||||||
ulong procParamAddress = 0,
|
ulong procParamAddress = 0,
|
||||||
string? title = null,
|
string? title = null,
|
||||||
string? titleId = null,
|
string? titleId = null,
|
||||||
string? version = null)
|
string? version = null,
|
||||||
|
uint tlsModuleId = 0,
|
||||||
|
ulong tlsMemorySize = 0,
|
||||||
|
ulong tlsStaticOffset = 0)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(programHeaders);
|
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||||
ArgumentNullException.ThrowIfNull(mappedRegions);
|
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||||
@@ -44,6 +47,9 @@ public sealed class SelfImage
|
|||||||
Title = title;
|
Title = title;
|
||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
Version = version;
|
Version = version;
|
||||||
|
TlsModuleId = tlsModuleId;
|
||||||
|
TlsMemorySize = tlsMemorySize;
|
||||||
|
TlsStaticOffset = tlsStaticOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSelf { get; }
|
public bool IsSelf { get; }
|
||||||
@@ -75,4 +81,11 @@ public sealed class SelfImage
|
|||||||
public string? TitleId { get; }
|
public string? TitleId { get; }
|
||||||
|
|
||||||
public string? Version { get; }
|
public string? Version { get; }
|
||||||
|
|
||||||
|
public uint TlsModuleId { get; }
|
||||||
|
|
||||||
|
public ulong TlsMemorySize { get; }
|
||||||
|
|
||||||
|
/// <summary>Variant II distance from the thread pointer to this module's static TLS base.</summary>
|
||||||
|
public ulong TlsStaticOffset { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -10,16 +11,12 @@ using SharpEmu.Core;
|
|||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Loader;
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
public sealed class SelfLoader : ISelfLoader
|
public sealed class SelfLoader : ISelfLoader
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
private const uint SelfMagic = 0x4F153D1D;
|
||||||
private const uint ElfMagic = 0x7F454C46;
|
|
||||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
|
||||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
|
||||||
private const ulong SelfSegmentFlag = 0x800;
|
private const ulong SelfSegmentFlag = 0x800;
|
||||||
private const int PageSize = 0x1000;
|
private const int PageSize = 0x1000;
|
||||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||||
@@ -58,11 +55,22 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
private const long DtSceSymTab = 0x61000039;
|
private const long DtSceSymTab = 0x61000039;
|
||||||
private const long DtSceSymTabSize = 0x6100003F;
|
private const long DtSceSymTabSize = 0x6100003F;
|
||||||
|
|
||||||
|
private const uint RelocationTypeNone = 0;
|
||||||
private const uint RelocationTypeAbsolute64 = 1;
|
private const uint RelocationTypeAbsolute64 = 1;
|
||||||
|
private const uint RelocationTypePc32 = 2;
|
||||||
|
private const uint RelocationTypePlt32 = 4;
|
||||||
private const uint RelocationTypeGlobalData = 6;
|
private const uint RelocationTypeGlobalData = 6;
|
||||||
private const uint RelocationTypeJumpSlot = 7;
|
private const uint RelocationTypeJumpSlot = 7;
|
||||||
private const uint RelocationTypeRelative = 8;
|
private const uint RelocationTypeRelative = 8;
|
||||||
private const uint RelocationTypeTlsModuleId = 16;
|
private const uint RelocationTypeUnsigned32 = 10;
|
||||||
|
private const uint RelocationTypeSigned32 = 11;
|
||||||
|
private const uint RelocationTypeTlsModuleId = 16; // R_X86_64_DTPMOD64
|
||||||
|
private const uint RelocationTypeTlsDtpOff64 = 17; // R_X86_64_DTPOFF64
|
||||||
|
private const uint RelocationTypeTlsTpOff64 = 18; // R_X86_64_TPOFF64
|
||||||
|
private const uint RelocationTypePc64 = 24;
|
||||||
|
private const uint RelocationTypeSize32 = 32;
|
||||||
|
private const uint RelocationTypeSize64 = 33;
|
||||||
|
private const uint RelocationTypeRelative64 = 38;
|
||||||
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
|
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
|
||||||
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
|
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
|
||||||
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
|
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
|
||||||
@@ -88,6 +96,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
|
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
|
||||||
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
|
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
|
||||||
|
|
||||||
|
static SelfLoader()
|
||||||
|
{
|
||||||
|
RunRelocationSelfChecks();
|
||||||
|
}
|
||||||
|
|
||||||
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
|
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
|
||||||
{
|
{
|
||||||
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
|
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
|
||||||
@@ -156,16 +169,22 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
virtualMemory.Clear();
|
virtualMemory.Clear();
|
||||||
_nextTlsModuleId = 1;
|
_nextTlsModuleId = 1;
|
||||||
|
GuestTlsTemplate.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
|
||||||
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
|
||||||
|
|
||||||
var loadContext = ParseLayout(imageData);
|
var loadContext = ParseLayout(imageData);
|
||||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||||
ValidateElfHeader(elfHeader);
|
ValidateElfHeader(elfHeader);
|
||||||
|
|
||||||
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
|
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
|
||||||
|
var hasTlsSegment = TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var processTlsHeader, out _) &&
|
||||||
|
processTlsHeader.MemorySize != 0;
|
||||||
|
var tlsModuleId = hasTlsSegment
|
||||||
|
? (_nextTlsModuleId == 0 ? 1u : _nextTlsModuleId)
|
||||||
|
: 0u;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} " +
|
||||||
|
$"assigned={tlsModuleId} has_pt_tls={hasTlsSegment}");
|
||||||
|
|
||||||
var totalImageSize = CalculateTotalImageSize(programHeaders);
|
var totalImageSize = CalculateTotalImageSize(programHeaders);
|
||||||
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
|
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
|
||||||
@@ -178,8 +197,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
||||||
{
|
{
|
||||||
|
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}).");
|
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}): {reason}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
imageBase = allocatedBase;
|
imageBase = allocatedBase;
|
||||||
@@ -198,6 +218,13 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
|
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
|
||||||
|
// Register every module before relocations so DTPMOD/DTPOFF/TPOFF use
|
||||||
|
// the module's real PT_TLS identity and Variant II static offset.
|
||||||
|
var tlsInfo = RegisterModuleTlsTemplate(
|
||||||
|
programHeaders,
|
||||||
|
virtualMemory,
|
||||||
|
imageBase,
|
||||||
|
tlsModuleId);
|
||||||
var importStubs = ResolveAndPatchImportStubs(
|
var importStubs = ResolveAndPatchImportStubs(
|
||||||
imageData,
|
imageData,
|
||||||
loadContext,
|
loadContext,
|
||||||
@@ -255,11 +282,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
|
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
|
if (tlsModuleId != 0 && _nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
|
||||||
{
|
{
|
||||||
_nextTlsModuleId++;
|
_nextTlsModuleId++;
|
||||||
}
|
}
|
||||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||||
|
|
||||||
return new SelfImage(
|
return new SelfImage(
|
||||||
loadContext.IsSelf,
|
loadContext.IsSelf,
|
||||||
@@ -276,7 +303,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
procParamAddress,
|
procParamAddress,
|
||||||
applicationInfo.Title,
|
applicationInfo.Title,
|
||||||
applicationInfo.TitleId,
|
applicationInfo.TitleId,
|
||||||
applicationInfo.Version);
|
applicationInfo.Version,
|
||||||
|
tlsModuleId,
|
||||||
|
tlsInfo.MemorySize,
|
||||||
|
tlsInfo.StaticOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
||||||
@@ -325,8 +355,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
||||||
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
|
||||||
{
|
{
|
||||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||||
@@ -348,10 +377,19 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (magic != ElfMagic)
|
// Not a recognized (fake-signed) SELF. Only a bare, decrypted ELF is
|
||||||
|
// acceptable here; anything else — most commonly a still-encrypted
|
||||||
|
// retail eboot — must be reported clearly rather than failing later
|
||||||
|
// with an opaque "not a valid ELF header" message.
|
||||||
|
const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
|
||||||
|
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||||
|
if (leadingWord != ElfMagicBigEndian)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Unsupported executable signature 0x{magic:X8}");
|
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
|
||||||
|
$"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
|
||||||
|
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
|
||||||
|
$"fake-signed (fSELF) image.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
||||||
@@ -414,15 +452,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var virtualAddress = header.VirtualAddress + imageBase;
|
var virtualAddress = header.VirtualAddress + imageBase;
|
||||||
|
|
||||||
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||||
if (header.Alignment > 1)
|
if (header.Alignment > 1)
|
||||||
{
|
{
|
||||||
var vaddrMod = virtualAddress % header.Alignment;
|
var vaddrMod = virtualAddress % header.Alignment;
|
||||||
var offsetMod = header.Offset % header.Alignment;
|
var offsetMod = header.Offset % header.Alignment;
|
||||||
if (vaddrMod != offsetMod)
|
if (vaddrMod != offsetMod)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Segment {index} ELF alignment mismatch! " +
|
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||||
}
|
}
|
||||||
@@ -465,6 +503,43 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ModuleTlsInfo RegisterModuleTlsTemplate(
|
||||||
|
IReadOnlyList<ProgramHeader> programHeaders,
|
||||||
|
IVirtualMemory virtualMemory,
|
||||||
|
ulong imageBase,
|
||||||
|
uint tlsModuleId)
|
||||||
|
{
|
||||||
|
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var tlsHeader, out _) ||
|
||||||
|
tlsHeader.MemorySize == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tdata (initialized) bytes come from the mapped segment; tbss is the
|
||||||
|
// implicitly-zero remainder up to MemorySize.
|
||||||
|
var fileSize = (int)Math.Min(tlsHeader.FileSize, tlsHeader.MemorySize);
|
||||||
|
var initImage = fileSize > 0 ? new byte[fileSize] : [];
|
||||||
|
if (fileSize > 0 &&
|
||||||
|
!virtualMemory.TryRead(imageBase + tlsHeader.VirtualAddress, initImage))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] Failed to read TLS init image at 0x{imageBase + tlsHeader.VirtualAddress:X}; seeding zeros.");
|
||||||
|
initImage = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var staticOffset = GuestTlsTemplate.RegisterModule(
|
||||||
|
tlsModuleId,
|
||||||
|
initImage,
|
||||||
|
tlsHeader.MemorySize,
|
||||||
|
tlsHeader.Alignment,
|
||||||
|
tlsHeader.VirtualAddress);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] Module {tlsModuleId} TLS template: memsz=0x{tlsHeader.MemorySize:X} " +
|
||||||
|
$"filesz=0x{tlsHeader.FileSize:X} align=0x{tlsHeader.Alignment:X} " +
|
||||||
|
$"static_offset=0x{staticOffset:X} total_static=0x{GuestTlsTemplate.StaticTlsSize:X}");
|
||||||
|
return new ModuleTlsInfo(tlsHeader.MemorySize, staticOffset);
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
|
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
|
||||||
ReadOnlySpan<byte> imageData,
|
ReadOnlySpan<byte> imageData,
|
||||||
LoadContext loadContext,
|
LoadContext loadContext,
|
||||||
@@ -544,12 +619,17 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
uint maxSymbolIndex = 0;
|
uint maxSymbolIndex = 0;
|
||||||
foreach (var relocation in relocations)
|
foreach (var relocation in relocations)
|
||||||
{
|
{
|
||||||
|
if (relocation.Type == RelocationTypeNone)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsSupportedRelocationType(relocation.Type))
|
if (!IsSupportedRelocationType(relocation.Type))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relocation.Type is RelocationTypeRelative or RelocationTypeTlsModuleId)
|
if (relocation.Type is RelocationTypeNone or RelocationTypeRelative or RelocationTypeRelative64 or RelocationTypeTlsModuleId)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -633,13 +713,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
importedRelocations = BuildImportedRelocations(descriptors);
|
importedRelocations = BuildImportedRelocations(descriptors);
|
||||||
|
|
||||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, orderedImportNids);
|
var stubImportNids = orderedImportNids
|
||||||
|
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
|
||||||
|
.ToArray();
|
||||||
|
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
||||||
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
||||||
|
|
||||||
int printCount = Math.Min(10, orderedImportNids.Count);
|
int printCount = Math.Min(10, stubImportNids.Length);
|
||||||
for (int i = 0; i < printCount; i++)
|
for (int i = 0; i < printCount; i++)
|
||||||
{
|
{
|
||||||
var nid = orderedImportNids[i];
|
var nid = stubImportNids[i];
|
||||||
var addr = stubsByAddress.First(x => x.Value == nid).Key;
|
var addr = stubsByAddress.First(x => x.Value == nid).Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,45 +746,65 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
foreach (var descriptor in descriptors)
|
foreach (var descriptor in descriptors)
|
||||||
{
|
{
|
||||||
ulong targetValue;
|
ulong symbolValue;
|
||||||
if (descriptor.ImportNid is null)
|
if (descriptor.ImportNid is null)
|
||||||
{
|
{
|
||||||
targetValue = AddSigned(descriptor.SymbolValue, descriptor.Addend);
|
symbolValue = descriptor.SymbolValue;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
|
if (addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
|
symbolValue = stubAddress;
|
||||||
}
|
}
|
||||||
|
else if (descriptor.IsWeak)
|
||||||
targetValue = AddSigned(stubAddress, descriptor.Addend);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetValue < 0x1000)
|
|
||||||
{
|
|
||||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
|
||||||
{
|
{
|
||||||
Log.Debug(
|
// ELF unresolved weak definitions use S=0. They must not
|
||||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
// receive a trap import stub, which would turn a permitted
|
||||||
|
// null test into a call to an unresolved-import handler.
|
||||||
|
symbolValue = 0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
|
||||||
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt64(virtualMemory, descriptor.TargetAddress, targetValue))
|
var targetValue = ComputeRelocationValue(descriptor, symbolValue);
|
||||||
|
|
||||||
|
if (targetValue < 0x1000 && descriptor.ValueKind is
|
||||||
|
RelocationValueKind.TlsOffset or
|
||||||
|
RelocationValueKind.PcRelative or
|
||||||
|
RelocationValueKind.SymbolSize)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}.");
|
// A TLS offset (TPOFF64/DTPOFF64) is a signed displacement, not a
|
||||||
|
// mapped address, so a small or negative value here is expected.
|
||||||
|
}
|
||||||
|
else if (targetValue < 0x1000 && !descriptor.IsWeak)
|
||||||
|
{
|
||||||
|
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||||
|
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRelocationValue(virtualMemory, descriptor, targetValue, out var writeError))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}: {writeError}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,29 +897,36 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSupportedRelocationType(relocation.Type))
|
if (!IsSupportedRelocationType(relocation.Type))
|
||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
// Surface unsupported relocation types loudly instead of
|
||||||
|
// skipping silently: TLS-relative (17/18), COPY (5), and
|
||||||
|
// IRELATIVE/ifunc (37) leave their targets unrelocated, which
|
||||||
|
// manifests later as reads of zero or calls into address 0.
|
||||||
|
ReportUnsupportedRelocation(relocation.Type, relocation.Offset, imageBase);
|
||||||
|
if (relocation.Type is 5 or 37)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
throw new NotSupportedException(
|
||||||
|
$"Relocation type {relocation.Type} requires deferred runtime-linker processing.");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress))
|
var relocationWriteSize = GetRelocationWriteSize(relocation.Type);
|
||||||
|
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, relocationWriteSize, out var targetAddress))
|
||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relocation.Type == RelocationTypeRelative)
|
if (relocation.Type is RelocationTypeRelative or RelocationTypeRelative64)
|
||||||
{
|
{
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
@@ -830,7 +940,12 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (relocation.Type == RelocationTypeTlsModuleId)
|
if (relocation.Type == RelocationTypeTlsModuleId)
|
||||||
{
|
{
|
||||||
var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId;
|
if (tlsModuleId == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"R_X86_64_DTPMOD64 at 0x{targetAddress:X16} references a module without PT_TLS.");
|
||||||
|
}
|
||||||
|
var dtpmodValue = tlsModuleId;
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
0,
|
0,
|
||||||
@@ -851,50 +966,100 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend;
|
if (relocation.Type is RelocationTypeTlsDtpOff64 or RelocationTypeTlsTpOff64)
|
||||||
var symbolBind = GetSymbolBind(symbol.Info);
|
|
||||||
if (symbolBind == SymbolBindLocal)
|
|
||||||
{
|
{
|
||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
// Variant II static TLS: the module block sits at
|
||||||
if (symbolAddress == 0)
|
// [tp - blockSize, tp). DTPOFF64 is the module-relative offset
|
||||||
|
// (st_value + addend); TPOFF64 is that offset expressed relative
|
||||||
|
// to the thread pointer, i.e. minus the aligned block size.
|
||||||
|
var tlsSymbolOffset = AddSigned(symbol.Value, relocation.Addend);
|
||||||
|
if (!GuestTlsTemplate.TryGetStaticOffset(tlsModuleId, out var moduleStaticOffset))
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"[LOADER][TLS] Missing PT_TLS registration for module {tlsModuleId}; " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"cannot apply relocation {relocation.Type} at 0x{targetAddress:X16}.");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
var tlsValue = relocation.Type == RelocationTypeTlsTpOff64
|
||||||
|
? unchecked(tlsSymbolOffset - moduleStaticOffset)
|
||||||
|
: tlsSymbolOffset;
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
addend,
|
0,
|
||||||
null,
|
null,
|
||||||
symbolAddress,
|
tlsValue,
|
||||||
RelocationValueKind.Pointer,
|
RelocationValueKind.TlsOffset,
|
||||||
IsDataImport: false));
|
IsDataImport: false));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var symbolBind = GetSymbolBind(symbol.Info);
|
||||||
|
if (symbolIndex == 0)
|
||||||
|
{
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: false));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symbolBind == SymbolBindLocal)
|
||||||
|
{
|
||||||
|
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
|
||||||
|
? 0
|
||||||
|
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
|
if (symbolAddress == 0)
|
||||||
|
{
|
||||||
|
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: false));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (symbol.Value != 0)
|
if (symbol.Value != 0)
|
||||||
{
|
{
|
||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
|
||||||
|
? 0
|
||||||
|
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
if (symbolAddress == 0)
|
if (symbolAddress == 0)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
|
||||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
{
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
targetAddress,
|
targetAddress,
|
||||||
addend,
|
symbol,
|
||||||
null,
|
|
||||||
symbolAddress,
|
symbolAddress,
|
||||||
RelocationValueKind.Pointer,
|
importNid: null,
|
||||||
IsDataImport: false));
|
isWeak: false));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,16 +1067,26 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
|
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
|
||||||
{
|
{
|
||||||
|
if (symbolBind == SymbolBindWeak)
|
||||||
|
{
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: true));
|
||||||
|
}
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -919,6 +1094,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var nid = ExtractNid(symbolName);
|
var nid = ExtractNid(symbolName);
|
||||||
if (string.IsNullOrWhiteSpace(nid))
|
if (string.IsNullOrWhiteSpace(nid))
|
||||||
{
|
{
|
||||||
|
if (symbolBind == SymbolBindWeak)
|
||||||
|
{
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: true));
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,16 +1112,74 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
orderedImportNids.Add(nid);
|
orderedImportNids.Add(nid);
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
targetAddress,
|
targetAddress,
|
||||||
addend,
|
symbol,
|
||||||
nid,
|
symbolAddress: 0,
|
||||||
0,
|
importNid: nid,
|
||||||
RelocationValueKind.Pointer,
|
isWeak: symbolBind == SymbolBindWeak));
|
||||||
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RelocationDescriptor CreateSymbolRelocationDescriptor(
|
||||||
|
ElfRelocation relocation,
|
||||||
|
ulong targetAddress,
|
||||||
|
ElfSymbol symbol,
|
||||||
|
ulong symbolAddress,
|
||||||
|
string? importNid,
|
||||||
|
bool isWeak)
|
||||||
|
{
|
||||||
|
var valueKind = relocation.Type switch
|
||||||
|
{
|
||||||
|
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypePc64 => RelocationValueKind.PcRelative,
|
||||||
|
RelocationTypeSize32 or RelocationTypeSize64 => RelocationValueKind.SymbolSize,
|
||||||
|
_ => RelocationValueKind.Pointer,
|
||||||
|
};
|
||||||
|
var writeKind = relocation.Type switch
|
||||||
|
{
|
||||||
|
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypeSigned32 => RelocationWriteKind.Int32,
|
||||||
|
RelocationTypeUnsigned32 or RelocationTypeSize32 => RelocationWriteKind.UInt32,
|
||||||
|
_ => RelocationWriteKind.UInt64,
|
||||||
|
};
|
||||||
|
var symbolValue = valueKind == RelocationValueKind.SymbolSize
|
||||||
|
? symbol.Size
|
||||||
|
: symbolAddress;
|
||||||
|
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot
|
||||||
|
? 0
|
||||||
|
: relocation.Addend;
|
||||||
|
return new RelocationDescriptor(
|
||||||
|
targetAddress,
|
||||||
|
addend,
|
||||||
|
importNid,
|
||||||
|
symbolValue,
|
||||||
|
valueKind,
|
||||||
|
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject,
|
||||||
|
writeKind,
|
||||||
|
isWeak);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldCreateImportStub(
|
||||||
|
string nid,
|
||||||
|
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||||
|
IModuleManager? moduleManager)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < descriptors.Count; i++)
|
||||||
|
{
|
||||||
|
var descriptor = descriptors[i];
|
||||||
|
if (!string.Equals(descriptor.ImportNid, nid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!descriptor.IsWeak || moduleManager?.TryGetExport(nid, out _) == true)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static void RegisterRuntimeSymbolsAndHooks(
|
private static void RegisterRuntimeSymbolsAndHooks(
|
||||||
ReadOnlySpan<byte> imageData,
|
ReadOnlySpan<byte> imageData,
|
||||||
LoadContext loadContext,
|
LoadContext loadContext,
|
||||||
@@ -959,8 +1202,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,8 +1277,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (preInitializers.Count != 0 || initializers.Count != 0)
|
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1418,8 +1661,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||||
if (mapSize > int.MaxValue)
|
if (mapSize > int.MaxValue)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||||
@@ -1691,11 +1934,51 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
private static bool IsSupportedRelocationType(uint relocationType)
|
private static bool IsSupportedRelocationType(uint relocationType)
|
||||||
{
|
{
|
||||||
return relocationType is
|
return relocationType is
|
||||||
|
RelocationTypeNone or
|
||||||
RelocationTypeAbsolute64 or
|
RelocationTypeAbsolute64 or
|
||||||
|
RelocationTypePc32 or
|
||||||
|
RelocationTypePlt32 or
|
||||||
RelocationTypeGlobalData or
|
RelocationTypeGlobalData or
|
||||||
RelocationTypeJumpSlot or
|
RelocationTypeJumpSlot or
|
||||||
RelocationTypeRelative or
|
RelocationTypeRelative or
|
||||||
RelocationTypeTlsModuleId;
|
RelocationTypeUnsigned32 or
|
||||||
|
RelocationTypeSigned32 or
|
||||||
|
RelocationTypeTlsModuleId or
|
||||||
|
RelocationTypeTlsDtpOff64 or
|
||||||
|
RelocationTypeTlsTpOff64 or
|
||||||
|
RelocationTypePc64 or
|
||||||
|
RelocationTypeSize32 or
|
||||||
|
RelocationTypeSize64 or
|
||||||
|
RelocationTypeRelative64;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<uint> _reportedUnsupportedRelocationTypes = new();
|
||||||
|
|
||||||
|
private static void ReportUnsupportedRelocation(uint relocationType, ulong offset, ulong imageBase)
|
||||||
|
{
|
||||||
|
// Report each distinct unsupported type once to keep the log useful.
|
||||||
|
lock (_reportedUnsupportedRelocationTypes)
|
||||||
|
{
|
||||||
|
if (!_reportedUnsupportedRelocationTypes.Add(relocationType))
|
||||||
|
{
|
||||||
|
if (IsFocusRelocationOffset(offset, imageBase))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocationType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = relocationType switch
|
||||||
|
{
|
||||||
|
5 => "R_X86_64_COPY",
|
||||||
|
37 => "R_X86_64_IRELATIVE (ifunc)",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] Unsupported relocation type {relocationType} ({name}) rejected " +
|
||||||
|
$"(first at off=0x{offset:X16}); COPY requires dependency symbol storage and IRELATIVE requires resolver execution.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong DetermineRequestedImageBase(
|
private static ulong DetermineRequestedImageBase(
|
||||||
@@ -1851,16 +2134,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||||
|
|
||||||
var guestAddr = location + imageBase;
|
var guestAddr = location + imageBase;
|
||||||
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||||
{
|
{
|
||||||
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (virtualMemory.TryRead(location, tableBytes))
|
if (virtualMemory.TryRead(location, tableBytes))
|
||||||
{
|
{
|
||||||
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1868,11 +2151,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
var slice = elfData.Slice((int)location, (int)size);
|
var slice = elfData.Slice((int)location, (int)size);
|
||||||
tableBytes = slice.ToArray();
|
tableBytes = slice.ToArray();
|
||||||
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||||
tableBytes = Array.Empty<byte>();
|
tableBytes = Array.Empty<byte>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1929,17 +2212,17 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||||
{
|
{
|
||||||
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||||
|
|
||||||
if (address == 0)
|
if (address == 0)
|
||||||
{
|
{
|
||||||
Log.Debug("-> return 0 (null)");
|
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
||||||
{
|
{
|
||||||
Log.Debug($"-> resolved raw 0x{resolved:X16}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1948,18 +2231,18 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var rebased = address + imageBase;
|
var rebased = address + imageBase;
|
||||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
||||||
{
|
{
|
||||||
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
||||||
return resolvedRebased;
|
return resolvedRebased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address < 0x10000)
|
if (address < 0x10000)
|
||||||
{
|
{
|
||||||
Log.Debug($"-> reject small 0x{address:X}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2049,6 +2332,106 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return virtualMemory.TryWrite(address, buffer);
|
return virtualMemory.TryWrite(address, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeRelocationValue(RelocationDescriptor descriptor, ulong resolvedSymbolValue)
|
||||||
|
{
|
||||||
|
var baseValue = descriptor.ValueKind == RelocationValueKind.SymbolSize
|
||||||
|
? descriptor.SymbolValue
|
||||||
|
: resolvedSymbolValue;
|
||||||
|
var value = AddSigned(baseValue, descriptor.Addend);
|
||||||
|
return descriptor.ValueKind == RelocationValueKind.PcRelative
|
||||||
|
? unchecked(value - descriptor.TargetAddress)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteRelocationValue(
|
||||||
|
IVirtualMemory virtualMemory,
|
||||||
|
RelocationDescriptor descriptor,
|
||||||
|
ulong value,
|
||||||
|
out string? error)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
var length = sizeof(ulong);
|
||||||
|
switch (descriptor.WriteKind)
|
||||||
|
{
|
||||||
|
case RelocationWriteKind.UInt64:
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RelocationWriteKind.UInt32:
|
||||||
|
if (value > uint.MaxValue)
|
||||||
|
{
|
||||||
|
error = $"value 0x{value:X16} overflows an unsigned 32-bit relocation";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)value);
|
||||||
|
length = sizeof(uint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RelocationWriteKind.Int32:
|
||||||
|
var signedValue = unchecked((long)value);
|
||||||
|
if (signedValue is < int.MinValue or > int.MaxValue)
|
||||||
|
{
|
||||||
|
error = $"value {signedValue} overflows a signed 32-bit relocation";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(buffer, (int)signedValue);
|
||||||
|
length = sizeof(int);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error = $"unknown relocation write kind {descriptor.WriteKind}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = virtualMemory.TryWrite(descriptor.TargetAddress, buffer[..length])
|
||||||
|
? null
|
||||||
|
: "target memory is not writable";
|
||||||
|
return error is null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetRelocationWriteSize(uint relocationType)
|
||||||
|
{
|
||||||
|
return relocationType is
|
||||||
|
RelocationTypePc32 or
|
||||||
|
RelocationTypePlt32 or
|
||||||
|
RelocationTypeUnsigned32 or
|
||||||
|
RelocationTypeSigned32 or
|
||||||
|
RelocationTypeSize32
|
||||||
|
? sizeof(uint)
|
||||||
|
: sizeof(ulong);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void RunRelocationSelfChecks()
|
||||||
|
{
|
||||||
|
var pc32 = new RelocationDescriptor(
|
||||||
|
TargetAddress: 0x1000,
|
||||||
|
Addend: -4,
|
||||||
|
ImportNid: null,
|
||||||
|
SymbolValue: 0x1800,
|
||||||
|
RelocationValueKind.PcRelative,
|
||||||
|
IsDataImport: false,
|
||||||
|
RelocationWriteKind.Int32);
|
||||||
|
Debug.Assert(
|
||||||
|
unchecked((long)ComputeRelocationValue(pc32, pc32.SymbolValue)) == 0x7FC,
|
||||||
|
"R_X86_64_PC32 did not apply S + A - P.");
|
||||||
|
|
||||||
|
var weak = new RelocationDescriptor(
|
||||||
|
TargetAddress: 0x2000,
|
||||||
|
Addend: 7,
|
||||||
|
ImportNid: "weak",
|
||||||
|
SymbolValue: 0,
|
||||||
|
RelocationValueKind.Pointer,
|
||||||
|
IsDataImport: false,
|
||||||
|
IsWeak: true);
|
||||||
|
Debug.Assert(
|
||||||
|
ComputeRelocationValue(weak, resolvedSymbolValue: 0) == 7,
|
||||||
|
"An unresolved weak relocation did not use S=0.");
|
||||||
|
Debug.Assert(
|
||||||
|
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
||||||
|
"An unresolved weak symbol incorrectly received a trap import stub.");
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
{
|
{
|
||||||
var mask = alignment - 1;
|
var mask = alignment - 1;
|
||||||
@@ -2257,6 +2640,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
|
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The CPU backend executes guest instructions natively, so a non
|
||||||
|
// x86-64 image can only fail deep in execution with an opaque SIGILL.
|
||||||
|
// Catch it here with an actionable message. (EM_X86_64 == 62.)
|
||||||
|
const ushort ElfMachineX86_64 = 62;
|
||||||
|
if (header.Machine != ElfMachineX86_64)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Unsupported ELF machine type 0x{header.Machine:X4}; SharpEmu only runs x86-64 (EM_X86_64) images.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
|
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
|
||||||
@@ -2337,10 +2730,25 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
public uint Type => (uint)(Info & uint.MaxValue);
|
public uint Type => (uint)(Info & uint.MaxValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct ModuleTlsInfo(ulong MemorySize, ulong StaticOffset);
|
||||||
|
|
||||||
private enum RelocationValueKind : byte
|
private enum RelocationValueKind : byte
|
||||||
{
|
{
|
||||||
Pointer = 0,
|
Pointer = 0,
|
||||||
TlsModuleId = 1
|
TlsModuleId = 1,
|
||||||
|
// A pre-computed TLS offset written verbatim (TPOFF64/DTPOFF64). Unlike
|
||||||
|
// Pointer it is a signed displacement, not a mapped address, so it is
|
||||||
|
// patched as-is without the low-address validity warning.
|
||||||
|
TlsOffset = 2,
|
||||||
|
PcRelative = 3,
|
||||||
|
SymbolSize = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum RelocationWriteKind : byte
|
||||||
|
{
|
||||||
|
UInt64 = 0,
|
||||||
|
UInt32 = 1,
|
||||||
|
Int32 = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RelocationDescriptor(
|
private readonly record struct RelocationDescriptor(
|
||||||
@@ -2349,7 +2757,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
string? ImportNid,
|
string? ImportNid,
|
||||||
ulong SymbolValue,
|
ulong SymbolValue,
|
||||||
RelocationValueKind ValueKind,
|
RelocationValueKind ValueKind,
|
||||||
bool IsDataImport);
|
bool IsDataImport,
|
||||||
|
RelocationWriteKind WriteKind = RelocationWriteKind.UInt64,
|
||||||
|
bool IsWeak = false);
|
||||||
|
|
||||||
private enum SelfSegmentResolveStatus
|
private enum SelfSegmentResolveStatus
|
||||||
{
|
{
|
||||||
@@ -2389,14 +2799,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
public ulong FileSize => _fileSize;
|
public ulong FileSize => _fileSize;
|
||||||
|
|
||||||
public bool HasKnownLayout =>
|
public bool HasKnownLayout =>
|
||||||
((_ident0 == 0x4F &&
|
_ident0 == 0x4F &&
|
||||||
_ident1 == 0x15 &&
|
_ident1 == 0x15 &&
|
||||||
_ident2 == 0x3D &&
|
_ident2 == 0x3D &&
|
||||||
_ident3 == 0x1D) ||
|
_ident3 == 0x1D &&
|
||||||
(_ident0 == 0x54 &&
|
|
||||||
_ident1 == 0x14 &&
|
|
||||||
_ident2 == 0xF5 &&
|
|
||||||
_ident3 == 0xEE)) &&
|
|
||||||
_ident4 == 0x00 &&
|
_ident4 == 0x00 &&
|
||||||
_ident5 == 0x01 &&
|
_ident5 == 0x01 &&
|
||||||
_ident6 == 0x01 &&
|
_ident6 == 0x01 &&
|
||||||
|
|||||||
@@ -48,7 +48,107 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
{
|
{
|
||||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
_hostMemory = hostMemory ?? CrossPlatformHostMemory.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CrossPlatformHostMemory : IHostMemory
|
||||||
|
{
|
||||||
|
public static readonly CrossPlatformHostMemory Instance = new();
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
unchecked((ulong)HostMemory.Alloc(
|
||||||
|
(void*)desiredAddress,
|
||||||
|
(nuint)size,
|
||||||
|
HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT,
|
||||||
|
ToRawProtection(protection)));
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
unchecked((ulong)HostMemory.Alloc(
|
||||||
|
(void*)desiredAddress,
|
||||||
|
(nuint)size,
|
||||||
|
HostMemory.MEM_RESERVE,
|
||||||
|
ToRawProtection(protection)));
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) =>
|
||||||
|
HostMemory.Alloc(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
HostMemory.MEM_COMMIT,
|
||||||
|
ToRawProtection(protection)) != null;
|
||||||
|
|
||||||
|
public bool Free(ulong address) =>
|
||||||
|
HostMemory.Free((void*)address, 0, HostMemory.MEM_RELEASE);
|
||||||
|
|
||||||
|
public bool Protect(
|
||||||
|
ulong address,
|
||||||
|
ulong size,
|
||||||
|
HostPageProtection protection,
|
||||||
|
out uint rawOldProtection) =>
|
||||||
|
HostMemory.Protect(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
ToRawProtection(protection),
|
||||||
|
out rawOldProtection);
|
||||||
|
|
||||||
|
public bool ProtectRaw(
|
||||||
|
ulong address,
|
||||||
|
ulong size,
|
||||||
|
uint rawProtection,
|
||||||
|
out uint rawOldProtection) =>
|
||||||
|
HostMemory.Protect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
|
||||||
|
|
||||||
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
|
{
|
||||||
|
if (HostMemory.Query((void*)address, out var raw) == 0)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = raw.State switch
|
||||||
|
{
|
||||||
|
HostMemory.MEM_FREE_STATE => HostRegionState.Free,
|
||||||
|
HostMemory.MEM_RESERVE => HostRegionState.Reserved,
|
||||||
|
_ => HostRegionState.Committed,
|
||||||
|
};
|
||||||
|
|
||||||
|
info = new HostRegionInfo(
|
||||||
|
raw.BaseAddress,
|
||||||
|
raw.AllocationBase,
|
||||||
|
raw.RegionSize,
|
||||||
|
state,
|
||||||
|
raw.State,
|
||||||
|
FromRawProtection(raw.Protect),
|
||||||
|
raw.Protect,
|
||||||
|
raw.AllocationProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size) =>
|
||||||
|
HostMemory.FlushInstructionCache((void*)address, (nuint)size);
|
||||||
|
|
||||||
|
private static uint ToRawProtection(HostPageProtection protection) => protection switch
|
||||||
|
{
|
||||||
|
HostPageProtection.NoAccess => HostMemory.PAGE_NOACCESS,
|
||||||
|
HostPageProtection.ReadOnly => HostMemory.PAGE_READONLY,
|
||||||
|
HostPageProtection.ReadWrite => HostMemory.PAGE_READWRITE,
|
||||||
|
HostPageProtection.Execute => HostMemory.PAGE_EXECUTE,
|
||||||
|
HostPageProtection.ReadExecute => HostMemory.PAGE_EXECUTE_READ,
|
||||||
|
HostPageProtection.ReadWriteExecute => HostMemory.PAGE_EXECUTE_READWRITE,
|
||||||
|
HostPageProtection.ExecuteWriteCopy => 0x80,
|
||||||
|
_ => HostMemory.PAGE_NOACCESS,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostPageProtection FromRawProtection(uint protection) => protection switch
|
||||||
|
{
|
||||||
|
HostMemory.PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||||
|
HostMemory.PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||||
|
HostMemory.PAGE_EXECUTE => HostPageProtection.Execute,
|
||||||
|
HostMemory.PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||||
|
HostMemory.PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||||
|
0x80 => HostPageProtection.ExecuteWriteCopy,
|
||||||
|
_ => HostPageProtection.NoAccess,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
@@ -98,6 +198,24 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string DescribeAddressForDiagnostics(ulong address)
|
||||||
|
{
|
||||||
|
if (!_hostMemory.Query(address, out var info))
|
||||||
|
{
|
||||||
|
return "unable to query host memory at this address";
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.State switch
|
||||||
|
{
|
||||||
|
HostRegionState.Free => "address reports free, but the exact-address reservation still failed",
|
||||||
|
HostRegionState.Reserved =>
|
||||||
|
$"already reserved by another host allocation (base=0x{info.AllocationBase:X16}, size=0x{info.RegionSize:X})",
|
||||||
|
HostRegionState.Committed =>
|
||||||
|
$"already committed by another host allocation (base=0x{info.AllocationBase:X16}, size=0x{info.RegionSize:X}, protect=0x{info.RawProtection:X})",
|
||||||
|
_ => $"in an unexpected host state (raw=0x{info.RawState:X})",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true)
|
public ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true)
|
||||||
{
|
{
|
||||||
if (size == 0)
|
if (size == 0)
|
||||||
@@ -249,42 +367,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||||
|
|
||||||
// Under Rosetta 2 the kernel can ignore placement hints for whole
|
// macOS needs alignment over-allocation; Linux uses exact-address search.
|
||||||
// windows, so page-stepped exact probes are pathological on macOS.
|
|
||||||
// Linux must keep using the exact-address search below: PS5 resource
|
|
||||||
// descriptors cannot represent ordinary 0x7F... host mappings. Linux
|
|
||||||
// HostMemory uses MAP_FIXED_NOREPLACE, making those low-address probes
|
|
||||||
// safe without clobbering existing host mappings.
|
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
// Prefer the requested low address. Besides matching the guest
|
|
||||||
// address model, this keeps the allocation representable by every
|
|
||||||
// PS5 GPU descriptor (the strictest ones carry 40 address bits).
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var exactAddress = AllocateAt(
|
|
||||||
cursor,
|
|
||||||
alignedSize,
|
|
||||||
executable,
|
|
||||||
allowAlternative: false);
|
|
||||||
if (exactAddress == cursor)
|
|
||||||
{
|
|
||||||
actualAddress = exactAddress;
|
|
||||||
UpdateAllocationSearchCursor(
|
|
||||||
desiredAddress,
|
|
||||||
effectiveAlignment,
|
|
||||||
executable,
|
|
||||||
exactAddress + alignedSize);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// Over-allocate by the alignment so a kernel-chosen placement
|
|
||||||
// always contains an aligned start; the unused head/tail stays
|
|
||||||
// part of the tracked region and is simply never handed out.
|
|
||||||
var reserveSize = effectiveAlignment > PageSize
|
var reserveSize = effectiveAlignment > PageSize
|
||||||
? alignedSize + effectiveAlignment
|
? alignedSize + effectiveAlignment
|
||||||
: alignedSize;
|
: alignedSize;
|
||||||
@@ -294,10 +379,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
if (posixAddress != 0)
|
if (posixAddress != 0)
|
||||||
{
|
{
|
||||||
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
||||||
const ulong gpuAddressLimit = 1UL << 40;
|
if (alignedBase + alignedSize <= posixAddress + reserveSize)
|
||||||
if (alignedBase < gpuAddressLimit &&
|
|
||||||
alignedSize <= gpuAddressLimit - alignedBase &&
|
|
||||||
alignedBase + alignedSize <= posixAddress + reserveSize)
|
|
||||||
{
|
{
|
||||||
actualAddress = alignedBase;
|
actualAddress = alignedBase;
|
||||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
|
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
|
||||||
@@ -327,19 +409,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (TryAllocateAtExact(cursor, alignedSize, executable, out actualAddress))
|
||||||
{
|
|
||||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
|
||||||
if (actualAddress == cursor)
|
|
||||||
{
|
|
||||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
actualAddress = 0;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
|
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
|
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
|
||||||
|
|||||||
@@ -7,25 +7,22 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
using SharpEmu.Libs.SaveData;
|
using SharpEmu.Libs.SaveData;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Libs.Fiber;
|
||||||
|
using SharpEmu.Libs.SystemService;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Runtime;
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
private readonly record struct LoadedModuleImage(string Path, SelfImage Image, int Handle, bool StartAtBoot);
|
||||||
|
|
||||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
|
||||||
|
|
||||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
@@ -89,19 +86,14 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
||||||
moduleManager.Freeze();
|
moduleManager.Freeze();
|
||||||
|
|
||||||
// Resolve the host platform once at the composition root; on unsupported
|
var virtualMemory = new PhysicalVirtualMemory();
|
||||||
// OSes this throws PlatformNotSupportedException with a clear message
|
|
||||||
// instead of failing on the first native call.
|
|
||||||
var hostPlatform = HostPlatform.Current;
|
|
||||||
|
|
||||||
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
|
|
||||||
|
|
||||||
var fileSystem = new PhysicalFileSystem();
|
var fileSystem = new PhysicalFileSystem();
|
||||||
|
|
||||||
return new SharpEmuRuntime(
|
return new SharpEmuRuntime(
|
||||||
new SelfLoader(),
|
new SelfLoader(),
|
||||||
virtualMemory,
|
virtualMemory,
|
||||||
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
new CpuDispatcher(virtualMemory, moduleManager),
|
||||||
moduleManager,
|
moduleManager,
|
||||||
Aerolib.Instance,
|
Aerolib.Instance,
|
||||||
cpuExecutionOptions,
|
cpuExecutionOptions,
|
||||||
@@ -139,20 +131,21 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||||
Log.Info($"Loading: {ebootPath}");
|
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||||
LastExecutionDiagnostics = null;
|
LastExecutionDiagnostics = null;
|
||||||
LastExecutionTrace = null;
|
LastExecutionTrace = null;
|
||||||
LastSessionSummary = null;
|
LastSessionSummary = null;
|
||||||
LastBasicBlockTrace = null;
|
LastBasicBlockTrace = null;
|
||||||
LastMilestoneLog = null;
|
LastMilestoneLog = null;
|
||||||
|
FiberExports.ResetRuntimeState();
|
||||||
KernelModuleRegistry.Reset();
|
KernelModuleRegistry.Reset();
|
||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
LogAppBundleInfo(normalizedEbootPath, image);
|
SystemServiceExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||||
@@ -175,7 +168,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName);
|
processImageName);
|
||||||
if (initializerResult is { } failedInitializerResult)
|
if (initializerResult is { } failedInitializerResult)
|
||||||
{
|
{
|
||||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -183,8 +176,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return failedInitializerResult;
|
return failedInitializerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info($"Dispatching, gen: {generation}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
@@ -194,8 +187,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName,
|
processImageName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
Log.Info($"DispatchEntry returned: {result}");
|
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||||
Log.Info($"Dispatch result: {result}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -366,66 +359,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
|
||||||
{
|
|
||||||
var executableName = Path.GetFileName(ebootPath);
|
|
||||||
if (string.IsNullOrWhiteSpace(executableName))
|
|
||||||
{
|
|
||||||
executableName = "eboot.bin";
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
|
||||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
|
||||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
|
||||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("App bundle info:");
|
|
||||||
builder.AppendLine($"- Display name: {displayName}");
|
|
||||||
builder.AppendLine($"- Version: {version}");
|
|
||||||
builder.AppendLine($"- Title ID: {titleId}");
|
|
||||||
builder.AppendLine($"- Content ID: {contentId}");
|
|
||||||
builder.AppendLine($"- Executable: {executableName}");
|
|
||||||
builder.Append("- Platform: PlayStation 5");
|
|
||||||
Log.Info(builder.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ResolveContentId(string? bundleRoot)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(bundleRoot))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var candidate in new[]
|
|
||||||
{
|
|
||||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
|
||||||
Path.Combine(bundleRoot, "param.json"),
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if (!File.Exists(candidate))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
|
||||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
|
||||||
{
|
|
||||||
return contentId.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||||
{
|
{
|
||||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||||
@@ -440,6 +373,20 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dump tools commonly place decrypted executables in an app0/decrypted
|
||||||
|
// sidecar while leaving Unity data in the parent app0 directory. Keep
|
||||||
|
// loading code/modules beside the decrypted eboot, but mount /app0 at
|
||||||
|
// the content-bearing parent so boot.config, metadata and assets resolve.
|
||||||
|
if (string.Equals(Path.GetFileName(app0Root), "decrypted", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var parentRoot = Path.GetDirectoryName(app0Root);
|
||||||
|
if (!string.IsNullOrWhiteSpace(parentRoot) &&
|
||||||
|
Directory.Exists(Path.Combine(parentRoot, "Media")))
|
||||||
|
{
|
||||||
|
app0Root = parentRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
|
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
|
||||||
return new App0BindingScope(app0VariableName);
|
return new App0BindingScope(app0VariableName);
|
||||||
}
|
}
|
||||||
@@ -485,6 +432,56 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetEhFrameInfo(
|
||||||
|
SelfImage image,
|
||||||
|
ulong imageSize,
|
||||||
|
out ulong ehFrameHeaderAddress,
|
||||||
|
out ulong ehFrameAddress,
|
||||||
|
out ulong ehFrameSize)
|
||||||
|
{
|
||||||
|
ehFrameHeaderAddress = 0;
|
||||||
|
ehFrameAddress = 0;
|
||||||
|
ehFrameSize = 0;
|
||||||
|
var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint
|
||||||
|
? image.EntryPoint - image.ElfHeader.EntryPoint
|
||||||
|
: 0UL;
|
||||||
|
for (var i = 0; i < image.ProgramHeaders.Count; i++)
|
||||||
|
{
|
||||||
|
var header = image.ProgramHeaders[i];
|
||||||
|
if (header.HeaderType != ProgramHeaderType.GnuEhFrame || header.MemorySize < 8)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerAddress = imageBase + header.VirtualAddress;
|
||||||
|
Span<byte> ehHeader = stackalloc byte[8];
|
||||||
|
if (!_virtualMemory.TryRead(headerAddress, ehHeader) ||
|
||||||
|
ehHeader[0] != 1 ||
|
||||||
|
ehHeader[1] != 0x1B)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relativeOffset = BinaryPrimitives.ReadInt32LittleEndian(ehHeader[4..]);
|
||||||
|
ehFrameHeaderAddress = headerAddress;
|
||||||
|
ehFrameAddress = unchecked((ulong)((long)headerAddress + 4 + relativeOffset));
|
||||||
|
if (ehFrameAddress < 0x10000)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relativeEhFrameAddress = ehFrameAddress - imageBase;
|
||||||
|
ehFrameSize = header.VirtualAddress > relativeEhFrameAddress
|
||||||
|
? header.VirtualAddress - relativeEhFrameAddress
|
||||||
|
: imageSize > header.VirtualAddress
|
||||||
|
? imageSize - header.VirtualAddress
|
||||||
|
: 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private OrbisGen2Result? RunPreloadedModuleInitializers(
|
private OrbisGen2Result? RunPreloadedModuleInitializers(
|
||||||
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
||||||
Generation generation,
|
Generation generation,
|
||||||
@@ -494,20 +491,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
for (var i = 0; i < loadedModuleImages.Count; i++)
|
for (var i = 0; i < loadedModuleImages.Count; i++)
|
||||||
{
|
{
|
||||||
var loadedModule = loadedModuleImages[i];
|
var loadedModule = loadedModuleImages[i];
|
||||||
|
if (!loadedModule.StartAtBoot)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint;
|
var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint;
|
||||||
if (initEntryPoint < 0x10000)
|
if (initEntryPoint < 0x10000)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!KernelModuleRegistry.TryBeginModuleStart(loadedModule.Handle, out _))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var moduleName = Path.GetFileName(loadedModule.Path);
|
var moduleName = Path.GetFileName(loadedModule.Path);
|
||||||
if (string.IsNullOrWhiteSpace(moduleName))
|
if (string.IsNullOrWhiteSpace(moduleName))
|
||||||
{
|
{
|
||||||
moduleName = $"module#{i}";
|
moduleName = $"module#{i}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||||
initEntryPoint,
|
initEntryPoint,
|
||||||
@@ -516,10 +523,13 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
activeRuntimeSymbols,
|
activeRuntimeSymbols,
|
||||||
moduleName,
|
moduleName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
KernelModuleRegistry.CompleteModuleStart(
|
||||||
|
loadedModule.Handle,
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Module start failed: {moduleName} -> {result}");
|
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -540,8 +550,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||||
|
|
||||||
var result = RunInitializerList(
|
var result = RunInitializerList(
|
||||||
$"{label}:preinit",
|
$"{label}:preinit",
|
||||||
@@ -580,8 +590,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
initializerAddress,
|
initializerAddress,
|
||||||
@@ -613,12 +623,17 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
var moduleDirectories = new[]
|
var moduleDirectories = new[]
|
||||||
{
|
{
|
||||||
Path.Combine(ebootDirectory, "sce_module"),
|
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
|
||||||
Path.Combine(ebootDirectory, "sce_modules"),
|
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
|
||||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
|
||||||
|
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
|
||||||
|
// them up front so the HLE loader can return a real module handle and dlsym
|
||||||
|
// can resolve their exports, but defer DT_INIT until the guest requests them.
|
||||||
|
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false),
|
||||||
}
|
}
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(Directory.Exists)
|
.Select(group => group.First())
|
||||||
|
.Where(entry => Directory.Exists(entry.Path))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (moduleDirectories.Length == 0)
|
if (moduleDirectories.Length == 0)
|
||||||
@@ -628,28 +643,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
var allModulePaths = moduleDirectories
|
var allModulePaths = moduleDirectories
|
||||||
.SelectMany(directory => Directory
|
.SelectMany(directory => Directory
|
||||||
.EnumerateFiles(directory)
|
.EnumerateFiles(directory.Path)
|
||||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(path =>
|
.Select(path => (Path: path, directory.StartAtBoot)))
|
||||||
|
.Where(entry =>
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(entry.Path);
|
||||||
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
|
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||||
})
|
})
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => group.First())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var modulePaths = allModulePaths
|
var modulePaths = allModulePaths
|
||||||
.Where(ShouldPreloadModule)
|
.Where(entry => ShouldPreloadModule(entry.Path))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var skippedModules = allModulePaths
|
var skippedModules = allModulePaths
|
||||||
.Where(path => !ShouldPreloadModule(path))
|
.Where(entry => !ShouldPreloadModule(entry.Path))
|
||||||
.Select(Path.GetFileName)
|
.Select(entry => Path.GetFileName(entry.Path))
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (skippedModules.Length > 0)
|
if (skippedModules.Length > 0)
|
||||||
{
|
{
|
||||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modulePaths.Length == 0)
|
if (modulePaths.Length == 0)
|
||||||
@@ -657,14 +674,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories.Select(entry => entry.Path))}");
|
||||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||||
var loadedModules = 0;
|
var loadedModules = 0;
|
||||||
var failedModules = 0;
|
var failedModules = 0;
|
||||||
var mergedImportCount = 0;
|
var mergedImportCount = 0;
|
||||||
var mergedSymbolCount = 0;
|
var mergedSymbolCount = 0;
|
||||||
foreach (var modulePath in modulePaths)
|
foreach (var moduleEntry in modulePaths)
|
||||||
{
|
{
|
||||||
|
var modulePath = moduleEntry.Path;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(modulePath);
|
var fileInfo = new FileInfo(modulePath);
|
||||||
@@ -689,25 +707,52 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
|
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
|
||||||
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
|
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
|
||||||
RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
InstallNativePluginCompatibilityHooks(importStubs, moduleImage, modulePath);
|
||||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
var moduleHandle = RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
||||||
|
var moduleName = Path.GetFileName(modulePath);
|
||||||
|
var startAtBoot = moduleEntry.StartAtBoot ||
|
||||||
|
string.Equals(moduleName, "libfmod.prx", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(moduleName, "libfmodstudio.prx", StringComparison.OrdinalIgnoreCase);
|
||||||
|
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage, moduleHandle, startAtBoot));
|
||||||
loadedModules++;
|
loadedModules++;
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
failedModules++;
|
failedModules++;
|
||||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void InstallNativePluginCompatibilityHooks(
|
||||||
|
IDictionary<ulong, string> importStubs,
|
||||||
|
SelfImage moduleImage,
|
||||||
|
string modulePath)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Path.GetFileName(modulePath), "libfmod.prx", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<string> compatibilityNids = ["uPLTdl3psGk"];
|
||||||
|
foreach (var nid in compatibilityNids)
|
||||||
|
{
|
||||||
|
if (moduleImage.RuntimeSymbols.TryGetValue(nid, out var address) && address >= 0x10000)
|
||||||
|
{
|
||||||
|
importStubs[address] = nid;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Installed FMOD compatibility hook: {nid} -> 0x{address:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RebindImportedDataSymbols(
|
private void RebindImportedDataSymbols(
|
||||||
SelfImage mainImage,
|
SelfImage mainImage,
|
||||||
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
||||||
@@ -724,8 +769,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (rebound != 0 || unresolved != 0)
|
if (rebound != 0 || unresolved != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,8 +802,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -770,8 +815,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -780,8 +825,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
rebound++;
|
rebound++;
|
||||||
@@ -817,8 +862,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -912,7 +957,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return !PreloadSkipModules.Contains(fileName);
|
return !PreloadSkipModules.Contains(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
private int RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
||||||
{
|
{
|
||||||
if (!TryComputeImageRange(image, out var baseAddress, out var size))
|
if (!TryComputeImageRange(image, out var baseAddress, out var size))
|
||||||
{
|
{
|
||||||
@@ -920,15 +965,27 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
size = 0;
|
size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = TryGetEhFrameInfo(
|
||||||
|
image,
|
||||||
|
size,
|
||||||
|
out var ehFrameHeaderAddress,
|
||||||
|
out var ehFrameAddress,
|
||||||
|
out var ehFrameSize);
|
||||||
var handle = KernelModuleRegistry.RegisterModule(
|
var handle = KernelModuleRegistry.RegisterModule(
|
||||||
modulePath,
|
modulePath,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
size,
|
size,
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
|
image.InitFunctionEntryPoint,
|
||||||
|
ehFrameHeaderAddress,
|
||||||
|
ehFrameAddress,
|
||||||
|
ehFrameSize,
|
||||||
isMain,
|
isMain,
|
||||||
isSystemModule);
|
isSystemModule);
|
||||||
Log.Info(
|
KernelModuleRegistry.RegisterModuleSymbols(handle, image.RuntimeSymbols);
|
||||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||||
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageReference Include="Iced" />
|
<PackageReference Include="Iced" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public sealed class GuiSettings
|
|||||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||||
public bool DiscordRichPresence { get; set; } = true;
|
public bool DiscordRichPresence { get; set; } = true;
|
||||||
|
|
||||||
|
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
/// <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();
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,11 @@
|
|||||||
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
||||||
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
||||||
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
||||||
|
"Options.Env.WritableApp0.Desc": "Allow titles to create and write files inside their install folder.\nNeeded by unpackaged dumps that write their save or config data under /app0.",
|
||||||
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
||||||
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
||||||
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||||
|
"Options.Env.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
|
||||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||||
"Options.Section.Emulation": "EMULATION",
|
"Options.Section.Emulation": "EMULATION",
|
||||||
"Options.Section.Logging": "LOGGING",
|
"Options.Section.Logging": "LOGGING",
|
||||||
@@ -139,8 +141,25 @@
|
|||||||
"Options.About" : "About",
|
"Options.About" : "About",
|
||||||
"About.Github.Label": "GitHub",
|
"About.Github.Label": "GitHub",
|
||||||
"About.Github.Desc": "Source code, issues and project development.",
|
"About.Github.Desc": "Source code, issues and project development.",
|
||||||
|
"About.Github.LatestCommitLabel": "Latest Commit",
|
||||||
|
"About.Github.LatestCommitDescription": "Latest commit on the main branch",
|
||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Join the community, get support and follow development.",
|
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||||
"About.GithubButton": "Contribute in GitHub!",
|
"About.GithubButton": "Contribute in GitHub!",
|
||||||
"About.DiscordButton": "Join our Discord!"
|
"About.DiscordButton": "Join our Discord!",
|
||||||
|
|
||||||
|
"Updater.Auto.Label": "Check for updates on startup",
|
||||||
|
"Updater.Auto.Desc": "Checks GitHub without delaying startup.",
|
||||||
|
"Updater.Label": "Updates",
|
||||||
|
"Updater.Check": "Check for updates",
|
||||||
|
"Updater.DownloadRestart": "Download and restart",
|
||||||
|
"Updater.Status.Ready": "Current build: {0}",
|
||||||
|
"Updater.Status.Checking": "Checking for updates…",
|
||||||
|
"Updater.Status.Current": "You are up to date ({0}).",
|
||||||
|
"Updater.Status.Available": "A new build is available: {0}",
|
||||||
|
"Updater.Status.Downloading": "Downloading update… {0}%",
|
||||||
|
"Updater.Status.Installing": "Installing update…",
|
||||||
|
"Updater.Status.Timeout": "Update check timed out after 10 seconds.",
|
||||||
|
"Updater.Status.Failed": "Could not check for updates.",
|
||||||
|
"Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,8 @@
|
|||||||
"About.Github.Label": "GitHub",
|
"About.Github.Label": "GitHub",
|
||||||
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Github.LatestCommitLabel": "Último Commit",
|
||||||
|
"About.Github.LatestCommitDescription": "Último commit en la rama main",
|
||||||
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||||
"About.GithubButton": "Contribuye en GitHub!",
|
"About.GithubButton": "Contribuye en GitHub!",
|
||||||
"About.DiscordButton": "Únete a nuestro Discord!"
|
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||||
|
|||||||
@@ -125,5 +125,20 @@
|
|||||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
"Dialog.LogFiles": "Günlük Dosyaları",
|
||||||
|
|
||||||
|
"Updater.Auto.Label": "Açılışta güncellemeleri denetle",
|
||||||
|
"Updater.Auto.Desc": "Açılışı geciktirmeden GitHub'ı denetler.",
|
||||||
|
"Updater.Label": "Güncellemeler",
|
||||||
|
"Updater.Check": "Güncellemeleri denetle",
|
||||||
|
"Updater.DownloadRestart": "İndir ve yeniden başlat",
|
||||||
|
"Updater.Status.Ready": "Mevcut build: {0}",
|
||||||
|
"Updater.Status.Checking": "Güncellemeler denetleniyor…",
|
||||||
|
"Updater.Status.Current": "Güncelsiniz ({0}).",
|
||||||
|
"Updater.Status.Available": "Yeni build mevcut: {0}",
|
||||||
|
"Updater.Status.Downloading": "Güncelleme indiriliyor… %{0}",
|
||||||
|
"Updater.Status.Installing": "Güncelleme kuruluyor…",
|
||||||
|
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
|
||||||
|
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
|
||||||
|
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="AutoUpdateLabel" Text="Check for updates on startup" FontSize="13" />
|
||||||
|
<TextBlock x:Name="AutoUpdateDesc" Text="Checks GitHub without delaying startup."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
|
||||||
|
IsChecked="True" VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
@@ -323,6 +333,40 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Classes="sectionTitle"
|
Classes="sectionTitle"
|
||||||
Text="ABOUT" />
|
Text="ABOUT" />
|
||||||
|
|
||||||
|
<!--Latest commit info-->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||||
|
Width="24" Height="24" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
|
||||||
|
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Column="1" x:Name="LatestCommitHashText" Classes="ghost"
|
||||||
|
Content="Loading…" FontSize="12" FontFamily="Consolas,monospace"
|
||||||
|
VerticalAlignment="Center" IsEnabled="False" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!--Update-->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<Image Source="avares://SharpEmu.GUI/Assets/update-icon.png"
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
||||||
|
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||||
|
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
||||||
|
Content="Check for updates" VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!--Github-->
|
<!--Github-->
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel Grid.Column="0"
|
<StackPanel Grid.Column="0"
|
||||||
@@ -420,6 +464,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_WRITABLE_APP0" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvWritableApp0Desc"
|
||||||
|
Text="Allow titles to create and write files inside their install folder. Needed by unpackaged dumps that write their save or config data under /app0."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvWritableApp0Toggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
@@ -453,6 +508,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="SHARPEMU_LOG_IO" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
<TextBlock x:Name="EnvLogIoDesc"
|
||||||
|
Text="Log file open, read, and path-resolve activity to the console. Use when a game cannot find its data files during boot."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToggleSwitch Grid.Column="1" x:Name="EnvLogIoToggle" OnContent="On" OffContent="Off"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
@@ -57,6 +58,9 @@ public partial class MainWindow : Window
|
|||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
private int _autoScrollTicks;
|
private int _autoScrollTicks;
|
||||||
private int _activePageIndex;
|
private int _activePageIndex;
|
||||||
|
private Updater.UpdateInfo? _availableUpdate;
|
||||||
|
private string _updateStatusKey = "Updater.Status.Ready";
|
||||||
|
private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"];
|
||||||
|
|
||||||
// Discord Rich Presence state.
|
// Discord Rich Presence state.
|
||||||
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
@@ -75,6 +79,10 @@ public partial class MainWindow : Window
|
|||||||
private long _navUpNextAt;
|
private long _navUpNextAt;
|
||||||
private long _navDownNextAt;
|
private long _navDownNextAt;
|
||||||
|
|
||||||
|
//Github http client for latest commit
|
||||||
|
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
|
||||||
|
private string? _latestCommitSha;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -131,17 +139,24 @@ public partial class MainWindow : Window
|
|||||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
};
|
};
|
||||||
|
AutoUpdateToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
_settings.CheckForUpdatesOnStartup = AutoUpdateToggle.IsChecked == true;
|
||||||
|
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
|
||||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||||
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||||
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||||
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||||
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||||
|
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.IsChecked == true);
|
||||||
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||||
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||||
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||||
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
||||||
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
||||||
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
||||||
|
EnvLogIoToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
SetEnvironmentToggle("SHARPEMU_LOG_IO", EnvLogIoToggle.IsChecked == true);
|
||||||
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||||
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||||
@@ -185,6 +200,21 @@ public partial class MainWindow : Window
|
|||||||
UseShellExecute = true
|
UseShellExecute = true
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LatestCommitHashText.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_latestCommitSha))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName =
|
||||||
|
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -226,6 +256,91 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Github http client config ----
|
||||||
|
// This is for getting lash commit id
|
||||||
|
private static HttpClient CreateGithubHttpClient()
|
||||||
|
{
|
||||||
|
var client = new HttpClient
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(15)
|
||||||
|
};
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("SharpEmu/1.0");
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(
|
||||||
|
new MediaTypeWithQualityHeaderValue("application/vnd.github.sha"));
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.Add(
|
||||||
|
"X-GitHub-Api-Version",
|
||||||
|
"2026-03-10");
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
private async Task LoadLatestCommitAsync()
|
||||||
|
{
|
||||||
|
const string apiUrl =
|
||||||
|
"https://api.github.com/repos/sharpemu/sharpemu/commits/main";
|
||||||
|
|
||||||
|
_latestCommitSha = null;
|
||||||
|
LatestCommitHashText.Content = "Loading…";
|
||||||
|
LatestCommitHashText.IsEnabled = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await GithubHttpClient.GetAsync(apiUrl);
|
||||||
|
var responseBody =
|
||||||
|
(await response.Content.ReadAsStringAsync()).Trim();
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
LatestCommitHashText.Content =
|
||||||
|
$"HTTP {(int)response.StatusCode}";
|
||||||
|
|
||||||
|
ToolTip.SetTip(
|
||||||
|
LatestCommitHashText,
|
||||||
|
string.IsNullOrWhiteSpace(responseBody)
|
||||||
|
? response.ReasonPhrase
|
||||||
|
: responseBody);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseBody.Length < 7)
|
||||||
|
{
|
||||||
|
LatestCommitHashText.Content = "Invalid response";
|
||||||
|
ToolTip.SetTip(LatestCommitHashText, responseBody);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the complete SHA for the URL.
|
||||||
|
_latestCommitSha = responseBody;
|
||||||
|
|
||||||
|
// Display only the short SHA.
|
||||||
|
LatestCommitHashText.Content =
|
||||||
|
responseBody[..Math.Min(7, responseBody.Length)];
|
||||||
|
|
||||||
|
LatestCommitHashText.IsEnabled = true;
|
||||||
|
|
||||||
|
ToolTip.SetTip(
|
||||||
|
LatestCommitHashText,
|
||||||
|
$"Open commit {_latestCommitSha}");
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
LatestCommitHashText.Content = "Timeout";
|
||||||
|
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
LatestCommitHashText.Content = "Connection error";
|
||||||
|
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LatestCommitHashText.Content = "Error";
|
||||||
|
ToolTip.SetTip(LatestCommitHashText, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Controller navigation ----
|
// ---- Controller navigation ----
|
||||||
|
|
||||||
private void PollGamepad()
|
private void PollGamepad()
|
||||||
@@ -371,6 +486,12 @@ public partial class MainWindow : Window
|
|||||||
ApplySettingsToControls();
|
ApplySettingsToControls();
|
||||||
LocateEmulator();
|
LocateEmulator();
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
|
_ = LoadLatestCommitAsync();
|
||||||
|
|
||||||
|
if (_settings.CheckForUpdatesOnStartup)
|
||||||
|
{
|
||||||
|
_ = CheckForUpdatesAsync();
|
||||||
|
}
|
||||||
await RescanLibraryAsync();
|
await RescanLibraryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,9 +548,11 @@ public partial class MainWindow : Window
|
|||||||
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||||
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||||
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||||
|
EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.Desc");
|
||||||
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||||
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||||
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||||
|
EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
|
||||||
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||||
@@ -472,8 +595,10 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
||||||
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
||||||
|
AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
|
||||||
|
AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
|
||||||
|
|
||||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle })
|
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
|
||||||
{
|
{
|
||||||
toggle.OnContent = loc.Get("Common.On");
|
toggle.OnContent = loc.Get("Common.On");
|
||||||
toggle.OffContent = loc.Get("Common.Off");
|
toggle.OffContent = loc.Get("Common.Off");
|
||||||
@@ -497,6 +622,10 @@ public partial class MainWindow : Window
|
|||||||
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
||||||
GithubButton.Content = loc.Get("About.GithubButton");
|
GithubButton.Content = loc.Get("About.GithubButton");
|
||||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||||
|
UpdateLabel.Text = loc.Get("Updater.Label");
|
||||||
|
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
|
||||||
|
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
|
||||||
|
RefreshUpdateText();
|
||||||
|
|
||||||
UpdateEmptyStateTexts();
|
UpdateEmptyStateTexts();
|
||||||
UpdateSelectedGameTexts();
|
UpdateSelectedGameTexts();
|
||||||
@@ -612,15 +741,87 @@ public partial class MainWindow : Window
|
|||||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||||
|
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
|
||||||
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||||
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||||
|
EnvWritableApp0Toggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_WRITABLE_APP0");
|
||||||
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
||||||
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
||||||
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
||||||
|
EnvLogIoToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_IO");
|
||||||
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||||
UpdateLogFilePathText();
|
UpdateLogFilePathText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnUpdateButtonAsync()
|
||||||
|
{
|
||||||
|
if (_availableUpdate is null)
|
||||||
|
{
|
||||||
|
await CheckForUpdatesAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateButton.IsEnabled = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var progress = new Progress<int>(value =>
|
||||||
|
SetUpdateStatus("Updater.Status.Downloading", value));
|
||||||
|
await Updater.DownloadAndRestartAsync(_availableUpdate, progress);
|
||||||
|
SetUpdateStatus("Updater.Status.Installing");
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
SetUpdateStatus("Updater.Status.Failed");
|
||||||
|
UpdateButton.IsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckForUpdatesAsync()
|
||||||
|
{
|
||||||
|
_availableUpdate = null;
|
||||||
|
UpdateButton.IsEnabled = false;
|
||||||
|
SetUpdateStatus("Updater.Status.Checking");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_availableUpdate = await Updater.CheckAsync(BuildInfo.CommitSha);
|
||||||
|
SetUpdateStatus(
|
||||||
|
_availableUpdate is null ? "Updater.Status.Current" : "Updater.Status.Available",
|
||||||
|
_availableUpdate?.Sha ?? BuildInfo.CommitSha ?? "dev");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
SetUpdateStatus("Updater.Status.Timeout");
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException)
|
||||||
|
{
|
||||||
|
SetUpdateStatus("Updater.Status.Unsupported");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
SetUpdateStatus("Updater.Status.Failed");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
UpdateButton.IsEnabled = true;
|
||||||
|
RefreshUpdateText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetUpdateStatus(string key, params object?[] args)
|
||||||
|
{
|
||||||
|
_updateStatusKey = key;
|
||||||
|
_updateStatusArgs = args;
|
||||||
|
RefreshUpdateText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshUpdateText()
|
||||||
|
{
|
||||||
|
UpdateStatusText.Text = Localization.Instance.Format(_updateStatusKey, _updateStatusArgs);
|
||||||
|
UpdateButton.Content = Localization.Instance.Get(
|
||||||
|
_availableUpdate is null ? "Updater.Check" : "Updater.DownloadRestart");
|
||||||
|
}
|
||||||
|
|
||||||
// Environment variables set on this process at the previous launch; children
|
// Environment variables set on this process at the previous launch; children
|
||||||
// inherit the process environment, so stale names must be cleared explicitly.
|
// inherit the process environment, so stale names must be cleared explicitly.
|
||||||
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||||
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
|
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
|
||||||
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||||
|
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
|
||||||
|
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Formats.Tar;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
/// <summary>Self-contained Windows updater; the emulator layers do not depend on it.</summary>
|
||||||
|
public static class Updater
|
||||||
|
{
|
||||||
|
private const string ApplyArgument = "--sharpemu-apply-update";
|
||||||
|
private const string LatestReleaseUrl = "https://api.github.com/repos/sharpemu/sharpemu/releases/latest";
|
||||||
|
private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
private static readonly HttpClient Http = CreateHttpClient();
|
||||||
|
|
||||||
|
public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size);
|
||||||
|
|
||||||
|
public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var platform = CurrentPlatform();
|
||||||
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
timeout.CancelAfter(CheckTimeout);
|
||||||
|
|
||||||
|
using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return ParseRelease(
|
||||||
|
await response.Content.ReadAsStringAsync(timeout.Token),
|
||||||
|
currentSha,
|
||||||
|
platform.Rid,
|
||||||
|
platform.Extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task DownloadAndRestartAsync(
|
||||||
|
UpdateInfo update,
|
||||||
|
IProgress<int>? progress = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), "SharpEmu.Update");
|
||||||
|
var payload = Path.Combine(root, "payload");
|
||||||
|
if (Directory.Exists(root))
|
||||||
|
{
|
||||||
|
Directory.Delete(root, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
var archive = Path.Combine(root, update.Name);
|
||||||
|
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
await using var output = File.Create(archive);
|
||||||
|
var buffer = new byte[81920];
|
||||||
|
long written = 0;
|
||||||
|
int read;
|
||||||
|
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
|
||||||
|
{
|
||||||
|
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||||
|
written += read;
|
||||||
|
progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (written != update.Size)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var platform = CurrentPlatform();
|
||||||
|
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
|
||||||
|
|
||||||
|
var start = new ProcessStartInfo(stagedExe)
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = payload,
|
||||||
|
};
|
||||||
|
start.ArgumentList.Add(ApplyArgument);
|
||||||
|
start.ArgumentList.Add(Environment.ProcessId.ToString());
|
||||||
|
start.ArgumentList.Add(AppContext.BaseDirectory);
|
||||||
|
using var helper = Process.Start(start)
|
||||||
|
?? throw new InvalidOperationException("The update installer could not be started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Runs from the downloaded executable after the old GUI exits.</summary>
|
||||||
|
public static bool TryApply(string[] args, out int exitCode)
|
||||||
|
{
|
||||||
|
exitCode = 0;
|
||||||
|
if (args.Length != 3 || args[0] != ApplyArgument)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (int.TryParse(args[1], out var oldPid))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Process.GetProcessById(oldPid).WaitForExit(30_000))
|
||||||
|
{
|
||||||
|
throw new TimeoutException("SharpEmu did not close within 30 seconds.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
// The old process has already exited.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = AppContext.BaseDirectory;
|
||||||
|
var target = Path.GetFullPath(args[2]);
|
||||||
|
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var relative = Path.GetRelativePath(source, file);
|
||||||
|
if (relative.Equals("gui-settings.json", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
relative.StartsWith("user" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
relative.StartsWith("logs" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
relative.StartsWith("Languages" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destination = Path.Combine(target, relative);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||||
|
File.Copy(file, destination, overwrite: true);
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
File.SetUnixFileMode(destination, File.GetUnixFileMode(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var restarted = Process.Start(new ProcessStartInfo(
|
||||||
|
Path.Combine(target, CurrentPlatform().ExecutableName))
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = target,
|
||||||
|
}) ?? throw new InvalidOperationException("The updated SharpEmu could not be started.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
exitCode = 1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best-effort diagnostics only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpdateInfo? ParseRelease(
|
||||||
|
string json,
|
||||||
|
string? currentSha,
|
||||||
|
string rid,
|
||||||
|
string extension)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>();
|
||||||
|
foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = asset.GetProperty("name").GetString() ?? "";
|
||||||
|
var marker = $"-{rid}-";
|
||||||
|
var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
markerIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sha = name[(markerIndex + marker.Length)..^extension.Length];
|
||||||
|
if (sha.Length < 7 || !sha.All(Uri.IsHexDigit))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.Add((
|
||||||
|
asset.GetProperty("created_at").GetDateTimeOffset(),
|
||||||
|
new UpdateInfo(
|
||||||
|
sha,
|
||||||
|
name,
|
||||||
|
asset.GetProperty("browser_download_url").GetString()!,
|
||||||
|
asset.GetProperty("size").GetInt64())));
|
||||||
|
}
|
||||||
|
|
||||||
|
var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update;
|
||||||
|
return latest is null || string.Equals(latest.Sha, currentSha, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? null
|
||||||
|
: latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractArchive(
|
||||||
|
string archive,
|
||||||
|
string payload,
|
||||||
|
string extension,
|
||||||
|
string executableName)
|
||||||
|
{
|
||||||
|
if (extension == ".zip")
|
||||||
|
{
|
||||||
|
ZipFile.ExtractToDirectory(archive, payload);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(payload);
|
||||||
|
using var compressed = File.OpenRead(archive);
|
||||||
|
using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
|
||||||
|
TarFile.ExtractToDirectory(gzip, payload, overwriteFiles: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var executable = Path.Combine(payload, executableName);
|
||||||
|
if (!File.Exists(executable))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"The update archive does not contain {executableName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
File.SetUnixFileMode(executable, File.GetUnixFileMode(executable) | UnixFileMode.UserExecute);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient()
|
||||||
|
{
|
||||||
|
var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
|
||||||
|
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SharpEmu", "0.0.1"));
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlatformInfo CurrentPlatform()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("SharpEmu releases require an x64 process.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows()) return new("win-x64", ".zip", "SharpEmu.exe");
|
||||||
|
if (OperatingSystem.IsLinux()) return new("linux-x64", ".tar.gz", "SharpEmu");
|
||||||
|
if (OperatingSystem.IsMacOS()) return new("osx-x64", ".tar.gz", "SharpEmu");
|
||||||
|
throw new PlatformNotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName);
|
||||||
|
}
|
||||||
@@ -26,6 +26,12 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
|
|
||||||
public ulong GsBase { get; set; }
|
public ulong GsBase { get; set; }
|
||||||
|
|
||||||
|
/// <summary>x87 control word observed at the current guest boundary.</summary>
|
||||||
|
public ushort FpuControlWord { get; set; } = 0x037F;
|
||||||
|
|
||||||
|
/// <summary>MXCSR observed at the current guest boundary.</summary>
|
||||||
|
public uint Mxcsr { get; set; } = 0x1F80;
|
||||||
|
|
||||||
public ulong this[CpuRegister register]
|
public ulong this[CpuRegister register]
|
||||||
{
|
{
|
||||||
get => _registers[(int)register];
|
get => _registers[(int)register];
|
||||||
|
|||||||
@@ -0,0 +1,613 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects guest CPU writes into memory that backs a host GPU image. On PS5
|
||||||
|
/// render targets alias unified memory, so games freely mix CPU writes and GPU
|
||||||
|
/// draws on the same surface (Chowdren titles memset their fog layers every
|
||||||
|
/// frame). Host GPU images are separate storage, so the video backend needs to
|
||||||
|
/// know when the guest CPU rewrote a surface to re-upload it. Ranges are
|
||||||
|
/// write-protected; the first write faults, the fault handler restores write
|
||||||
|
/// access and marks the range dirty, and the video backend consumes the dirty
|
||||||
|
/// flag once per flip and re-arms protection after re-uploading.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe class GuestImageWriteTracker
|
||||||
|
{
|
||||||
|
private const int ProtRead = 0x1;
|
||||||
|
private const int ProtWrite = 0x2;
|
||||||
|
private const int ClockMonotonicRaw = 4;
|
||||||
|
|
||||||
|
private sealed class TrackedRange
|
||||||
|
{
|
||||||
|
public ulong Address;
|
||||||
|
public ulong ByteCount;
|
||||||
|
public ulong Start;
|
||||||
|
public ulong End;
|
||||||
|
public int Dirty;
|
||||||
|
public int Armed;
|
||||||
|
public int FirstCpuWriteSeen;
|
||||||
|
public int PendingFirstCpuWrite;
|
||||||
|
public bool TraceLifetime;
|
||||||
|
public long SourceSequence;
|
||||||
|
public long FirstCpuWriteTraceSequence;
|
||||||
|
public long FirstCpuWriteTimestampNanoseconds;
|
||||||
|
public ulong FirstCpuWriteAddress;
|
||||||
|
public ulong FirstCpuWritePage;
|
||||||
|
public string Source = "unspecified";
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct Timespec
|
||||||
|
{
|
||||||
|
public long Seconds;
|
||||||
|
public long Nanoseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
||||||
|
|
||||||
|
// Snapshot array read lock-free from the signal handler; rebuilt on every
|
||||||
|
// mutation under the gate. Signal handlers must not take managed locks.
|
||||||
|
private static TrackedRange[] _rangeSnapshot = [];
|
||||||
|
|
||||||
|
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
||||||
|
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
|
||||||
|
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||||
|
private static readonly (bool Wildcard, string[] Sources) _lifetimeSourceTraceFilter =
|
||||||
|
ParseSourceList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME"));
|
||||||
|
private static readonly bool _lifetimeTraceEnabled =
|
||||||
|
_lifetimeTraceFilter.Wildcard ||
|
||||||
|
_lifetimeTraceFilter.Addresses.Length != 0 ||
|
||||||
|
_lifetimeSourceTraceFilter.Wildcard ||
|
||||||
|
_lifetimeSourceTraceFilter.Sources.Length != 0;
|
||||||
|
private static readonly long _lifetimeTraceEpochNanoseconds =
|
||||||
|
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
|
||||||
|
private static long _lifetimeTraceSequence;
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
|
||||||
|
private static extern int Mprotect(nint address, nuint length, int protection);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
|
||||||
|
private static extern int ClockGetTime(int clockId, Timespec* time);
|
||||||
|
|
||||||
|
public static bool Enabled => _enabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exercises the fault-handling path once outside signal context so every
|
||||||
|
/// branch is JIT-compiled (and, under Rosetta 2, translated) before a real
|
||||||
|
/// fault arrives — a cold signal path is silently never entered there.
|
||||||
|
/// </summary>
|
||||||
|
public static void WarmUp()
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scratch = NativeMemory.AllocZeroed(4096);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Warm the timestamp P/Invoke used by the signal-safe scalar
|
||||||
|
// capture path before a real protected-page write reaches it.
|
||||||
|
_ = GetMonotonicNanoseconds();
|
||||||
|
var address = (ulong)scratch;
|
||||||
|
Track(address, 4096);
|
||||||
|
_ = TryHandleWriteFault(address);
|
||||||
|
_ = ConsumeDirty(address);
|
||||||
|
Untrack(address);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMemory.Free(scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Registers a range and arms write protection on it.</summary>
|
||||||
|
public static void Track(
|
||||||
|
ulong address,
|
||||||
|
ulong byteCount,
|
||||||
|
long sourceSequence = 0,
|
||||||
|
string source = "unspecified")
|
||||||
|
{
|
||||||
|
if (!_enabled || address == 0 || byteCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (start, length) = PageAlign(address, byteCount);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_rangesByAddress.TryGetValue(address, out var range);
|
||||||
|
if (range is not null &&
|
||||||
|
(range.Start != start ||
|
||||||
|
range.End != start + length ||
|
||||||
|
range.ByteCount != byteCount))
|
||||||
|
{
|
||||||
|
// Never resize an object that is still reachable from the
|
||||||
|
// signal handler's lock-free snapshot. Retire it and publish
|
||||||
|
// a fresh immutable range.
|
||||||
|
DisarmLocked(range, "replace-range");
|
||||||
|
_rangesByAddress.Remove(address);
|
||||||
|
range = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range is null)
|
||||||
|
{
|
||||||
|
range = new TrackedRange
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
ByteCount = byteCount,
|
||||||
|
Start = start,
|
||||||
|
End = start + length,
|
||||||
|
TraceLifetime =
|
||||||
|
ShouldTraceRange(start, start + length) || ShouldTraceSource(source),
|
||||||
|
SourceSequence = sourceSequence,
|
||||||
|
Source = source,
|
||||||
|
};
|
||||||
|
_rangesByAddress[address] = range;
|
||||||
|
RebuildSnapshotLocked();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
range.SourceSequence = sourceSequence;
|
||||||
|
range.Source = source;
|
||||||
|
range.TraceLifetime =
|
||||||
|
ShouldTraceRange(range.Start, range.End) || ShouldTraceSource(source);
|
||||||
|
ArmLocked(range, "arm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Untrack(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
DisarmLocked(range, "untrack");
|
||||||
|
_rangesByAddress.Remove(address);
|
||||||
|
RebuildSnapshotLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when the guest CPU wrote the range since the last call,
|
||||||
|
/// clearing the flag. The caller re-arms via <see cref="Rearm"/> after it
|
||||||
|
/// finished reading the guest bytes.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ConsumeDirty(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
return Interlocked.Exchange(ref range.Dirty, 0) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Non-consuming variant of <see cref="ConsumeDirty"/>: reports whether
|
||||||
|
/// the range has been written since it was last re-armed, leaving the
|
||||||
|
/// flag for the owner that evicts and re-uploads.
|
||||||
|
/// </summary>
|
||||||
|
public static bool PeekDirty(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
return Volatile.Read(ref range.Dirty) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Rearm(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
ArmLocked(range, "rearm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||||
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||||
|
/// POSIX signal bridge, but a managed Buffer.MemoryCopy into a protected
|
||||||
|
/// page is surfaced by the runtime as a fatal AccessViolation instead of
|
||||||
|
/// a resumable guest fault. Visit every page in the write span up front so
|
||||||
|
/// all overlapping texture owners are dirtied and made writable.
|
||||||
|
/// </summary>
|
||||||
|
public static void NotifyManagedWrite(ulong address, ulong byteCount)
|
||||||
|
{
|
||||||
|
if (!_enabled || address == 0 || byteCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = address > ulong.MaxValue - byteCount
|
||||||
|
? ulong.MaxValue
|
||||||
|
: address + byteCount;
|
||||||
|
var candidate = address;
|
||||||
|
while (candidate < end)
|
||||||
|
{
|
||||||
|
_ = TryHandleWriteFault(candidate);
|
||||||
|
var nextPage = (candidate & ~0xFFFUL) + 0x1000UL;
|
||||||
|
if (nextPage <= candidate)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
candidate = nextPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flushes scalar first-write records captured by the POSIX signal handler.
|
||||||
|
/// Call only from ordinary managed execution, never from signal context.
|
||||||
|
/// </summary>
|
||||||
|
public static void FlushPendingDiagnostics()
|
||||||
|
{
|
||||||
|
if (!_enabled || !_lifetimeTraceEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var range in _rangesByAddress.Values)
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal-handler entry: if the fault address lies in a tracked, armed
|
||||||
|
/// range, restore write access, mark the range dirty, and return true so
|
||||||
|
/// the faulting write can be retried. Must not allocate or lock.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryHandleWriteFault(ulong faultAddress)
|
||||||
|
{
|
||||||
|
if (!_enabled || faultAddress == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ranges = Volatile.Read(ref _rangeSnapshot);
|
||||||
|
var writableStart = ulong.MaxValue;
|
||||||
|
var writableEnd = 0UL;
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (faultAddress < range.Start || faultAddress >= range.End)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writableStart = Math.Min(writableStart, range.Start);
|
||||||
|
writableEnd = Math.Max(writableEnd, range.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writableStart == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ranges are page-aligned and may overlap (font atlases and other
|
||||||
|
// suballocations commonly share pages). Unprotecting one range also
|
||||||
|
// makes every overlapping tracked page writable. Expand to the full
|
||||||
|
// transitive overlap and dirty/disarm every owner, otherwise only the
|
||||||
|
// first dictionary entry observes the write and the others retain a
|
||||||
|
// stale cached texture indefinitely.
|
||||||
|
var expanded = true;
|
||||||
|
while (expanded)
|
||||||
|
{
|
||||||
|
expanded = false;
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (range.Start >= writableEnd || range.End <= writableStart)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = Math.Min(writableStart, range.Start);
|
||||||
|
var end = Math.Max(writableEnd, range.End);
|
||||||
|
if (start != writableStart || end != writableEnd)
|
||||||
|
{
|
||||||
|
writableStart = start;
|
||||||
|
writableEnd = end;
|
||||||
|
expanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsUnprotect = false;
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (range.Start < writableEnd && range.End > writableStart &&
|
||||||
|
Volatile.Read(ref range.Armed) != 0)
|
||||||
|
{
|
||||||
|
needsUnprotect = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsUnprotect &&
|
||||||
|
Mprotect(
|
||||||
|
(nint)writableStart,
|
||||||
|
(nuint)(writableEnd - writableStart),
|
||||||
|
ProtRead | ProtWrite) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (range.Start >= writableEnd || range.End <= writableStart)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||||
|
if (wasArmed &&
|
||||||
|
range.TraceLifetime &&
|
||||||
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||||
|
{
|
||||||
|
// Signal context: capture preallocated scalar fields only.
|
||||||
|
// Formatting and I/O are deferred to a locked safe path.
|
||||||
|
range.FirstCpuWriteTraceSequence =
|
||||||
|
Interlocked.Increment(ref _lifetimeTraceSequence);
|
||||||
|
range.FirstCpuWriteTimestampNanoseconds = GetMonotonicNanoseconds();
|
||||||
|
range.FirstCpuWriteAddress = faultAddress;
|
||||||
|
range.FirstCpuWritePage = faultAddress & ~0xFFFUL;
|
||||||
|
Volatile.Write(ref range.PendingFirstCpuWrite, 1);
|
||||||
|
Volatile.Write(ref range.FirstCpuWriteSeen, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Volatile.Write(ref range.Dirty, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ArmLocked(TrackedRange range, string operation)
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
if (Interlocked.Exchange(ref range.Armed, 1) == 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new publication/rearm starts a new first-write lifetime.
|
||||||
|
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
|
||||||
|
var failed = Mprotect(
|
||||||
|
(nint)range.Start,
|
||||||
|
(nuint)(range.End - range.Start),
|
||||||
|
ProtRead) != 0;
|
||||||
|
if (failed)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref range.Armed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.TraceLifetime)
|
||||||
|
{
|
||||||
|
TraceLifetime(
|
||||||
|
range,
|
||||||
|
failed ? $"{operation}-failed-errno-{Marshal.GetLastPInvokeError()}" : operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisarmLocked(TrackedRange range, string operation)
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
|
||||||
|
if (wasArmed)
|
||||||
|
{
|
||||||
|
_ = Mprotect(
|
||||||
|
(nint)range.Start,
|
||||||
|
(nuint)(range.End - range.Start),
|
||||||
|
ProtRead | ProtWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.TraceLifetime)
|
||||||
|
{
|
||||||
|
TraceLifetime(range, wasArmed ? operation : $"{operation}-already-disarmed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildSnapshotLocked()
|
||||||
|
{
|
||||||
|
_rangeSnapshot = _rangesByAddress.Values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
||||||
|
{
|
||||||
|
const ulong pageMask = 0xFFFUL;
|
||||||
|
var start = address & ~pageMask;
|
||||||
|
var end = (address + byteCount + pageMask) & ~pageMask;
|
||||||
|
return (start, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceRange(ulong start, ulong end)
|
||||||
|
{
|
||||||
|
if (_lifetimeTraceFilter.Wildcard)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addresses = _lifetimeTraceFilter.Addresses;
|
||||||
|
for (var index = 0; index < addresses.Length; index++)
|
||||||
|
{
|
||||||
|
if (addresses[index] >= start && addresses[index] < end)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool Wildcard, ulong[] Addresses) ParseAddressList(string? addresses)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(addresses))
|
||||||
|
{
|
||||||
|
return (false, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedAddresses = new List<ulong>();
|
||||||
|
foreach (var token in addresses.Split(
|
||||||
|
[',', ';', ' ', '\t'],
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (token == "*")
|
||||||
|
{
|
||||||
|
return (true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = token.AsSpan();
|
||||||
|
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
span = span[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ulong.TryParse(span, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
|
||||||
|
{
|
||||||
|
parsedAddresses.Add(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, parsedAddresses.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceSource(string source)
|
||||||
|
{
|
||||||
|
if (_lifetimeSourceTraceFilter.Wildcard)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.IndexOf(_lifetimeSourceTraceFilter.Sources, source) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool Wildcard, string[] Sources) ParseSourceList(string? sources)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sources))
|
||||||
|
{
|
||||||
|
return (false, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedSources = new List<string>();
|
||||||
|
foreach (var token in sources.Split(
|
||||||
|
[',', ';'],
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (token == "*")
|
||||||
|
{
|
||||||
|
return (true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSources.Add(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, parsedSources.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FlushPendingFirstCpuWrite(TrackedRange range)
|
||||||
|
{
|
||||||
|
var spin = new SpinWait();
|
||||||
|
while (Volatile.Read(ref range.FirstCpuWriteSeen) == 1)
|
||||||
|
{
|
||||||
|
spin.SpinOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!range.TraceLifetime || Interlocked.Exchange(ref range.PendingFirstCpuWrite, 0) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceLifetime(
|
||||||
|
range,
|
||||||
|
"first-cpu-write-disarm",
|
||||||
|
range.FirstCpuWriteAddress,
|
||||||
|
range.FirstCpuWritePage,
|
||||||
|
range.FirstCpuWriteTraceSequence,
|
||||||
|
range.FirstCpuWriteTimestampNanoseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceLifetime(
|
||||||
|
TrackedRange range,
|
||||||
|
string operation,
|
||||||
|
ulong faultAddress = 0,
|
||||||
|
ulong faultPage = 0,
|
||||||
|
long traceSequence = 0,
|
||||||
|
long timestampNanoseconds = 0)
|
||||||
|
{
|
||||||
|
if (traceSequence == 0)
|
||||||
|
{
|
||||||
|
traceSequence = Interlocked.Increment(ref _lifetimeTraceSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestampNanoseconds == 0)
|
||||||
|
{
|
||||||
|
timestampNanoseconds = GetMonotonicNanoseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedMilliseconds =
|
||||||
|
(timestampNanoseconds - _lifetimeTraceEpochNanoseconds) / 1_000_000.0;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[WT][LIFETIME] seq={traceSequence} t_ms={elapsedMilliseconds:F3} " +
|
||||||
|
$"event={operation} source_seq={range.SourceSequence} source='{range.Source}' " +
|
||||||
|
$"requested=0x{range.Address:X16}+0x{range.ByteCount:X} " +
|
||||||
|
$"range=0x{range.Start:X16}..0x{range.End:X16} " +
|
||||||
|
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long GetMonotonicNanoseconds()
|
||||||
|
{
|
||||||
|
Timespec time;
|
||||||
|
return ClockGetTime(ClockMonotonicRaw, &time) == 0
|
||||||
|
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,14 @@ public interface IGuestThreadScheduler
|
|||||||
{
|
{
|
||||||
bool SupportsGuestContextTransfer { get; }
|
bool SupportsGuestContextTransfer { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Associates a pthread identity created on the primary guest executor
|
||||||
|
/// with its live CPU context. Primary execution does not pass through
|
||||||
|
/// TryStartThread, but kernel exception delivery must still be able to
|
||||||
|
/// target it.
|
||||||
|
/// </summary>
|
||||||
|
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
|
||||||
|
|
||||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||||
|
|
||||||
bool TryJoinThread(
|
bool TryJoinThread(
|
||||||
@@ -53,6 +61,19 @@ public interface IGuestThreadScheduler
|
|||||||
|
|
||||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a new guest scheduling priority to a live thread, mapping it
|
||||||
|
/// onto the host thread if one is running. Returns false when the thread
|
||||||
|
/// handle is unknown.
|
||||||
|
/// </summary>
|
||||||
|
bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a new affinity mask for a guest thread and re-applies it to
|
||||||
|
/// the host thread where the platform supports it.
|
||||||
|
/// </summary>
|
||||||
|
bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask);
|
||||||
|
|
||||||
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
||||||
|
|
||||||
bool TryCallGuestFunction(
|
bool TryCallGuestFunction(
|
||||||
@@ -65,11 +86,36 @@ public interface IGuestThreadScheduler
|
|||||||
string reason,
|
string reason,
|
||||||
out string? error);
|
out string? error);
|
||||||
|
|
||||||
|
bool TryCallGuestFunction(
|
||||||
|
CpuContext callerContext,
|
||||||
|
ulong entryPoint,
|
||||||
|
ulong arg0,
|
||||||
|
ulong arg1,
|
||||||
|
ulong arg2,
|
||||||
|
ulong stackAddress,
|
||||||
|
ulong stackSize,
|
||||||
|
string reason,
|
||||||
|
out ulong returnValue,
|
||||||
|
out string? error);
|
||||||
|
|
||||||
bool TryCallGuestContinuation(
|
bool TryCallGuestContinuation(
|
||||||
CpuContext callerContext,
|
CpuContext callerContext,
|
||||||
GuestCpuContinuation continuation,
|
GuestCpuContinuation continuation,
|
||||||
string reason,
|
string reason,
|
||||||
out string? error);
|
out string? error);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously invokes an installed kernel exception handler as the
|
||||||
|
/// target guest thread. This is used by IL2CPP's stop-the-world collector:
|
||||||
|
/// the handler acknowledges suspension and may remain blocked until the
|
||||||
|
/// collecting thread resumes it.
|
||||||
|
/// </summary>
|
||||||
|
bool TryRaiseGuestException(
|
||||||
|
CpuContext callerContext,
|
||||||
|
ulong threadHandle,
|
||||||
|
ulong handler,
|
||||||
|
int exceptionType,
|
||||||
|
out string? error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct GuestImportCallFrame(
|
public readonly record struct GuestImportCallFrame(
|
||||||
@@ -94,13 +140,34 @@ public readonly record struct GuestCpuContinuation(
|
|||||||
ulong Rdi,
|
ulong Rdi,
|
||||||
ulong R8,
|
ulong R8,
|
||||||
ulong R9,
|
ulong R9,
|
||||||
|
ulong R10,
|
||||||
|
ulong R11,
|
||||||
ulong R12,
|
ulong R12,
|
||||||
ulong R13,
|
ulong R13,
|
||||||
ulong R14,
|
ulong R14,
|
||||||
ulong R15);
|
ulong R15,
|
||||||
|
ushort FpuControlWord,
|
||||||
|
uint Mxcsr,
|
||||||
|
bool RestoreFullFpuState);
|
||||||
|
|
||||||
public static class GuestThreadExecution
|
public static class GuestThreadExecution
|
||||||
{
|
{
|
||||||
|
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
private readonly Func<int> _resume;
|
||||||
|
private readonly Func<bool> _tryWake;
|
||||||
|
|
||||||
|
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
|
||||||
|
{
|
||||||
|
_resume = resume;
|
||||||
|
_tryWake = tryWake;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Resume() => _resume();
|
||||||
|
|
||||||
|
public bool TryWake() => _tryWake();
|
||||||
|
}
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static ulong _currentGuestThreadHandle;
|
private static ulong _currentGuestThreadHandle;
|
||||||
|
|
||||||
@@ -246,6 +313,23 @@ public static class GuestThreadExecution
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compatibility bridge for exports that still describe blocked work as a
|
||||||
|
// resume/wake delegate pair. New hot paths should provide an
|
||||||
|
// IGuestThreadBlockWaiter directly to avoid allocating closures.
|
||||||
|
public static bool RequestCurrentThreadBlock(
|
||||||
|
CpuContext? context,
|
||||||
|
string reason,
|
||||||
|
string? wakeKey,
|
||||||
|
Func<int> resumeHandler,
|
||||||
|
Func<bool> wakeHandler,
|
||||||
|
long blockDeadlineTimestamp = 0) =>
|
||||||
|
RequestCurrentThreadBlock(
|
||||||
|
context,
|
||||||
|
reason,
|
||||||
|
wakeKey,
|
||||||
|
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
|
||||||
|
blockDeadlineTimestamp);
|
||||||
|
|
||||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||||
{
|
{
|
||||||
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||||
@@ -314,6 +398,27 @@ public static class GuestThreadExecution
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation,
|
||||||
|
out string wakeKey,
|
||||||
|
out Func<int>? resumeHandler,
|
||||||
|
out Func<bool>? wakeHandler,
|
||||||
|
out long blockDeadlineTimestamp)
|
||||||
|
{
|
||||||
|
var consumed = TryConsumeCurrentThreadBlock(
|
||||||
|
out reason,
|
||||||
|
out continuation,
|
||||||
|
out hasContinuation,
|
||||||
|
out wakeKey,
|
||||||
|
out var waiter,
|
||||||
|
out blockDeadlineTimestamp);
|
||||||
|
resumeHandler = waiter is null ? null : waiter.Resume;
|
||||||
|
wakeHandler = waiter is null ? null : waiter.TryWake;
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||||
{
|
{
|
||||||
if (timeout <= TimeSpan.Zero)
|
if (timeout <= TimeSpan.Zero)
|
||||||
@@ -360,10 +465,15 @@ public static class GuestThreadExecution
|
|||||||
context[CpuRegister.Rdi],
|
context[CpuRegister.Rdi],
|
||||||
context[CpuRegister.R8],
|
context[CpuRegister.R8],
|
||||||
context[CpuRegister.R9],
|
context[CpuRegister.R9],
|
||||||
|
context[CpuRegister.R10],
|
||||||
|
context[CpuRegister.R11],
|
||||||
context[CpuRegister.R12],
|
context[CpuRegister.R12],
|
||||||
context[CpuRegister.R13],
|
context[CpuRegister.R13],
|
||||||
context[CpuRegister.R14],
|
context[CpuRegister.R14],
|
||||||
context[CpuRegister.R15]);
|
context[CpuRegister.R15],
|
||||||
|
context.FpuControlWord,
|
||||||
|
context.Mxcsr,
|
||||||
|
RestoreFullFpuState: false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-wide registry of ELF PT_TLS templates and per-thread dynamic thread
|
||||||
|
/// vectors. AMD64 uses TLS Variant II: startup/static module blocks precede the
|
||||||
|
/// thread pointer, while modules introduced after a thread was initialized are
|
||||||
|
/// represented by separately allocated DTV entries.
|
||||||
|
/// </summary>
|
||||||
|
public static class GuestTlsTemplate
|
||||||
|
{
|
||||||
|
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||||
|
// modules can require more than one host page of Variant II static TLS;
|
||||||
|
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||||
|
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||||
|
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||||
|
private static ulong _staticTlsSize;
|
||||||
|
private static ulong _maximumAlignment = 1;
|
||||||
|
private static ulong _generation;
|
||||||
|
|
||||||
|
static GuestTlsTemplate()
|
||||||
|
{
|
||||||
|
RunTlsLayoutSelfChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ModuleTemplate
|
||||||
|
{
|
||||||
|
public required ulong ModuleId { get; init; }
|
||||||
|
public required byte[] InitImage { get; init; }
|
||||||
|
public required ulong MemorySize { get; init; }
|
||||||
|
public required ulong Alignment { get; init; }
|
||||||
|
public required ulong AlignmentBias { get; init; }
|
||||||
|
public required ulong StaticOffset { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ThreadDtv
|
||||||
|
{
|
||||||
|
public ulong Generation { get; set; }
|
||||||
|
public Dictionary<ulong, DtvEntry> Entries { get; } = new();
|
||||||
|
public nint DtvAllocationBase { get; set; }
|
||||||
|
public ulong DtvAddress { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DtvEntry
|
||||||
|
{
|
||||||
|
public required ulong Address { get; init; }
|
||||||
|
public nint AllocationBase { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main executable's initialized PT_TLS bytes.</summary>
|
||||||
|
public static byte[] InitImage
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modules.TryGetValue(1, out var module)
|
||||||
|
? (byte[])module.InitImage.Clone()
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Aligned size of the main executable's static TLS block.</summary>
|
||||||
|
public static ulong BlockSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modules.TryGetValue(1, out var module)
|
||||||
|
? module.StaticOffset
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main executable's PT_TLS alignment.</summary>
|
||||||
|
public static ulong Alignment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modules.TryGetValue(1, out var module)
|
||||||
|
? module.Alignment
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Total Variant II static TLS span required below the TCB.</summary>
|
||||||
|
public static ulong StaticTlsSize
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _staticTlsSize; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Largest PT_TLS alignment required by the registered modules.</summary>
|
||||||
|
public static ulong MaximumAlignment
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _maximumAlignment; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Current DTV generation, incremented for every registered module.</summary>
|
||||||
|
public static ulong Generation
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _generation; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Backwards-compatible main-module registration entry point.
|
||||||
|
/// </summary>
|
||||||
|
public static void Set(ReadOnlySpan<byte> initImage, ulong memorySize, ulong alignment)
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
RegisterModule(1, initImage, memorySize, alignment, alignmentBias: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers one module's PT_TLS template and allocates its Variant II
|
||||||
|
/// static offset. Modules must be registered in loader module-ID order.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong RegisterModule(
|
||||||
|
ulong moduleId,
|
||||||
|
ReadOnlySpan<byte> initImage,
|
||||||
|
ulong memorySize,
|
||||||
|
ulong alignment,
|
||||||
|
ulong alignmentBias = 0)
|
||||||
|
{
|
||||||
|
if (moduleId == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(moduleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedAlignment = alignment == 0 ? 1 : alignment;
|
||||||
|
if ((normalizedAlignment & (normalizedAlignment - 1)) != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"PT_TLS alignment 0x{alignment:X} is not a power of two.");
|
||||||
|
}
|
||||||
|
if (memorySize > int.MaxValue || (ulong)initImage.Length > memorySize)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("PT_TLS template size is invalid or exceeds the supported process limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_modules.TryGetValue(moduleId, out var existing))
|
||||||
|
{
|
||||||
|
if (existing.MemorySize != memorySize ||
|
||||||
|
existing.Alignment != normalizedAlignment ||
|
||||||
|
existing.AlignmentBias != alignmentBias ||
|
||||||
|
!existing.InitImage.AsSpan().SequenceEqual(initImage))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"TLS module {moduleId} was registered with a different template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.StaticOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeBSD/AMD64 Variant II: choose the smallest offset satisfying
|
||||||
|
// offset - previous >= size and (-offset) % align == p_vaddr % align.
|
||||||
|
var staticOffset = CalculateStaticOffset(
|
||||||
|
_staticTlsSize,
|
||||||
|
memorySize,
|
||||||
|
normalizedAlignment,
|
||||||
|
alignmentBias);
|
||||||
|
if (staticOffset > StartupStaticTlsReservation)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Static TLS requires 0x{staticOffset:X} bytes, but startup maps only " +
|
||||||
|
$"0x{StartupStaticTlsReservation:X} bytes below the thread pointer.");
|
||||||
|
}
|
||||||
|
_modules.Add(moduleId, new ModuleTemplate
|
||||||
|
{
|
||||||
|
ModuleId = moduleId,
|
||||||
|
InitImage = initImage.ToArray(),
|
||||||
|
MemorySize = memorySize,
|
||||||
|
Alignment = normalizedAlignment,
|
||||||
|
AlignmentBias = alignmentBias,
|
||||||
|
StaticOffset = staticOffset,
|
||||||
|
});
|
||||||
|
_staticTlsSize = staticOffset;
|
||||||
|
_maximumAlignment = Math.Max(_maximumAlignment, normalizedAlignment);
|
||||||
|
_generation++;
|
||||||
|
return staticOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the static TP-relative block offset for a module.</summary>
|
||||||
|
public static bool TryGetStaticOffset(ulong moduleId, out ulong staticOffset)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_modules.TryGetValue(moduleId, out var module))
|
||||||
|
{
|
||||||
|
staticOffset = module.StaticOffset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticOffset = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Clears all module templates and frees dynamic DTV blocks.</summary>
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var dtv in _threadDtvs.Values)
|
||||||
|
{
|
||||||
|
FreeDynamicEntries(dtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadDtvs.Clear();
|
||||||
|
_modules.Clear();
|
||||||
|
_staticTlsSize = 0;
|
||||||
|
_maximumAlignment = 1;
|
||||||
|
_generation++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes every currently registered startup TLS block for a thread
|
||||||
|
/// and records their addresses in that thread's DTV.
|
||||||
|
/// </summary>
|
||||||
|
public static void SeedThreadBlock(CpuContext context, ulong threadPointer)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
if (threadPointer == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_threadDtvs.Remove(threadPointer, out var oldDtv))
|
||||||
|
{
|
||||||
|
FreeDynamicEntries(oldDtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dtv = new ThreadDtv();
|
||||||
|
_threadDtvs.Add(threadPointer, dtv);
|
||||||
|
foreach (var module in _modules.Values)
|
||||||
|
{
|
||||||
|
dtv.Entries[module.ModuleId] = CreateStaticOrDynamicEntry(context, threadPointer, module);
|
||||||
|
}
|
||||||
|
RebuildGuestDtv(context, threadPointer, dtv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the DTV lookup used by <c>__tls_get_addr</c>. Unknown module
|
||||||
|
/// IDs and offsets outside the module's TLS image are rejected with zero.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong ResolveAddress(CpuContext context, ulong moduleId, ulong offset)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
if (context.FsBase == 0 || moduleId == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modules.TryGetValue(moduleId, out var module) || offset >= module.MemorySize)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_threadDtvs.TryGetValue(context.FsBase, out var dtv))
|
||||||
|
{
|
||||||
|
dtv = new ThreadDtv();
|
||||||
|
_threadDtvs.Add(context.FsBase, dtv);
|
||||||
|
foreach (var startupModule in _modules.Values)
|
||||||
|
{
|
||||||
|
dtv.Entries[startupModule.ModuleId] = CreateStaticOrDynamicEntry(context, context.FsBase, startupModule);
|
||||||
|
}
|
||||||
|
RebuildGuestDtv(context, context.FsBase, dtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryAdded = false;
|
||||||
|
if (!dtv.Entries.TryGetValue(moduleId, out var entry))
|
||||||
|
{
|
||||||
|
// This module appeared after the thread's startup TLS layout was
|
||||||
|
// seeded. Model rtld's lazy DTV allocation instead of assuming
|
||||||
|
// unused space exists below the already-live thread pointer.
|
||||||
|
entry = CreateDynamicEntry(module);
|
||||||
|
dtv.Entries.Add(moduleId, entry);
|
||||||
|
entryAdded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryAdded || dtv.Generation != _generation)
|
||||||
|
{
|
||||||
|
RebuildGuestDtv(context, context.FsBase, dtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return checked(entry.Address + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DtvEntry CreateStaticOrDynamicEntry(
|
||||||
|
CpuContext context,
|
||||||
|
ulong threadPointer,
|
||||||
|
ModuleTemplate module)
|
||||||
|
{
|
||||||
|
if (threadPointer >= module.StaticOffset)
|
||||||
|
{
|
||||||
|
var address = threadPointer - module.StaticOffset;
|
||||||
|
var zeroImage = module.MemorySize == 0 ? [] : new byte[(int)module.MemorySize];
|
||||||
|
if ((zeroImage.Length == 0 || context.Memory.TryWrite(address, zeroImage)) &&
|
||||||
|
(module.InitImage.Length == 0 || context.Memory.TryWrite(address, module.InitImage)))
|
||||||
|
{
|
||||||
|
return new DtvEntry { Address = address };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateDynamicEntry(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DtvEntry CreateDynamicEntry(ModuleTemplate module)
|
||||||
|
{
|
||||||
|
var size = Math.Max(1UL, module.MemorySize);
|
||||||
|
var allocationSize = checked(size + module.Alignment - 1);
|
||||||
|
if (allocationSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("TLS module allocation exceeds the supported host allocation size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationBase = Marshal.AllocHGlobal((int)allocationSize);
|
||||||
|
var alignedAddress = AlignUp(unchecked((ulong)allocationBase), module.Alignment);
|
||||||
|
Marshal.Copy(new byte[(int)size], 0, unchecked((nint)alignedAddress), (int)size);
|
||||||
|
if (module.InitImage.Length != 0)
|
||||||
|
{
|
||||||
|
Marshal.Copy(module.InitImage, 0, unchecked((nint)alignedAddress), module.InitImage.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DtvEntry
|
||||||
|
{
|
||||||
|
Address = alignedAddress,
|
||||||
|
AllocationBase = allocationBase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FreeDynamicEntries(ThreadDtv dtv)
|
||||||
|
{
|
||||||
|
foreach (var entry in dtv.Entries.Values)
|
||||||
|
{
|
||||||
|
if (entry.AllocationBase != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(entry.AllocationBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dtv.DtvAllocationBase != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
|
||||||
|
dtv.DtvAllocationBase = 0;
|
||||||
|
dtv.DtvAddress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildGuestDtv(CpuContext context, ulong threadPointer, ThreadDtv dtv)
|
||||||
|
{
|
||||||
|
var maximumModuleId = _modules.Count == 0 ? 0UL : _modules.Keys.Max();
|
||||||
|
var byteSize = checked(2UL * sizeof(ulong) + maximumModuleId * sizeof(ulong));
|
||||||
|
if (byteSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Guest DTV exceeds the supported host allocation size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var newAllocation = Marshal.AllocHGlobal((int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
|
||||||
|
var newAddress = unchecked((ulong)newAllocation);
|
||||||
|
Marshal.Copy(new byte[(int)Math.Max((ulong)sizeof(ulong) * 2, byteSize)], 0, newAllocation, (int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
|
||||||
|
Marshal.WriteInt64(newAllocation, 0, unchecked((long)_generation));
|
||||||
|
Marshal.WriteInt64(newAllocation, sizeof(ulong), unchecked((long)maximumModuleId));
|
||||||
|
foreach (var (moduleId, entry) in dtv.Entries)
|
||||||
|
{
|
||||||
|
var slotOffset = checked((int)(2UL * sizeof(ulong) + (moduleId - 1) * sizeof(ulong)));
|
||||||
|
Marshal.WriteInt64(newAllocation, slotOffset, unchecked((long)entry.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.TryWriteUInt64(threadPointer + sizeof(ulong), newAddress))
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(newAllocation);
|
||||||
|
throw new InvalidOperationException("Failed to install the guest DTV pointer in the thread control block.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dtv.DtvAllocationBase != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
|
||||||
|
}
|
||||||
|
dtv.DtvAllocationBase = newAllocation;
|
||||||
|
dtv.DtvAddress = newAddress;
|
||||||
|
dtv.Generation = _generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
|
{
|
||||||
|
var mask = alignment - 1;
|
||||||
|
return checked((value + mask) & ~mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong CalculateStaticOffset(
|
||||||
|
ulong previousOffset,
|
||||||
|
ulong size,
|
||||||
|
ulong alignment,
|
||||||
|
ulong alignmentBias)
|
||||||
|
{
|
||||||
|
var result = checked(previousOffset + size + alignment - 1);
|
||||||
|
return result - ((result + alignmentBias) & (alignment - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void RunTlsLayoutSelfChecks()
|
||||||
|
{
|
||||||
|
var firstOffset = RegisterModule(1, [0x11, 0x22], 0x20, 0x10, 0);
|
||||||
|
var secondOffset = RegisterModule(2, [0x7A], 0x18, 0x20, 8);
|
||||||
|
Debug.Assert(firstOffset == 0x20, "First Variant II TLS offset is incorrect.");
|
||||||
|
Debug.Assert(secondOffset >= firstOffset + 0x18, "TLS modules overlap in the static layout.");
|
||||||
|
Debug.Assert((unchecked(0UL - secondOffset) & 0x1F) == 8, "PT_TLS virtual-address congruence was not preserved.");
|
||||||
|
|
||||||
|
var lateEntry = CreateDynamicEntry(_modules[2]);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.Assert(lateEntry.AllocationBase != 0, "A late TLS module did not receive a dynamic DTV block.");
|
||||||
|
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address)) == 0x7A, "Late-module tdata was not initialized.");
|
||||||
|
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address + 1)) == 0, "Late-module tbss was not zero initialized.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(lateEntry.AllocationBase);
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ public static class HleDataSymbols
|
|||||||
private const string LibcNeedFlagNid = "P330P3dFF68";
|
private const string LibcNeedFlagNid = "P330P3dFF68";
|
||||||
private const string LibcInternalNeedFlagNid = "ZT4ODD2Ts9o";
|
private const string LibcInternalNeedFlagNid = "ZT4ODD2Ts9o";
|
||||||
private const int ProgNameMaxBytes = 511;
|
private const int ProgNameMaxBytes = 511;
|
||||||
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBABEUL;
|
// Terminator canaries reserve the low byte as NUL. Keep the process data
|
||||||
|
// symbol and every per-thread TLS copy byte-for-byte identical.
|
||||||
|
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBA00UL;
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
||||||
|
|||||||
@@ -163,13 +163,18 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
private const int PROT_EXEC = 0x4;
|
private const int PROT_EXEC = 0x4;
|
||||||
|
|
||||||
private const int MAP_PRIVATE = 0x02;
|
private const int MAP_PRIVATE = 0x02;
|
||||||
private const int MAP_FIXED = 0x10;
|
|
||||||
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
||||||
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
||||||
|
|
||||||
// Linux-only: fail instead of clobbering an existing mapping.
|
// Linux-only: fail instead of clobbering an existing mapping.
|
||||||
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
||||||
|
|
||||||
|
private const int KERN_SUCCESS = 0;
|
||||||
|
|
||||||
|
// On Darwin fixed placement is represented by the absence of
|
||||||
|
// VM_FLAGS_ANYWHERE. Do not add VM_FLAGS_OVERWRITE: overlap must fail.
|
||||||
|
private const int VM_FLAGS_FIXED = 0;
|
||||||
|
|
||||||
private static readonly nint MAP_FAILED = -1;
|
private static readonly nint MAP_FAILED = -1;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
@@ -181,6 +186,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
public ulong Size;
|
public ulong Size;
|
||||||
public uint DefaultProtect;
|
public uint DefaultProtect;
|
||||||
public Dictionary<ulong, uint>? PageProtects;
|
public Dictionary<ulong, uint>? PageProtects;
|
||||||
|
public bool UsesMachAllocation;
|
||||||
|
|
||||||
public ulong End => Base + Size;
|
public ulong End => Base + Size;
|
||||||
|
|
||||||
@@ -251,6 +257,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
}
|
}
|
||||||
|
|
||||||
nint result;
|
nint result;
|
||||||
|
var usesMachAllocation = false;
|
||||||
if (address != null)
|
if (address != null)
|
||||||
{
|
{
|
||||||
// Win32 maps at exactly the requested address or fails
|
// Win32 maps at exactly the requested address or fails
|
||||||
@@ -280,15 +287,17 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
if (result == MAP_FAILED && OperatingSystem.IsMacOS())
|
if (result == MAP_FAILED && OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
// The hint-only attempt above didn't land at the requested
|
// The hint-only attempt above didn't land at the requested
|
||||||
// address. This is routinely the case for the PS5's fixed
|
// address. mach_vm_allocate with fixed placement and no
|
||||||
// 32 GiB image base under Rosetta 2 on Apple Silicon, where
|
// overwrite flag atomically maps the requested range or
|
||||||
// the kernel never honors that hint. Retry with true
|
// fails if any host mapping already owns it. Unlike
|
||||||
// MAP_FIXED: this can clobber an untracked host mapping
|
// MAP_FIXED, it cannot clobber CLR, dyld, JIT, or Rosetta
|
||||||
// (dyld, the runtime's JIT heap, Rosetta) if one already
|
// memory that is absent from our shadow table.
|
||||||
// sits exactly there, but without it guest images that
|
Trace($"exact mmap hint failed, retrying with fixed Mach allocation: addr=0x{(ulong)address:X16}");
|
||||||
// require this base never load at all on macOS.
|
result = AllocateDarwinFixed(
|
||||||
Trace($"exact mmap hint failed, retrying with MAP_FIXED: addr=0x{(ulong)address:X16}");
|
(nint)address,
|
||||||
result = mmap((nint)address, (nuint)alignedSize, posixProtect, flags | MAP_FIXED, -1, 0);
|
alignedSize,
|
||||||
|
posixProtect);
|
||||||
|
usesMachAllocation = result != MAP_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||||
@@ -316,7 +325,8 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
{
|
{
|
||||||
Base = (ulong)result,
|
Base = (ulong)result,
|
||||||
Size = alignedSize,
|
Size = alignedSize,
|
||||||
DefaultProtect = protect
|
DefaultProtect = protect,
|
||||||
|
UsesMachAllocation = usesMachAllocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (void*)result;
|
return (void*)result;
|
||||||
@@ -335,8 +345,15 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Regions.Remove((ulong)address);
|
var released = region.UsesMachAllocation
|
||||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
? mach_vm_deallocate(mach_task_self(), (ulong)address, region.Size) == KERN_SUCCESS
|
||||||
|
: munmap((nint)address, (nuint)region.Size) == 0;
|
||||||
|
if (released)
|
||||||
|
{
|
||||||
|
Regions.Remove((ulong)address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return released;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +521,40 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static nint AllocateDarwinFixed(nint requestedAddress, ulong size, int posixProtect)
|
||||||
|
{
|
||||||
|
var address = (ulong)requestedAddress;
|
||||||
|
var result = mach_vm_allocate(
|
||||||
|
mach_task_self(),
|
||||||
|
ref address,
|
||||||
|
size,
|
||||||
|
VM_FLAGS_FIXED);
|
||||||
|
if (result != KERN_SUCCESS || address != (ulong)requestedAddress)
|
||||||
|
{
|
||||||
|
if (result == KERN_SUCCESS)
|
||||||
|
{
|
||||||
|
_ = mach_vm_deallocate(mach_task_self(), address, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace(
|
||||||
|
$"fixed Mach allocation failed: addr=0x{(ulong)requestedAddress:X16} " +
|
||||||
|
$"size=0x{size:X} kern_return={result}");
|
||||||
|
return MAP_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mprotect((nint)address, (nuint)size, posixProtect) == 0)
|
||||||
|
{
|
||||||
|
return (nint)address;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = Marshal.GetLastPInvokeError();
|
||||||
|
_ = mach_vm_deallocate(mach_task_self(), address, size);
|
||||||
|
Trace(
|
||||||
|
$"fixed Mach allocation protection failed: addr=0x{address:X16} " +
|
||||||
|
$"size=0x{size:X} errno={error}");
|
||||||
|
return MAP_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
private static void Trace(string message)
|
private static void Trace(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||||
@@ -524,5 +575,14 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||||||
|
|
||||||
[DllImport("libc", SetLastError = true)]
|
[DllImport("libc", SetLastError = true)]
|
||||||
private static extern int mprotect(nint addr, nuint length, int prot);
|
private static extern int mprotect(nint addr, nuint length, int prot);
|
||||||
|
|
||||||
|
[DllImport("libSystem.B.dylib")]
|
||||||
|
private static extern uint mach_task_self();
|
||||||
|
|
||||||
|
[DllImport("libSystem.B.dylib")]
|
||||||
|
private static extern int mach_vm_allocate(uint target, ref ulong address, ulong size, int flags);
|
||||||
|
|
||||||
|
[DllImport("libSystem.B.dylib")]
|
||||||
|
private static extern int mach_vm_deallocate(uint target, ulong address, ulong size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ using System.Collections.Concurrent;
|
|||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs work on the real process main thread. GLFW windowing must live on
|
/// Runs work on the real process main thread. macOS only allows AppKit (and
|
||||||
/// that thread on macOS (AppKit) and Linux (X11's single event queue), so the
|
/// therefore GLFW windowing) on that thread, so the CLI moves emulation onto
|
||||||
/// CLI moves emulation onto a worker thread, parks the main thread in
|
/// a worker thread, parks the main thread in <see cref="Pump"/>, and the
|
||||||
/// <see cref="Pump"/>, and the video presenter posts its window loop here. On
|
/// video presenter posts its window loop here. On other platforms
|
||||||
/// Windows <see cref="IsAvailable"/> stays false and the window keeps its own
|
/// <see cref="IsAvailable"/> stays false and nothing changes.
|
||||||
/// thread.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HostMainThread
|
public static class HostMainThread
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,498 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cross-platform host virtual memory API with Win32 semantics.
|
||||||
|
/// On Windows this forwards directly to kernel32. On POSIX systems it is
|
||||||
|
/// implemented over mmap/mprotect/munmap with a shadow region table that
|
||||||
|
/// answers VirtualQuery-style questions and tracks page protections.
|
||||||
|
/// POSIX anonymous mappings are demand-paged by the kernel, so Win32
|
||||||
|
/// "reserve-only" regions are mapped as committed memory directly and
|
||||||
|
/// commit requests become protection changes.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe class HostMemory
|
||||||
|
{
|
||||||
|
public const uint MEM_COMMIT = 0x1000;
|
||||||
|
public const uint MEM_RESERVE = 0x2000;
|
||||||
|
public const uint MEM_RELEASE = 0x8000;
|
||||||
|
public const uint MEM_FREE_STATE = 0x10000;
|
||||||
|
public const uint MEM_PRIVATE = 0x20000;
|
||||||
|
|
||||||
|
public const uint PAGE_NOACCESS = 0x01;
|
||||||
|
public const uint PAGE_READONLY = 0x02;
|
||||||
|
public const uint PAGE_READWRITE = 0x04;
|
||||||
|
public const uint PAGE_EXECUTE = 0x10;
|
||||||
|
public const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
|
public const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
|
|
||||||
|
private const ulong PageSize = 0x1000;
|
||||||
|
|
||||||
|
/// <summary>Win32 MEMORY_BASIC_INFORMATION (64-bit) layout.</summary>
|
||||||
|
public struct BasicInfo
|
||||||
|
{
|
||||||
|
public ulong BaseAddress;
|
||||||
|
public ulong AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public uint Alignment1;
|
||||||
|
public ulong RegionSize;
|
||||||
|
public uint State;
|
||||||
|
public uint Protect;
|
||||||
|
public uint Type;
|
||||||
|
public uint Alignment2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualAlloc(address, size, allocationType, protect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Alloc(address, size, allocationType, protect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Free(void* address, nuint size, uint freeType)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualFree(address, size, freeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Free(address, size, freeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualProtect(address, size, newProtect, out oldProtect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Protect(address, size, newProtect, out oldProtect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nuint Query(void* address, out BasicInfo info)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualQuery(address, out info, (nuint)sizeof(BasicInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Query(address, out info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void FlushInstructionCache(void* address, nuint size)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
Win32FlushInstructionCache(Win32GetCurrentProcess(), address, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The emulator only executes x86-64 guest code, so a non-Windows host
|
||||||
|
// is either x86-64 (including Rosetta 2 translation) with a coherent
|
||||||
|
// instruction cache, or would need sys_icache_invalidate for a future
|
||||||
|
// arm64 recompiler. Nothing to do today.
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualAlloc", SetLastError = true)]
|
||||||
|
private static extern void* Win32VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualFree", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool Win32VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualProtect", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool Win32VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualQuery")]
|
||||||
|
private static extern nuint Win32VirtualQuery(void* lpAddress, out BasicInfo lpBuffer, nuint dwLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess")]
|
||||||
|
private static extern void* Win32GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "FlushInstructionCache")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool Win32FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
|
private static class Posix
|
||||||
|
{
|
||||||
|
private const int PROT_NONE = 0x0;
|
||||||
|
private const int PROT_READ = 0x1;
|
||||||
|
private const int PROT_WRITE = 0x2;
|
||||||
|
private const int PROT_EXEC = 0x4;
|
||||||
|
|
||||||
|
private const int MAP_PRIVATE = 0x02;
|
||||||
|
private const int MAP_FIXED = 0x10;
|
||||||
|
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
||||||
|
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
||||||
|
|
||||||
|
// Linux-only: fail instead of clobbering an existing mapping.
|
||||||
|
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
||||||
|
|
||||||
|
private static readonly nint MAP_FAILED = -1;
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly SortedList<ulong, Region> Regions = new();
|
||||||
|
|
||||||
|
private sealed class Region
|
||||||
|
{
|
||||||
|
public ulong Base;
|
||||||
|
public ulong Size;
|
||||||
|
public uint DefaultProtect;
|
||||||
|
public Dictionary<ulong, uint>? PageProtects;
|
||||||
|
|
||||||
|
public ulong End => Base + Size;
|
||||||
|
|
||||||
|
public uint ProtectAt(ulong pageAddress)
|
||||||
|
{
|
||||||
|
if (PageProtects is not null && PageProtects.TryGetValue(pageAddress, out var overriden))
|
||||||
|
{
|
||||||
|
return overriden;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultProtect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedSize = AlignUp((ulong)size, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (allocationType == MEM_COMMIT && address != null &&
|
||||||
|
TryFindRegionLocked((ulong)address, out var existing))
|
||||||
|
{
|
||||||
|
// Note: MEM_RESERVE requests that overlap an existing
|
||||||
|
// region must fail like Win32 does; only a pure commit
|
||||||
|
// may target pages inside a tracked mapping.
|
||||||
|
// Commit inside an existing mapping: the pages are already
|
||||||
|
// backed (demand paged), so only apply the protection.
|
||||||
|
var start = AlignDown((ulong)address, PageSize);
|
||||||
|
var end = Math.Min(existing.End, AlignUp((ulong)address + alignedSize, PageSize));
|
||||||
|
if (end <= start)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(protect)) != 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtectRangeLocked(existing, start, end - start, protect);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((allocationType & MEM_RESERVE) == 0)
|
||||||
|
{
|
||||||
|
// MEM_COMMIT alone outside any known region is invalid here.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var posixProtect = ToPosixProtect(protect);
|
||||||
|
var flags = MAP_PRIVATE | MAP_ANON;
|
||||||
|
if ((allocationType & MEM_COMMIT) == 0)
|
||||||
|
{
|
||||||
|
// Reserve-only: keep the requested protection so the region
|
||||||
|
// is usable without a separate commit step, but tell the
|
||||||
|
// kernel not to account swap for it where supported.
|
||||||
|
flags |= MAP_NORESERVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
nint result;
|
||||||
|
if (address != null)
|
||||||
|
{
|
||||||
|
// Win32 maps at exactly the requested address or fails
|
||||||
|
// without touching existing mappings. Fail up front on
|
||||||
|
// any overlap we track, then place the mapping: Linux
|
||||||
|
// gets MAP_FIXED_NOREPLACE (fails cleanly on host
|
||||||
|
// mappings too). Darwin lacks NOREPLACE and plain
|
||||||
|
// MAP_FIXED would silently clobber untracked host
|
||||||
|
// memory (dyld, the runtime's JIT heap, Rosetta), so
|
||||||
|
// pass the address as a hint instead -- the kernel
|
||||||
|
// honors it when the range is free and relocates the
|
||||||
|
// mapping otherwise, which we treat as failure.
|
||||||
|
if (OverlapsTrackedRegionLocked((ulong)address, alignedSize))
|
||||||
|
{
|
||||||
|
Trace($"exact overlap: addr=0x{(ulong)address:X16} size=0x{alignedSize:X}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
|
||||||
|
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
|
||||||
|
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||||
|
{
|
||||||
|
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
if (result != MAP_FAILED)
|
||||||
|
{
|
||||||
|
munmap(result, (nuint)alignedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = mmap(0, (nuint)alignedSize, posixProtect, flags, -1, 0);
|
||||||
|
if (result == MAP_FAILED)
|
||||||
|
{
|
||||||
|
Trace($"mmap failed: size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Regions[(ulong)result] = new Region
|
||||||
|
{
|
||||||
|
Base = (ulong)result,
|
||||||
|
Size = alignedSize,
|
||||||
|
DefaultProtect = protect
|
||||||
|
};
|
||||||
|
|
||||||
|
return (void*)result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Free(void* address, nuint size, uint freeType)
|
||||||
|
{
|
||||||
|
_ = size;
|
||||||
|
_ = freeType;
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!Regions.TryGetValue((ulong)address, out var region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Regions.Remove((ulong)address);
|
||||||
|
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||||
|
{
|
||||||
|
oldProtect = PAGE_NOACCESS;
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = AlignDown((ulong)address, PageSize);
|
||||||
|
var end = AlignUp((ulong)address + size, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!TryFindRegionLocked(start, out var region) || end > region.End)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldProtect = region.ProtectAt(start);
|
||||||
|
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(newProtect)) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtectRangeLocked(region, start, end - start, newProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nuint Query(void* address, out BasicInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (TryFindRegionLocked(pageAddress, out var region))
|
||||||
|
{
|
||||||
|
// Win32 VirtualQuery reports a run of pages sharing the
|
||||||
|
// same protection, so stop the run where it changes. Do
|
||||||
|
// not walk the whole mapping page-by-page here: PS5 GPU
|
||||||
|
// apertures can span hundreds of GiB, while protection
|
||||||
|
// overrides are sparse. A tiny read inside such a mapping
|
||||||
|
// otherwise performs tens of millions of dictionary
|
||||||
|
// lookups before it can return.
|
||||||
|
var pageProtects = region.PageProtects;
|
||||||
|
uint protect;
|
||||||
|
ulong runEnd;
|
||||||
|
if (pageProtects is null || pageProtects.Count == 0)
|
||||||
|
{
|
||||||
|
protect = region.DefaultProtect;
|
||||||
|
runEnd = region.End;
|
||||||
|
}
|
||||||
|
else if (pageProtects.TryGetValue(pageAddress, out protect))
|
||||||
|
{
|
||||||
|
runEnd = pageAddress + PageSize;
|
||||||
|
while (runEnd < region.End &&
|
||||||
|
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
|
||||||
|
nextProtect == protect)
|
||||||
|
{
|
||||||
|
runEnd += PageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
protect = region.DefaultProtect;
|
||||||
|
runEnd = region.End;
|
||||||
|
foreach (var overrideAddress in pageProtects.Keys)
|
||||||
|
{
|
||||||
|
if (overrideAddress > pageAddress && overrideAddress < runEnd)
|
||||||
|
{
|
||||||
|
runEnd = overrideAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseAddress = pageAddress;
|
||||||
|
info.AllocationBase = region.Base;
|
||||||
|
info.AllocationProtect = region.DefaultProtect;
|
||||||
|
info.RegionSize = runEnd - pageAddress;
|
||||||
|
info.State = MEM_COMMIT;
|
||||||
|
info.Protect = protect;
|
||||||
|
info.Type = MEM_PRIVATE;
|
||||||
|
return (nuint)sizeof(BasicInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untracked host memory (runtime heaps, stacks, libraries) is
|
||||||
|
// reported as a free block reaching to the next tracked region
|
||||||
|
// so scanning callers keep advancing.
|
||||||
|
var nextBase = ulong.MaxValue;
|
||||||
|
foreach (var regionBase in Regions.Keys)
|
||||||
|
{
|
||||||
|
if (regionBase > pageAddress)
|
||||||
|
{
|
||||||
|
nextBase = regionBase;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseAddress = pageAddress;
|
||||||
|
info.AllocationBase = 0;
|
||||||
|
info.AllocationProtect = PAGE_NOACCESS;
|
||||||
|
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
|
||||||
|
info.State = MEM_FREE_STATE;
|
||||||
|
info.Protect = PAGE_NOACCESS;
|
||||||
|
info.Type = 0;
|
||||||
|
return (nuint)sizeof(BasicInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool OverlapsTrackedRegionLocked(ulong start, ulong size)
|
||||||
|
{
|
||||||
|
var end = start + size;
|
||||||
|
foreach (var region in Regions.Values)
|
||||||
|
{
|
||||||
|
if (region.Base < end && start < region.End)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindRegionLocked(ulong address, out Region region)
|
||||||
|
{
|
||||||
|
region = null!;
|
||||||
|
var keys = Regions.Keys;
|
||||||
|
var low = 0;
|
||||||
|
var high = keys.Count - 1;
|
||||||
|
Region? candidate = null;
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
var middle = low + ((high - low) >> 1);
|
||||||
|
var entry = Regions.Values[middle];
|
||||||
|
if (entry.Base <= address)
|
||||||
|
{
|
||||||
|
candidate = entry;
|
||||||
|
low = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = middle - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate is null || address >= candidate.End)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
region = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
|
||||||
|
{
|
||||||
|
if (start == region.Base && size >= region.Size)
|
||||||
|
{
|
||||||
|
region.DefaultProtect = protect;
|
||||||
|
region.PageProtects = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.PageProtects ??= new Dictionary<ulong, uint>();
|
||||||
|
var end = start + size;
|
||||||
|
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
|
||||||
|
{
|
||||||
|
if (protect == region.DefaultProtect)
|
||||||
|
{
|
||||||
|
region.PageProtects.Remove(pageAddress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
region.PageProtects[pageAddress] = protect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ToPosixProtect(uint win32Protect)
|
||||||
|
{
|
||||||
|
return win32Protect switch
|
||||||
|
{
|
||||||
|
PAGE_NOACCESS => PROT_NONE,
|
||||||
|
PAGE_READONLY => PROT_READ,
|
||||||
|
PAGE_READWRITE => PROT_READ | PROT_WRITE,
|
||||||
|
PAGE_EXECUTE => PROT_READ | PROT_EXEC,
|
||||||
|
PAGE_EXECUTE_READ => PROT_READ | PROT_EXEC,
|
||||||
|
PAGE_EXECUTE_READWRITE => PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||||
|
_ => PROT_READ | PROT_WRITE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[HOSTMEM] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignDown(ulong value, ulong alignment) => value & ~(alignment - 1);
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment) => checked((value + alignment - 1) & ~(alignment - 1));
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern nint mmap(nint addr, nuint length, int prot, int flags, int fd, long offset);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int munmap(nint addr, nuint length);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int mprotect(nint addr, nuint length, int prot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="SharpEmu.Core" />
|
<InternalsVisibleTo Include="SharpEmu.Core" />
|
||||||
|
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+6196
-1541
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
public static partial class AgcExports
|
||||||
|
{
|
||||||
|
// Exact Gen5 primary register defaults originally reverse-engineered by Kyty (MIT).
|
||||||
|
// Hashes, offsets, and values match the tables consumed by the PS5 AGC SDK.
|
||||||
|
private static RegisterDefaultGroup[] CreatePrimaryRegisterDefaults() =>
|
||||||
|
[
|
||||||
|
// context register groups
|
||||||
|
new(0u, 0u, 0xE24F806Du, [new(0x202u, 0x00CC0010u)]), // CB_COLOR_CONTROL
|
||||||
|
new(0u, 1u, 0xF6C28182u, [new(0x109u, 0x00000000u)]), // CB_DCC_CONTROL
|
||||||
|
new(0u, 2u, 0x6F6E55A5u, [new(0x104u, 0x00000000u)]), // CB_RMI_GL2_CACHE_CONTROL
|
||||||
|
new(0u, 3u, 0x0BC65DA4u, [new(0x08Fu, 0x00000000u)]), // CB_SHADER_MASK
|
||||||
|
new(0u, 4u, 0x9E5AD592u, [new(0x08Eu, 0x0000000Fu)]), // CB_TARGET_MASK
|
||||||
|
new(0u, 5u, 0xBB513B98u, [new(0x2DCu, 0x0000AA00u)]), // DB_ALPHA_TO_MASK
|
||||||
|
new(0u, 6u, 0xAB64B23Bu, [new(0x001u, 0x00000000u)]), // DB_COUNT_CONTROL
|
||||||
|
new(0u, 7u, 0x53C39964u, [new(0x200u, 0x00000000u)]), // DB_DEPTH_CONTROL
|
||||||
|
new(0u, 8u, 0x01396B11u, [new(0x201u, 0x00000000u)]), // DB_EQAA
|
||||||
|
new(0u, 9u, 0x7D42019Au, [new(0x000u, 0x00000000u)]), // DB_RENDER_CONTROL
|
||||||
|
new(0u, 10u, 0x3548F523u, [new(0x006u, 0x00000000u)]), // PS_SHADER_SAMPLE_EXCLUSION_MASK
|
||||||
|
new(0u, 11u, 0xF43AD28Au, [new(0x01Fu, 0x00000000u)]), // DB_RMI_L2_CACHE_CONTROL
|
||||||
|
new(0u, 12u, 0x6DE4C312u, [new(0x203u, 0x00000000u)]), // DB_SHADER_CONTROL
|
||||||
|
new(0u, 13u, 0x00A77AE0u, [new(0x2B0u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE0
|
||||||
|
new(0u, 14u, 0x00A779B7u, [new(0x2B1u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE1
|
||||||
|
new(0u, 15u, 0x5100100Cu, [new(0x10Cu, 0x00000000u)]), // DB_STENCILREFMASK
|
||||||
|
new(0u, 16u, 0x59958BBAu, [new(0x10Du, 0x00000000u)]), // DB_STENCILREFMASK_BF
|
||||||
|
new(0u, 17u, 0x0C06F17Cu, [new(0x10Bu, 0x00000000u)]), // DB_STENCIL_CONTROL
|
||||||
|
new(0u, 18u, 0x6F104B72u, [new(0x1FFu, 0x00000000u)]), // GE_MAX_OUTPUT_PER_SUBGROUP
|
||||||
|
new(0u, 19u, 0x25C70D9Cu, [new(0x204u, 0x00000000u)]), // PA_CL_CLIP_CNTL
|
||||||
|
new(0u, 20u, 0x3881201Eu, [new(0x20Du, 0x00000000u)]), // PA_CL_OBJPRIM_ID_CNTL
|
||||||
|
new(0u, 21u, 0x09AFDDAFu, [new(0x206u, 0x0000043Fu)]), // PA_CL_VTE_CNTL
|
||||||
|
new(0u, 22u, 0x367D63CFu, [new(0x2F8u, 0x00000000u)]), // PA_SC_AA_CONFIG
|
||||||
|
new(0u, 23u, 0x43707DB8u, [new(0x083u, 0x0000FFFFu)]), // PA_SC_CLIPRECT_RULE
|
||||||
|
new(0u, 24u, 0xF6AE26BAu, [new(0x313u, 0x00000000u)]), // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL
|
||||||
|
new(0u, 25u, 0x1B917652u, [new(0x800003FEu, 0x00000000u)]), // PA_SC_FSR_ENABLE
|
||||||
|
new(0u, 26u, 0x94B1E4F7u, [new(0x0EAu, 0x00000000u)]), // PA_SC_HORIZ_GRID
|
||||||
|
new(0u, 27u, 0xE3661B6Cu, [new(0x0E9u, 0x00000000u)]), // PA_SC_LEFT_VERT_GRID
|
||||||
|
new(0u, 28u, 0x1EB8D73Au, [new(0x292u, 0x00000002u)]), // PA_SC_MODE_CNTL_0
|
||||||
|
new(0u, 29u, 0x15051FA3u, [new(0x293u, 0x00000000u)]), // PA_SC_MODE_CNTL_1
|
||||||
|
new(0u, 30u, 0x9C51A7F1u, [new(0x0E8u, 0x00000000u)]), // PA_SC_RIGHT_VERT_GRID
|
||||||
|
new(0u, 31u, 0xA20EFC70u, [new(0x080u, 0x00000000u)]), // PA_SC_WINDOW_OFFSET
|
||||||
|
new(0u, 32u, 0x0EC09F6Eu, [new(0x211u, 0x00000000u)]), // PA_STATE_STEREO_X
|
||||||
|
new(0u, 33u, 0x34A7D6D3u, [new(0x210u, 0x00000000u)]), // PA_STEREO_CNTL
|
||||||
|
new(0u, 34u, 0xCE831B94u, [new(0x08Du, 0x00000000u)]), // PA_SU_HARDWARE_SCREEN_OFFSET
|
||||||
|
new(0u, 35u, 0x5CC72A74u, [new(0x282u, 0x00000008u)]), // PA_SU_LINE_CNTL
|
||||||
|
new(0u, 36u, 0x3B77713Cu, [new(0x281u, 0xFFFF0000u)]), // PA_SU_POINT_MINMAX
|
||||||
|
new(0u, 37u, 0x40F64410u, [new(0x280u, 0x00080008u)]), // PA_SU_POINT_SIZE
|
||||||
|
new(0u, 38u, 0x69441268u, [new(0x2DFu, 0x00000000u)]), // PA_SU_POLY_OFFSET_CLAMP
|
||||||
|
new(0u, 39u, 0x2E418B83u, [new(0x2DEu, 0x000001E9u)]), // PA_SU_POLY_OFFSET_DB_FMT_CNTL
|
||||||
|
new(0u, 40u, 0xA00D0C8Du, [new(0x205u, 0x00000240u)]), // PA_SU_SC_MODE_CNTL
|
||||||
|
new(0u, 41u, 0xB1289FB3u, [new(0x20Cu, 0x00000001u)]), // PA_SU_SMALL_PRIM_FILTER_CNTL
|
||||||
|
new(0u, 42u, 0x144832FBu, [new(0x2F9u, 0x0000002Du)]), // PA_SU_VTX_CNTL
|
||||||
|
new(0u, 43u, 0x9890D9FAu, [new(0x1BAu, 0x00000000u)]), // SPI_TMPRING_SIZE
|
||||||
|
new(0u, 44u, 0x9016FAF1u, [new(0x2A6u, 0x00000000u)]), // VGT_DRAW_PAYLOAD_CNTL
|
||||||
|
new(0u, 45u, 0x4B73CE27u, [new(0x2CEu, 0x00000400u)]), // VGT_GS_MAX_VERT_OUT
|
||||||
|
new(0u, 46u, 0x5F5A3E7Bu, [new(0x29Bu, 0x00000002u)]), // VGT_GS_OUT_PRIM_TYPE
|
||||||
|
new(0u, 47u, 0xD4AF3A51u, [new(0x2D6u, 0x00000000u)]), // VGT_LS_HS_CONFIG
|
||||||
|
new(0u, 48u, 0x6CF4F543u, [new(0x2A3u, 0xFFFFFFFFu)]), // VGT_PRIMITIVEID_RESET
|
||||||
|
new(0u, 49u, 0x5FB86CCBu, [new(0x2A1u, 0x00000000u)]), // VGT_PRIMITIVEID_EN
|
||||||
|
new(0u, 50u, 0xEDEFA188u, [new(0x2ADu, 0x00000000u)]), // VGT_REUSE_OFF
|
||||||
|
new(0u, 51u, 0xD0DE9EE6u, [new(0x2D5u, 0x00000000u)]), // VGT_SHADER_STAGES_EN
|
||||||
|
new(0u, 52u, 0xC5831803u, [new(0x2D4u, 0x88101000u)]), // VGT_TESS_DISTRIBUTION
|
||||||
|
new(0u, 53u, 0x8E6DE84Bu, [new(0x2DBu, 0x00000000u)]), // VGT_TF_PARAM
|
||||||
|
new(0u, 54u, 0xD0771662u, [new(0x2F5u, 0x00000000u), new(0x2F6u, 0x00000000u)]), // PA_SC_CENTROID_PRIORITY_0, PA_SC_CENTROID_PRIORITY_1
|
||||||
|
new(0u, 55u, 0x569F7444u, [new(0x2FEu, 0x00000000u)]), // PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_0
|
||||||
|
new(0u, 56u, 0x5C6637CDu, [new(0x30Eu, 0xFFFFFFFFu), new(0x30Fu, 0xFFFFFFFFu)]), // PA_SC_AA_MASK_X0Y0_X1Y0, PA_SC_AA_MASK_X0Y1_X1Y1
|
||||||
|
new(0u, 57u, 0xCAE3E690u, [new(0x311u, 0x00000002u), new(0x312u, 0x03FF0080u)]), // PA_SC_BINNER_CNTL_0, PA_SC_BINNER_CNTL_1
|
||||||
|
new(0u, 58u, 0x43FBD769u, [new(0x105u, 0x00000000u), new(0x107u, 0x00000000u), new(0x106u, 0x00000000u), new(0x108u, 0x00000000u)]), // CB_BLEND_RED, CB_BLEND_BLUE, CB_BLEND_GREEN, CB_BLEND_ALPHA
|
||||||
|
new(0u, 59u, 0xEF550356u, [new(0x1E0u, 0x20010001u)]), // CB_BLEND0_CONTROL
|
||||||
|
new(0u, 60u, 0x8F52E279u, [new(0x020u, 0x00000000u), new(0x021u, 0x00000000u)]), // TA_BC_BASE_ADDR, TA_BC_BASE_ADDR_HI
|
||||||
|
new(0u, 61u, 0x1F2D8149u, [new(0x084u, 0x00000000u), new(0x085u, 0x20002000u)]), // PA_SC_CLIPRECT_0_TL, PA_SC_CLIPRECT_0_BR
|
||||||
|
new(0u, 62u, 0x853D0614u, [new(0x800003FFu, 0x00000000u)]), // CX_NOP
|
||||||
|
new(0u, 63u, 0x4413C6F9u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // DB_DEPTH_BOUNDS_MIN, DB_DEPTH_BOUNDS_MAX
|
||||||
|
new(0u, 64u, 0x67096014u, [new(0x010u, 0x80000000u), new(0x011u, 0x20000000u), new(0x012u, 0x00000000u), new(0x013u, 0x00000000u), new(0x014u, 0x00000000u), new(0x015u, 0x00000000u), new(0x01Au, 0x00000000u), new(0x01Bu, 0x00000000u), new(0x01Cu, 0x00000000u), new(0x01Du, 0x00000000u), new(0x01Eu, 0x00000000u), new(0x002u, 0x00000000u), new(0x005u, 0x00000000u), new(0x007u, 0x00000000u), new(0x00Bu, 0x00000000u), new(0x00Au, 0x00000000u)]), // DB_Z_INFO, DB_STENCIL_INFO, DB_Z_READ_BASE, DB_STENCIL_READ_BASE, DB_Z_WRITE_BASE, DB_STENCIL_WRITE_BASE, DB_Z_READ_BASE_HI, DB_STENCIL_READ_BASE_HI, DB_Z_WRITE_BASE_HI, DB_STENCIL_WRITE_BASE_HI, DB_HTILE_DATA_BASE_HI, DB_DEPTH_VIEW, DB_HTILE_DATA_BASE, DB_DEPTH_SIZE_XY, DB_DEPTH_CLEAR, DB_STENCIL_CLEAR
|
||||||
|
new(0u, 65u, 0x88F5E915u, [new(0x0EBu, 0xFF00FF00u), new(0x0ECu, 0x00000000u)]), // PA_SC_FOV_WINDOW_LR, PA_SC_FOV_WINDOW_TB
|
||||||
|
new(0u, 66u, 0x033F1EFFu, [new(0x800003FCu, 0x00000000u), new(0x800003FDu, 0x00000000u)]), // FSR_RECURSIONS0, FSR_RECURSIONS1
|
||||||
|
new(0u, 67u, 0x918106BBu, [new(0x090u, 0x80000000u), new(0x091u, 0x40004000u)]), // PA_SC_GENERIC_SCISSOR_TL, PA_SC_GENERIC_SCISSOR_BR
|
||||||
|
new(0u, 68u, 0x95F0E7ACu, [new(0x2FAu, 0x4E7E0000u), new(0x2FBu, 0x4E7E0000u), new(0x2FCu, 0x4E7E0000u), new(0x2FDu, 0x4E7E0000u)]), // PA_CL_GB_VERT_CLIP_ADJ, PA_CL_GB_VERT_DISC_ADJ, PA_CL_GB_HORZ_CLIP_ADJ, PA_CL_GB_HORZ_DISC_ADJ
|
||||||
|
new(0u, 69u, 0xB48CBAB2u, [new(0x2E2u, 0x00000000u), new(0x2E3u, 0x00000000u)]), // PA_SU_POLY_OFFSET_BACK_SCALE, PA_SU_POLY_OFFSET_BACK_OFFSET
|
||||||
|
new(0u, 70u, 0x05BB3BC6u, [new(0x2E0u, 0x00000000u), new(0x2E1u, 0x00000000u)]), // PA_SU_POLY_OFFSET_FRONT_SCALE, PA_SU_POLY_OFFSET_FRONT_OFFSET
|
||||||
|
new(0u, 71u, 0x94FABA07u, [new(0x003u, 0x00000000u), new(0x004u, 0x00000000u)]), // DB_RENDER_OVERRIDE, DB_RENDER_OVERRIDE2
|
||||||
|
new(0u, 72u, 0x38E92C91u, [new(0x318u, 0x00000000u), new(0x31Bu, 0x00000000u), new(0x31Cu, 0x00000000u), new(0x31Du, 0x00000000u), new(0x31Eu, 0x00000048u), new(0x31Fu, 0x00000000u), new(0x321u, 0x00000000u), new(0x323u, 0x00000000u), new(0x324u, 0x00000000u), new(0x325u, 0x00000000u), new(0x390u, 0x00000000u), new(0x398u, 0x00000000u), new(0x3A0u, 0x00000000u), new(0x3A8u, 0x00000000u), new(0x3B0u, 0x00000000u), new(0x3B8u, 0x0006C000u)]), // CB_COLOR0_BASE, CB_COLOR0_VIEW, CB_COLOR0_INFO, CB_COLOR0_ATTRIB, CB_COLOR0_DCC_CONTROL, CB_COLOR0_CMASK, CB_COLOR0_FMASK, CB_COLOR0_CLEAR_WORD0, CB_COLOR0_CLEAR_WORD1, CB_COLOR0_DCC_BASE, CB_COLOR0_BASE_EXT, CB_COLOR0_CMASK_BASE_EXT, CB_COLOR0_FMASK_BASE_EXT, CB_COLOR0_DCC_BASE_EXT, CB_COLOR0_ATTRIB2, CB_COLOR0_ATTRIB3
|
||||||
|
new(0u, 73u, 0x0B177B43u, [new(0x00Cu, 0x00000000u), new(0x00Du, 0x40004000u)]), // PA_SC_SCREEN_SCISSOR_TL, PA_SC_SCREEN_SCISSOR_BR
|
||||||
|
new(0u, 74u, 0x48531062u, [new(0x191u, 0x00000000u)]), // SPI_PS_INPUT_CNTL_0
|
||||||
|
new(0u, 75u, 0xAAA964B9u, [new(0x16Fu, 0x00000000u), new(0x170u, 0x00000000u), new(0x171u, 0x00000000u), new(0x172u, 0x00000000u)]), // PA_CL_UCP_0_X, PA_CL_UCP_0_Y, PA_CL_UCP_0_Z, PA_CL_UCP_0_W
|
||||||
|
new(0u, 76u, 0x7690AF6Fu, [new(0x10Fu, 0x4E7E0000u), new(0x111u, 0x4E7E0000u), new(0x113u, 0x4E7E0000u), new(0x110u, 0x00000000u), new(0x112u, 0x00000000u), new(0x114u, 0x00000000u), new(0x094u, 0x80000000u), new(0x095u, 0x40004000u), new(0x0B4u, 0x00000000u), new(0x0B5u, 0x00000000u)]), // PA_CL_VPORT_XSCALE, PA_CL_VPORT_YSCALE, PA_CL_VPORT_ZSCALE, PA_CL_VPORT_XOFFSET, PA_CL_VPORT_YOFFSET, PA_CL_VPORT_ZOFFSET, PA_SC_VPORT_SCISSOR_0_TL, PA_SC_VPORT_SCISSOR_0_BR, PA_SC_VPORT_ZMIN_0, PA_SC_VPORT_ZMAX_0
|
||||||
|
new(0u, 77u, 0x078D7060u, [new(0x081u, 0x80000000u), new(0x082u, 0x40004000u)]), // PA_SC_WINDOW_SCISSOR_TL, PA_SC_WINDOW_SCISSOR_BR
|
||||||
|
// shader register groups
|
||||||
|
new(1u, 0u, 0x5D6E3EC7u, [new(0x212u, 0x00000000u)]), // COMPUTE_PGM_RSRC1
|
||||||
|
new(1u, 1u, 0x57E7079Au, [new(0x213u, 0x00000000u)]), // COMPUTE_PGM_RSRC2
|
||||||
|
new(1u, 2u, 0x7467FAFDu, [new(0x228u, 0x00000000u)]), // COMPUTE_PGM_RSRC3
|
||||||
|
new(1u, 3u, 0x9E826B50u, [new(0x215u, 0x00000000u)]), // COMPUTE_RESOURCE_LIMITS
|
||||||
|
new(1u, 4u, 0xDC484F18u, [new(0x218u, 0x00000000u)]), // COMPUTE_TMPRING_SIZE
|
||||||
|
new(1u, 5u, 0x5DA8BCA3u, [new(0x08Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_GS
|
||||||
|
new(1u, 6u, 0x5CA726D8u, [new(0x10Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_HS
|
||||||
|
new(1u, 7u, 0x5DD28360u, [new(0x00Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_PS
|
||||||
|
new(1u, 8u, 0x57EFA0BEu, [new(0x08Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_GS
|
||||||
|
new(1u, 9u, 0x502363D5u, [new(0x10Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_HS
|
||||||
|
new(1u, 10u, 0x506D14BDu, [new(0x00Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_PS
|
||||||
|
new(1u, 11u, 0xB2609506u, [new(0x224u, 0x00000000u)]), // COMPUTE_USER_ACCUM_0
|
||||||
|
new(1u, 12u, 0x9E5CFB8Au, [new(0x107u, 0x00000000u), new(0x087u, 0x00000000u), new(0x007u, 0x00000000u)]), // SPI_SHADER_PGM_RSRC3_HS, SPI_SHADER_PGM_RSRC3_GS, SPI_SHADER_PGM_RSRC3_PS
|
||||||
|
new(1u, 13u, 0xC918DF3Eu, [new(0x20Cu, 0x00000000u), new(0x20Du, 0x00000000u)]), // COMPUTE_PGM_LO, COMPUTE_PGM_HI
|
||||||
|
new(1u, 14u, 0xC9751C9Cu, [new(0x0C8u, 0x00000000u), new(0x0C9u, 0x00000000u)]), // SPI_SHADER_PGM_LO_ES, SPI_SHADER_PGM_HI_ES
|
||||||
|
new(1u, 15u, 0xC97EF77Au, [new(0x088u, 0x00000000u), new(0x089u, 0x00000000u)]), // SPI_SHADER_PGM_LO_GS, SPI_SHADER_PGM_HI_GS
|
||||||
|
new(1u, 16u, 0xC927C6B9u, [new(0x108u, 0x00000000u), new(0x109u, 0x00000000u)]), // SPI_SHADER_PGM_LO_HS, SPI_SHADER_PGM_HI_HS
|
||||||
|
new(1u, 17u, 0xC92A1EC5u, [new(0x148u, 0x00000000u), new(0x149u, 0x00000000u)]), // SPI_SHADER_PGM_LO_LS, SPI_SHADER_PGM_HI_LS
|
||||||
|
new(1u, 18u, 0xC9E01B31u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // SPI_SHADER_PGM_LO_PS, SPI_SHADER_PGM_HI_PS
|
||||||
|
new(1u, 19u, 0x50685F29u, [new(0x800002FFu, 0x00000000u)]), // SH_NOP
|
||||||
|
new(1u, 20u, 0xB26219CAu, [new(0x0B2u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_ESGS_0
|
||||||
|
new(1u, 21u, 0xB25B6CF9u, [new(0x132u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_LSHS_0
|
||||||
|
new(1u, 22u, 0xB2F86101u, [new(0x032u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_PS_0
|
||||||
|
new(1u, 23u, 0x07E3B155u, [new(0x082u, 0x00000000u), new(0x083u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_GS, SPI_SHADER_USER_DATA_ADDR_HI_GS
|
||||||
|
new(1u, 24u, 0x07E383C6u, [new(0x102u, 0x00000000u), new(0x103u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_HS, SPI_SHADER_USER_DATA_ADDR_HI_HS
|
||||||
|
new(1u, 25u, 0xBDA98653u, [new(0x240u, 0x00000000u)]), // COMPUTE_USER_DATA_0
|
||||||
|
new(1u, 26u, 0xBDBD1D0Fu, [new(0x08Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_GS_0
|
||||||
|
new(1u, 27u, 0xBD946FD4u, [new(0x10Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_HS_0
|
||||||
|
new(1u, 28u, 0xBDF02A4Cu, [new(0x00Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_PS_0
|
||||||
|
// uconfig register groups
|
||||||
|
new(2u, 0u, 0x19E93E85u, [new(0x41Fu, 0x00000000u)]), // GDS_OA_ADDRESS
|
||||||
|
new(2u, 1u, 0x3B5C2AF3u, [new(0x41Du, 0x00000000u)]), // GDS_OA_CNTL
|
||||||
|
new(2u, 2u, 0x47974A35u, [new(0x41Eu, 0x00000000u)]), // GDS_OA_COUNTER
|
||||||
|
new(2u, 3u, 0x105971C2u, [new(0x25Bu, 0x00000000u)]), // GE_CNTL
|
||||||
|
new(2u, 4u, 0x7D137765u, [new(0x24Au, 0x00000000u)]), // GE_INDX_OFFSET
|
||||||
|
new(2u, 5u, 0xD187FEBCu, [new(0x24Bu, 0x00000000u)]), // GE_MULTI_PRIM_IB_RESET_EN
|
||||||
|
new(2u, 6u, 0x12F854ACu, [new(0x25Fu, 0x00000000u)]), // GE_STEREO_CNTL
|
||||||
|
new(2u, 7u, 0x40D49AD1u, [new(0x262u, 0x00000000u)]), // GE_USER_VGPR_EN
|
||||||
|
new(2u, 8u, 0x8C0923DAu, [new(0x80003FF4u, 0x00000000u)]), // FSR_EXTEND_SUBPIXEL_ROUNDING
|
||||||
|
new(2u, 9u, 0xBB8DF494u, [new(0x80003FFDu, 0x00000000u)]), // TEXTURE_GRADIENT_CONTROL
|
||||||
|
new(2u, 10u, 0xF6D8A76Eu, [new(0x382u, 0x40000040u)]), // TEXTURE_GRADIENT_FACTORS
|
||||||
|
new(2u, 11u, 0x7620F1E9u, [new(0x248u, 0x00000000u)]), // VGT_OBJECT_ID
|
||||||
|
new(2u, 12u, 0x9EBFAB10u, [new(0x242u, 0x00000000u)]), // VGT_PRIMITIVE_TYPE
|
||||||
|
new(2u, 13u, 0x98A09D0Eu, [new(0x380u, 0x00000000u), new(0x381u, 0x00000000u)]), // TA_CS_BC_BASE_ADDR, TA_CS_BC_BASE_ADDR_HI
|
||||||
|
new(2u, 14u, 0x195D37D2u, [new(0x80003FF5u, 0x00000000u), new(0x80003FF6u, 0x00000000u)]), // FSR_ALPHA_VALUE0, FSR_ALPHA_VALUE1
|
||||||
|
new(2u, 15u, 0xF9EC4F85u, [new(0x80003FF7u, 0x00000000u), new(0x80003FF8u, 0x00000000u), new(0x80003FF9u, 0x00000000u), new(0x80003FFAu, 0x00000000u)]), // FSR_CONTROL_POINT0, FSR_CONTROL_POINT1, FSR_CONTROL_POINT2, FSR_CONTROL_POINT3
|
||||||
|
new(2u, 16u, 0x4626B750u, [new(0x80003FFBu, 0x00000000u), new(0x80003FFCu, 0x00000000u)]), // FSR_WINDOW0, FSR_WINDOW1
|
||||||
|
new(2u, 17u, 0x4CC673A0u, [new(0x80003FFEu, 0x00000000u)]), // MEMORY_MAPPING_MASK
|
||||||
|
new(2u, 18u, 0xDE5B3431u, [new(0x80003FFFu, 0x00000000u)]), // UC_NOP
|
||||||
|
new(2u, 19u, 0x036AC8A6u, [new(0x25Cu, 0x00000000u)]), // GE_USER_VGPR1
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
@@ -26,5 +27,7 @@ internal static class AgcShaderCompilerHooks
|
|||||||
{
|
{
|
||||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
||||||
|
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
||||||
|
VulkanVideoPresenter.GuestDataPool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deswizzles RDNA2 (GFX10) tiled texture surfaces into linear layout so they
|
||||||
|
/// can be uploaded to Vulkan. PS5 stores most textures in a swizzled layout
|
||||||
|
/// selected by the 5-bit SWIZZLE_MODE in the image descriptor; uploading those
|
||||||
|
/// bytes verbatim samples as garbage.
|
||||||
|
///
|
||||||
|
/// The GFX10 addressing uses power-of-two swizzle blocks (256 B / 4 KiB /
|
||||||
|
/// 64 KiB) whose internal element order follows the standard (S), display (D),
|
||||||
|
/// render (R), or z-order/depth (Z) equations. This implements the exact base
|
||||||
|
/// S and Z 2D single-sample modes plus the PS5's RB+ 64 KiB Z_X/R_X equations;
|
||||||
|
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||||
|
/// equations are being ported.
|
||||||
|
/// </summary>
|
||||||
|
internal static class GnmTiling
|
||||||
|
{
|
||||||
|
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
|
||||||
|
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
|
||||||
|
// topology. Each entry describes one address bit as an XOR of X/Y bits.
|
||||||
|
private readonly record struct AddressBit(uint XMask, uint YMask);
|
||||||
|
|
||||||
|
private static readonly AddressBit[][] RbPlus64KRenderX =
|
||||||
|
[
|
||||||
|
// 1 byte/element: nibble01=0, nibble2=307, nibble3=379.
|
||||||
|
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(6), Y(6), XY(7, 8), XY(8, 7)],
|
||||||
|
// 2 bytes/element: nibble01=1, nibble2=307, nibble3=389.
|
||||||
|
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(3), X(6), XY(7, 7), XY(8, 6)],
|
||||||
|
// 4 bytes/element: nibble01=39, nibble2=307, nibble3=381.
|
||||||
|
[Zero, Zero, X(0), X(1), Y(0), Y(1), X(2), Y(2),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(3), Y(3), XY(6, 7), XY(7, 6)],
|
||||||
|
// 8 bytes/element: nibble01=6, nibble2=307, nibble3=382.
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), X(1), X(2), Y(1),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(2), X(3), XY(7, 3), XY(6, 6)],
|
||||||
|
// 16 bytes/element: nibble01=7, nibble2=307, nibble3=390.
|
||||||
|
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(2), Y(2), XY(6, 3), XY(3, 6)],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly AddressBit[][] RbPlus64KDepthX =
|
||||||
|
[
|
||||||
|
// 1 byte/element: nibble01=8, nibble2=306, nibble3=379.
|
||||||
|
[X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3), Y(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(6), Y(6), XY(7, 8), XY(8, 7)],
|
||||||
|
// 2 bytes/element: nibble01=9, nibble2=306, nibble3=389.
|
||||||
|
[Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(3), X(6), XY(7, 7), XY(8, 6)],
|
||||||
|
// 4 bytes/element: nibble01=10, nibble2=306, nibble3=381.
|
||||||
|
[Zero, Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(3), Y(3), XY(6, 7), XY(7, 6)],
|
||||||
|
// 8 bytes/element: nibble01=11, nibble2=307, nibble3=382.
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), X(1), Y(1), X(2),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(2), X(3), XY(7, 3), XY(6, 6)],
|
||||||
|
// 16 bytes/element is identical to R_X for a 2D single-sample image.
|
||||||
|
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(2), Y(2), XY(6, 3), XY(3, 6)],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly AddressBit[][] RbPlus64KStandard =
|
||||||
|
[
|
||||||
|
// GFX10_SW_64K_S_RBPLUS_PATINFO, 1 byte/element.
|
||||||
|
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||||
|
Y(4), X(4), Y(5), X(5), Y(6), X(6), Y(7), X(7)],
|
||||||
|
// 2 bytes/element.
|
||||||
|
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||||
|
Y(3), X(4), Y(4), X(5), Y(5), X(6), Y(6), X(7)],
|
||||||
|
// 4 bytes/element.
|
||||||
|
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
|
||||||
|
Y(3), X(3), Y(4), X(4), Y(5), X(5), Y(6), X(6)],
|
||||||
|
// 8 bytes/element (also BC1/BC4 compressed blocks).
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
|
||||||
|
Y(2), X(3), Y(3), X(4), Y(4), X(5), Y(5), X(6)],
|
||||||
|
// 16 bytes/element (also 16-byte BC compressed blocks).
|
||||||
|
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
|
||||||
|
Y(2), X(2), Y(3), X(3), Y(4), X(4), Y(5), X(5)],
|
||||||
|
];
|
||||||
|
|
||||||
|
// GFX10 4K_S has a separate 12-bit micro-tile equation. It is not the
|
||||||
|
// generic x/y interleave used by the 64K standard block; using that larger
|
||||||
|
// equation leaves a regular grid in linearized atlases.
|
||||||
|
private static readonly AddressBit[][] Standard4K =
|
||||||
|
[
|
||||||
|
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||||
|
Y(4), X(4), Y(5), X(5)],
|
||||||
|
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||||
|
Y(3), X(4), Y(4), X(5)],
|
||||||
|
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
|
||||||
|
Y(3), X(3), Y(4), X(4)],
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
|
||||||
|
Y(2), X(3), Y(3), X(4)],
|
||||||
|
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
|
||||||
|
Y(2), X(2), Y(3), X(3)],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly bool _enabled = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static readonly bool _disabled = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
|
||||||
|
"0",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static readonly HashSet<uint> _reportedModes = new();
|
||||||
|
|
||||||
|
public static bool Enabled => _enabled || !_disabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base S/Z modes and the Oberon RB+ 64 KiB Z_X/R_X modes for which this
|
||||||
|
/// implementation carries the exact AddrLib address equations. Other
|
||||||
|
/// bank/pipe-XOR variants remain opt-in.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsTrustedByDefault(uint swizzleMode) =>
|
||||||
|
// Exact base S/Z modes. D/R use different GFX10 swizzle equations and
|
||||||
|
// the T/X modes additionally apply pipe/bank XOR between blocks.
|
||||||
|
swizzleMode is 1 or 4 or 5 or 8 or 9 or 24 or 27;
|
||||||
|
|
||||||
|
// Detile a surface when it is verified-correct by default (trusted base mode),
|
||||||
|
// or when the user opts the approximate modes in with SHARPEMU_DETILE=1.
|
||||||
|
// SHARPEMU_DETILE=0 forces the old raw-upload behavior for everything.
|
||||||
|
private static bool ShouldDetile(uint swizzleMode) =>
|
||||||
|
swizzleMode != 0 && !_disabled && (_enabled || IsTrustedByDefault(swizzleMode));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when a surface with the given swizzle mode needs deswizzling.
|
||||||
|
/// Mode 0 is linear and never needs it.
|
||||||
|
/// </summary>
|
||||||
|
public static bool NeedsDetile(uint swizzleMode) => ShouldDetile(swizzleMode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the physical byte span occupied by a tiled mip. GNM allocates whole
|
||||||
|
/// swizzle blocks even when the logical image is much smaller than a block;
|
||||||
|
/// reading only the logical texel count truncates most source offsets during
|
||||||
|
/// detiling (a 64x64 BC1 mode-9 image is 2 KiB logically but occupies one
|
||||||
|
/// 64 KiB swizzle block).
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryGetTiledByteCount(
|
||||||
|
uint swizzleMode,
|
||||||
|
int elementsWide,
|
||||||
|
int elementsHigh,
|
||||||
|
int bytesPerElement,
|
||||||
|
out ulong byteCount)
|
||||||
|
{
|
||||||
|
byteCount = 0;
|
||||||
|
if (!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 blockElements = blockBytes >> bppLog2;
|
||||||
|
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||||
|
if (blockWidth == 0 || blockHeight == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocksWide = ((ulong)elementsWide + (ulong)blockWidth - 1) / (ulong)blockWidth;
|
||||||
|
var blocksHigh = ((ulong)elementsHigh + (ulong)blockHeight - 1) / (ulong)blockHeight;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byteCount = checked(blocksWide * blocksHigh * (ulong)blockBytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
byteCount = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||||
|
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||||
|
/// block-compressed formats, so callers pass the element grid dimensions
|
||||||
|
/// and the bytes per element. Returns false (leaving output untouched) for
|
||||||
|
/// unsupported swizzle modes so the caller can fall back to the raw bytes.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryDetile(
|
||||||
|
ReadOnlySpan<byte> tiled,
|
||||||
|
Span<byte> linear,
|
||||||
|
uint swizzleMode,
|
||||||
|
int elementsWide,
|
||||||
|
int elementsHigh,
|
||||||
|
int bytesPerElement)
|
||||||
|
{
|
||||||
|
if (!ShouldDetile(swizzleMode) || elementsWide <= 0 || elementsHigh <= 0 || bytesPerElement <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetSwizzleKind(swizzleMode, out var kind, out var blockBytes))
|
||||||
|
{
|
||||||
|
ReportUnsupported(swizzleMode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||||
|
if (bppLog2 < 0)
|
||||||
|
{
|
||||||
|
// Non-power-of-two element size (e.g. 24-bit) is not swizzled in a
|
||||||
|
// way this equation models.
|
||||||
|
ReportUnsupported(swizzleMode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block dimensions in elements: a swizzle block holds blockBytes bytes,
|
||||||
|
// laid out as a square-ish power-of-two element grid scaled by bpp.
|
||||||
|
var blockElements = blockBytes >> bppLog2;
|
||||||
|
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||||
|
if (blockWidth == 0 || blockHeight == 0)
|
||||||
|
{
|
||||||
|
ReportUnsupported(swizzleMode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||||
|
var requiredLinear = (long)elementsWide * elementsHigh * bytesPerElement;
|
||||||
|
if (linear.Length < requiredLinear)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompute the within-block element offset for each (x, y) inside a
|
||||||
|
// single block. The swizzle equation only depends on the in-block
|
||||||
|
// coordinates, so this table is reused for every block — turning the
|
||||||
|
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
|
||||||
|
// Detiling a 2048x2048 texture is millions of elements; without this the
|
||||||
|
// per-pixel math makes DETILE unusably slow during asset streaming.
|
||||||
|
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||||
|
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
||||||
|
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
||||||
|
{
|
||||||
|
for (var bx = 0; bx < blockWidth; bx++)
|
||||||
|
{
|
||||||
|
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
||||||
|
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
||||||
|
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = 0; y < elementsHigh; y++)
|
||||||
|
{
|
||||||
|
var blockY = y / blockHeight;
|
||||||
|
var inBlockY = y % blockHeight;
|
||||||
|
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||||
|
var tableRowBase = inBlockY * blockWidth;
|
||||||
|
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||||
|
for (var x = 0; x < elementsWide; x++)
|
||||||
|
{
|
||||||
|
var blockX = x / blockWidth;
|
||||||
|
var inBlockX = x % blockWidth;
|
||||||
|
|
||||||
|
var blockIndex = rowBlockBase + blockX;
|
||||||
|
var sourceByte = hasExactXorPattern
|
||||||
|
? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern)
|
||||||
|
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||||
|
(long)bytesPerElement;
|
||||||
|
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||||
|
if (sourceByte + bytesPerElement > tiled.Length ||
|
||||||
|
destByte + bytesPerElement > linear.Length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tiled.Slice((int)sourceByte, bytesPerElement)
|
||||||
|
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SwizzleKind
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
ZOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly AddressBit Zero = new(0, 0);
|
||||||
|
|
||||||
|
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||||
|
|
||||||
|
private static AddressBit Y(int bit) => new(0, 1u << bit);
|
||||||
|
|
||||||
|
private static AddressBit XY(int xBit, int yBit) => new(1u << xBit, 1u << yBit);
|
||||||
|
|
||||||
|
private static AddressBit XY(int xBit, int yBit1, int yBit2) =>
|
||||||
|
new(1u << xBit, (1u << yBit1) | (1u << yBit2));
|
||||||
|
|
||||||
|
private static bool TryGetExactXorPattern(
|
||||||
|
uint swizzleMode,
|
||||||
|
int bytesPerElementLog2,
|
||||||
|
out AddressBit[] pattern)
|
||||||
|
{
|
||||||
|
pattern = [];
|
||||||
|
if ((uint)bytesPerElementLog2 >= RbPlus64KRenderX.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = swizzleMode switch
|
||||||
|
{
|
||||||
|
5 => Standard4K[bytesPerElementLog2],
|
||||||
|
9 => RbPlus64KStandard[bytesPerElementLog2],
|
||||||
|
24 => RbPlus64KDepthX[bytesPerElementLog2],
|
||||||
|
27 => RbPlus64KRenderX[bytesPerElementLog2],
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
return pattern.Length != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
|
||||||
|
{
|
||||||
|
uint offset = 0;
|
||||||
|
for (var bit = 0; bit < pattern.Length; bit++)
|
||||||
|
{
|
||||||
|
var equation = pattern[bit];
|
||||||
|
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
|
||||||
|
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
|
||||||
|
offset |= (uint)parity << bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetSwizzleKind(uint swizzleMode, out SwizzleKind kind, out int blockBytes)
|
||||||
|
{
|
||||||
|
// GFX10 AddrLib SWIZZLE_MODE enumeration:
|
||||||
|
// 1-3 = 256 B S/D/R
|
||||||
|
// 4-7 = 4 KiB Z/S/D/R
|
||||||
|
// 8-11 = 64 KiB Z/S/D/R
|
||||||
|
// 16-19 = 64 KiB Z/S/D/R _T
|
||||||
|
// 20-23 = 4 KiB Z/S/D/R _X
|
||||||
|
// 24-27 = 64 KiB Z/S/D/R _X
|
||||||
|
// The pipe/bank XOR (_T/_X) affects which block a given tile lands in,
|
||||||
|
// but the *within-block* element order matches the base S/Z equation,
|
||||||
|
// which is what dominates visible correctness. We model the block
|
||||||
|
// interior and treat blocks as linear-ordered.
|
||||||
|
kind = SwizzleKind.Standard;
|
||||||
|
blockBytes = 0;
|
||||||
|
switch (swizzleMode)
|
||||||
|
{
|
||||||
|
case 4: case 8: case 16: case 20: case 24:
|
||||||
|
kind = SwizzleKind.ZOrder;
|
||||||
|
break;
|
||||||
|
case 1: case 2: case 3:
|
||||||
|
case 5: case 6: case 7:
|
||||||
|
case 9: case 10: case 11:
|
||||||
|
case 17: case 18: case 19:
|
||||||
|
case 21: case 22: case 23:
|
||||||
|
case 25: case 26: case 27:
|
||||||
|
kind = SwizzleKind.Standard;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockBytes = swizzleMode switch
|
||||||
|
{
|
||||||
|
>= 1 and <= 3 => 256,
|
||||||
|
>= 4 and <= 7 => 4096,
|
||||||
|
>= 20 and <= 23 => 4096,
|
||||||
|
_ => 65536,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard (S) 2D swizzle: within a block, the element order interleaves x
|
||||||
|
// and y bits with x taking the low bit — the AMD "standard" microtile.
|
||||||
|
private static long StandardSwizzleOffset(uint x, uint y, int blockWidth, int blockHeight)
|
||||||
|
{
|
||||||
|
var widthBits = BitLog2((uint)blockWidth);
|
||||||
|
var heightBits = BitLog2((uint)blockHeight);
|
||||||
|
long offset = 0;
|
||||||
|
var outBit = 0;
|
||||||
|
var xi = 0;
|
||||||
|
var yi = 0;
|
||||||
|
while (xi < widthBits || yi < heightBits)
|
||||||
|
{
|
||||||
|
if (xi < widthBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((x >> xi) & 1u) << outBit++;
|
||||||
|
xi++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yi < heightBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((y >> yi) & 1u) << outBit++;
|
||||||
|
yi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Z-order (Morton) swizzle: pure bit interleave, y taking the low bit.
|
||||||
|
private static long MortonInterleave(uint x, uint y, int blockWidth, int blockHeight)
|
||||||
|
{
|
||||||
|
var widthBits = BitLog2((uint)blockWidth);
|
||||||
|
var heightBits = BitLog2((uint)blockHeight);
|
||||||
|
long offset = 0;
|
||||||
|
var outBit = 0;
|
||||||
|
var xi = 0;
|
||||||
|
var yi = 0;
|
||||||
|
while (xi < widthBits || yi < heightBits)
|
||||||
|
{
|
||||||
|
if (yi < heightBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((y >> yi) & 1u) << outBit++;
|
||||||
|
yi++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xi < widthBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((x >> xi) & 1u) << outBit++;
|
||||||
|
xi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int Width, int Height) SquareBlockDimensions(int blockElements)
|
||||||
|
{
|
||||||
|
if (blockElements <= 0 || (blockElements & (blockElements - 1)) != 0)
|
||||||
|
{
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalBits = BitLog2((uint)blockElements);
|
||||||
|
// Split as evenly as possible with width >= height (x gets the extra bit).
|
||||||
|
var widthBits = (totalBits + 1) / 2;
|
||||||
|
var heightBits = totalBits - widthBits;
|
||||||
|
return (1 << widthBits, 1 << heightBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int BitLog2(uint value)
|
||||||
|
{
|
||||||
|
if (value == 0 || (value & (value - 1)) != 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Numerics.BitOperations.TrailingZeroCount(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportUnsupported(uint swizzleMode)
|
||||||
|
{
|
||||||
|
lock (_reportedModes)
|
||||||
|
{
|
||||||
|
if (!_reportedModes.Add(swizzleMode))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] GNM detile: unsupported swizzle mode {swizzleMode}; texture uploaded linear.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,17 @@
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
|
/// <summary>
|
||||||
// AgcExports re-checks every waiter against guest memory on each submit and resumes
|
/// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM
|
||||||
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
|
/// condition. AgcExports re-checks every waiter against guest memory on each
|
||||||
// DmaData packets or by direct CPU writes).
|
/// submit and resumes the ones whose condition became true (labels are advanced
|
||||||
|
/// by ReleaseMem / WriteData / DmaData packets, or by direct CPU writes).
|
||||||
|
///
|
||||||
|
/// This preserves cross-submit ordering: the work that follows a wait inside a
|
||||||
|
/// DCB is only queued once the awaited completion label is genuinely written,
|
||||||
|
/// instead of being force-satisfied at parse time and running ahead of the
|
||||||
|
/// compute/graphics work it depends on (which produced a black composite).
|
||||||
|
/// </summary>
|
||||||
internal static class GpuWaitRegistry
|
internal static class GpuWaitRegistry
|
||||||
{
|
{
|
||||||
public struct WaitingDcb
|
public struct WaitingDcb
|
||||||
@@ -19,13 +26,56 @@ internal static class GpuWaitRegistry
|
|||||||
public ulong ReferenceValue;
|
public ulong ReferenceValue;
|
||||||
public ulong Mask;
|
public ulong Mask;
|
||||||
public uint CompareFunction;
|
public uint CompareFunction;
|
||||||
|
public uint ControlValue;
|
||||||
public bool Is64Bit;
|
public bool Is64Bit;
|
||||||
|
public bool IsStandard;
|
||||||
|
public object? Memory;
|
||||||
|
public string? QueueName;
|
||||||
|
public ulong SubmissionId;
|
||||||
|
// Stopwatch timestamp captured at registration. Stale waiters remain
|
||||||
|
// registered; this only controls one-shot diagnostics.
|
||||||
|
public long RegisteredTicks;
|
||||||
|
public bool StaleReported;
|
||||||
public object? State;
|
public object? State;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||||
|
|
||||||
|
public static int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
foreach (var (_, list) in _waiters)
|
||||||
|
{
|
||||||
|
total += list.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CountForMemory(object memory)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
foreach (var (_, list) in _waiters)
|
||||||
|
{
|
||||||
|
foreach (var waiter in list)
|
||||||
|
{
|
||||||
|
total += ReferenceEquals(waiter.Memory, memory) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void Register(ulong address, WaitingDcb waiter)
|
public static void Register(ulong address, WaitingDcb waiter)
|
||||||
{
|
{
|
||||||
waiter.WaitAddress = address;
|
waiter.WaitAddress = address;
|
||||||
@@ -41,9 +91,15 @@ internal static class GpuWaitRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
|
/// <summary>
|
||||||
// returns null when the memory is unreadable; such waiters are kept registered.
|
/// Re-evaluates every registered waiter. <paramref name="readValue"/>
|
||||||
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
|
/// receives (address, is64Bit) and returns null when the memory is
|
||||||
|
/// unreadable; such waiters are kept registered. Returns the waiters whose
|
||||||
|
/// condition is now satisfied (removed from the registry), or null.
|
||||||
|
/// </summary>
|
||||||
|
public static List<WaitingDcb>? CollectSatisfied(
|
||||||
|
object memory,
|
||||||
|
Func<ulong, bool, ulong?> readValue)
|
||||||
{
|
{
|
||||||
List<WaitingDcb>? woken = null;
|
List<WaitingDcb>? woken = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -53,6 +109,11 @@ internal static class GpuWaitRegistry
|
|||||||
{
|
{
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
for (var i = list.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
|
if (!ReferenceEquals(list[i].Memory, memory))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var value = readValue(address, list[i].Is64Bit);
|
var value = readValue(address, list[i].Is64Bit);
|
||||||
if (value is null || !Compare(list[i], value.Value))
|
if (value is null || !Compare(list[i], value.Value))
|
||||||
{
|
{
|
||||||
@@ -71,7 +132,7 @@ internal static class GpuWaitRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emptied != null)
|
if (emptied is not null)
|
||||||
{
|
{
|
||||||
foreach (var address in emptied)
|
foreach (var address in emptied)
|
||||||
{
|
{
|
||||||
@@ -83,20 +144,122 @@ internal static class GpuWaitRegistry
|
|||||||
return woken;
|
return woken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns waiters that have remained unsatisfied longer than
|
||||||
|
/// <paramref name="maxAgeTicks"/> exactly once, without removing them or
|
||||||
|
/// changing their labels. Missing GPU work must fail closed: advancing a
|
||||||
|
/// command buffer without its real producer corrupts cross-queue ordering.
|
||||||
|
/// </summary>
|
||||||
|
public static List<WaitingDcb>? CollectUnreportedStale(
|
||||||
|
object memory,
|
||||||
|
long nowTicks,
|
||||||
|
long maxAgeTicks)
|
||||||
|
{
|
||||||
|
List<WaitingDcb>? stale = null;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var (_, list) in _waiters)
|
||||||
|
{
|
||||||
|
for (var i = list.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var waiter = list[i];
|
||||||
|
if (!ReferenceEquals(waiter.Memory, memory) ||
|
||||||
|
waiter.StaleReported ||
|
||||||
|
nowTicks - waiter.RegisteredTicks < maxAgeTicks)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stale ??= new List<WaitingDcb>();
|
||||||
|
waiter.StaleReported = true;
|
||||||
|
list[i] = waiter;
|
||||||
|
stale.Add(waiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns watched labels overlapped by a newly discovered producer. Used
|
||||||
|
/// only for diagnostics; producer completion still wakes through the
|
||||||
|
/// normal CollectSatisfied path after the ordered memory write executes.
|
||||||
|
/// </summary>
|
||||||
|
public static List<(ulong Address, int Count)> SnapshotInRange(
|
||||||
|
object memory,
|
||||||
|
ulong start,
|
||||||
|
ulong length)
|
||||||
|
{
|
||||||
|
var matches = new List<(ulong Address, int Count)>();
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = start > ulong.MaxValue - length ? ulong.MaxValue : start + length;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var (address, list) in _waiters)
|
||||||
|
{
|
||||||
|
var matchingCount = 0;
|
||||||
|
var any64Bit = false;
|
||||||
|
foreach (var waiter in list)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(waiter.Memory, memory))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingCount++;
|
||||||
|
any64Bit |= waiter.Is64Bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = any64Bit
|
||||||
|
? sizeof(ulong)
|
||||||
|
: sizeof(uint);
|
||||||
|
var waitEnd = address > ulong.MaxValue - (ulong)width
|
||||||
|
? ulong.MaxValue
|
||||||
|
: address + (ulong)width;
|
||||||
|
if (start < waitEnd && address < end)
|
||||||
|
{
|
||||||
|
matches.Add((address, matchingCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||||
{
|
{
|
||||||
var masked = value & waiter.Mask;
|
var masked = value & waiter.Mask;
|
||||||
|
var reference = waiter.ReferenceValue & waiter.Mask;
|
||||||
return waiter.CompareFunction switch
|
return waiter.CompareFunction switch
|
||||||
{
|
{
|
||||||
1 => masked < waiter.ReferenceValue,
|
0 => true,
|
||||||
2 => masked <= waiter.ReferenceValue,
|
1 => masked < reference,
|
||||||
3 => masked == waiter.ReferenceValue,
|
2 => masked <= reference,
|
||||||
4 => masked != waiter.ReferenceValue,
|
3 => masked == reference,
|
||||||
5 => masked >= waiter.ReferenceValue,
|
4 => masked != reference,
|
||||||
6 => masked > waiter.ReferenceValue,
|
5 => masked >= reference,
|
||||||
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
|
6 => masked > reference,
|
||||||
// satisfied keeps a malformed packet from suspending its DCB forever.
|
// 7 is reserved; treating it as satisfied keeps a malformed packet
|
||||||
|
// from suspending forever.
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_waiters.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,31 +7,11 @@ namespace SharpEmu.Libs.Ajm;
|
|||||||
|
|
||||||
public static class AjmExports
|
public static class AjmExports
|
||||||
{
|
{
|
||||||
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
|
|
||||||
private static int _nextContextId;
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "dl+4eHSzUu4",
|
Nid = "dl+4eHSzUu4",
|
||||||
ExportName = "sceAjmInitialize",
|
ExportName = "sceAjmInitialize",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAjm")]
|
LibraryName = "libSceAjm")]
|
||||||
public static int Initialize(CpuContext ctx)
|
public static int Initialize(CpuContext ctx) =>
|
||||||
{
|
SharpEmu.Libs.Audio.AjmExports.AjmInitialize(ctx);
|
||||||
var reserved = ctx[CpuRegister.Rdi];
|
|
||||||
var outContextAddress = ctx[CpuRegister.Rsi];
|
|
||||||
|
|
||||||
// AJM requires a zero reserved argument.
|
|
||||||
if (reserved != 0 || outContextAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
|
||||||
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public static class AmprExports
|
|||||||
public ulong Buffer;
|
public ulong Buffer;
|
||||||
public ulong Size;
|
public ulong Size;
|
||||||
public ulong WriteOffset;
|
public ulong WriteOffset;
|
||||||
|
public ulong CommandCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CachedHostFile
|
private sealed class CachedHostFile
|
||||||
@@ -267,53 +268,19 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong bytesRead = 0;
|
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||||
|
|
||||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
|
||||||
// speculative reads and only consume the bytes on success paths.
|
|
||||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
|
||||||
{
|
{
|
||||||
if (destination != 0 && size > 0)
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
int chunkSize = (int)Math.Min(size, 4096);
|
|
||||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
|
||||||
zeros.Clear();
|
|
||||||
while (bytesRead < size)
|
|
||||||
{
|
|
||||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
|
||||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
bytesRead += (ulong)currentChunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset -1 means "continue after the previous read of this file id".
|
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||||
if (fileOffset == unchecked((ulong)(long)-1))
|
|
||||||
{
|
|
||||||
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
|
||||||
}
|
|
||||||
else if (fileOffset > long.MaxValue)
|
|
||||||
{
|
|
||||||
fileOffset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
|
||||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
|
||||||
|
|
||||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -418,6 +385,35 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gzndltBEzWc",
|
||||||
|
ExportName = "sceAmprCommandBufferGetNumCommands",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int CommandBufferGetNumCommands(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
|
if (commandBuffer == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong commandCount;
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
commandCount = state.CommandCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "get_num_commands", commandBuffer, commandCount, 0);
|
||||||
|
ctx[CpuRegister.Rax] = commandCount;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "H896Pt-yB4I",
|
Nid = "H896Pt-yB4I",
|
||||||
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
|
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
|
||||||
@@ -533,7 +529,7 @@ public static class AmprExports
|
|||||||
var offset = 0UL;
|
var offset = 0UL;
|
||||||
while (offset < writeOffset)
|
while (offset < writeOffset)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -649,6 +645,7 @@ public static class AmprExports
|
|||||||
state.Buffer = buffer;
|
state.Buffer = buffer;
|
||||||
state.Size = size;
|
state.Size = size;
|
||||||
state.WriteOffset = writeOffset;
|
state.WriteOffset = writeOffset;
|
||||||
|
state.CommandCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,6 +678,7 @@ public static class AmprExports
|
|||||||
state.Buffer = buffer;
|
state.Buffer = buffer;
|
||||||
state.Size = size;
|
state.Size = size;
|
||||||
state.WriteOffset = 0;
|
state.WriteOffset = 0;
|
||||||
|
state.CommandCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -911,6 +909,7 @@ public static class AmprExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.WriteOffset += recordSize;
|
state.WriteOffset += recordSize;
|
||||||
|
state.CommandCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -963,6 +962,19 @@ public static class AmprExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||||
{
|
{
|
||||||
if (!_traceAmpr)
|
if (!_traceAmpr)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class AjmExports
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
|
||||||
|
private static int _nextContextId;
|
||||||
|
public static int AjmInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var reserved = ctx[CpuRegister.Rdi];
|
||||||
|
var outputAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (reserved != 0 || outputAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x806A0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(value, contextId);
|
||||||
|
if (!ctx.Memory.TryWrite(outputAddress, value))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x806A0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contexts[contextId] = 0;
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MHur6qCsUus",
|
||||||
|
ExportName = "sceAjmFinalize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmFinalize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Q3dyFuwGn64",
|
||||||
|
ExportName = "sceAjmModuleRegister",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmModuleRegister(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var reserved = ctx[CpuRegister.Rdx];
|
||||||
|
if (reserved != 0 || !Contexts.ContainsKey(contextId))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x806A0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ajm.module_register context={contextId} codec={codecType} reserved={reserved}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Wi7DtlLV+KI",
|
||||||
|
ExportName = "sceAjmModuleUnregister",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmModuleUnregister(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MmpF1XsQiHw",
|
||||||
|
ExportName = "sceAjmBatchInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmBatchInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// The caller owns and initializes the batch storage. This API resets
|
||||||
|
// its submission cursor on hardware; FMOD does not consume a return
|
||||||
|
// value or an additional output object here.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,22 +3,70 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
public static class AudioOut2Exports
|
public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
// FMOD's PS5 backend allocates this ABI structure as four 16-byte lanes.
|
||||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
// Clearing 0x80 bytes here overwrote the caller's stack canary immediately
|
||||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
// following the 0x40-byte parameter block.
|
||||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
private const int AudioOut2ContextParamSize = 0x40;
|
||||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
|
||||||
// populate.
|
|
||||||
private const int AudioOut2ContextParamSize = 0x30;
|
|
||||||
private const int AudioOut2ContextMemorySize = 0x10000;
|
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||||
|
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||||
private static long _nextContextHandle = 1;
|
private static long _nextContextHandle = 1;
|
||||||
private static long _nextUserHandle = 1;
|
private static long _nextUserHandle = 1;
|
||||||
private static int _nextPortId;
|
private static int _nextPortId;
|
||||||
|
private static long _pushTraceCount;
|
||||||
|
|
||||||
|
// Per-context audio parameters captured at ContextCreate so ContextAdvance
|
||||||
|
// can pace to the real playback cadence (grain samples at the sample rate).
|
||||||
|
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
|
||||||
|
|
||||||
|
private sealed class ContextState
|
||||||
|
{
|
||||||
|
private readonly object _paceGate = new();
|
||||||
|
private long _nextAdvanceTimestamp;
|
||||||
|
|
||||||
|
public ContextState(uint frequency, uint channels, uint grainSamples)
|
||||||
|
{
|
||||||
|
Frequency = frequency == 0 ? 48000 : frequency;
|
||||||
|
Channels = channels == 0 ? 2 : channels;
|
||||||
|
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint Frequency { get; }
|
||||||
|
public uint Channels { get; }
|
||||||
|
public uint GrainSamples { get; }
|
||||||
|
|
||||||
|
// Blocks the advancing thread until one grain worth of wall-clock time
|
||||||
|
// has elapsed since the previous advance, matching hardware timing so
|
||||||
|
// audio-gated titles neither spin nor drift ahead.
|
||||||
|
public void PaceAdvance()
|
||||||
|
{
|
||||||
|
long delay;
|
||||||
|
lock (_paceGate)
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_nextAdvanceTimestamp < now)
|
||||||
|
{
|
||||||
|
_nextAdvanceTimestamp = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = _nextAdvanceTimestamp - now;
|
||||||
|
_nextAdvanceTimestamp += checked(
|
||||||
|
(long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "g2tViFIohHE",
|
Nid = "g2tViFIohHE",
|
||||||
@@ -41,7 +89,7 @@ public static class AudioOut2Exports
|
|||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
if (paramAddress == 0)
|
if (paramAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
@@ -52,8 +100,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(paramAddress, param)
|
return ctx.Memory.TryWrite(paramAddress, param)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -64,15 +112,22 @@ public static class AudioOut2Exports
|
|||||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||||
? ctx.SetReturn(0)
|
memoryInfo.Clear();
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -88,13 +143,34 @@ public static class AudioOut2Exports
|
|||||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read channels/frequency/grain from the reset-param blob so the
|
||||||
|
// context can pace advances to the real audio cadence.
|
||||||
|
uint channels = 2;
|
||||||
|
uint frequency = 48000;
|
||||||
|
uint grain = 256;
|
||||||
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
|
if (ctx.Memory.TryRead(paramAddress, param))
|
||||||
|
{
|
||||||
|
var pc = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
||||||
|
var pf = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
||||||
|
var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
|
||||||
|
if (pc is > 0 and <= 8) channels = pc;
|
||||||
|
if (pf is >= 8000 and <= 192000) frequency = pf;
|
||||||
|
// Values below one cache line are flags/counts in observed PS5
|
||||||
|
// callers, not audio grains. Keep the hardware-sized default.
|
||||||
|
if (pg is >= 64 and <= 0x4000) grain = pg;
|
||||||
|
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
Contexts[handle] = new ContextState(frequency, channels, grain);
|
||||||
? ctx.SetReturn(0)
|
TraceAudioOut2($"context-create handle=0x{handle:X} frequency={frequency} channels={channels} grain={grain} memory=0x{memoryAddress:X} size=0x{memorySize:X}");
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -102,7 +178,77 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2ContextDestroy",
|
ExportName = "sceAudioOut2ContextDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DxGyV8dtOR8",
|
||||||
|
ExportName = "sceAudioOut2ContextBedWrite",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "aII9h5nli9U",
|
||||||
|
ExportName = "sceAudioOut2ContextPush",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextPush(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var traceCount = Interlocked.Increment(ref _pushTraceCount);
|
||||||
|
if (traceCount <= 16)
|
||||||
|
{
|
||||||
|
TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Contexts.TryGetValue(handle, out var context))
|
||||||
|
{
|
||||||
|
// FMOD's PS5 output path uses ContextPush as the submission clock
|
||||||
|
// and does not call ContextAdvance. Pace pushes to one hardware
|
||||||
|
// grain so the feeder cannot outrun playback and starve the game.
|
||||||
|
context.PaceAdvance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PE2zHMqLSHs",
|
||||||
|
ExportName = "sceAudioOut2ContextAdvance",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextAdvance(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Advancing renders one grain of audio on hardware; pace it to the same
|
||||||
|
// wall-clock cadence so the guest audio thread runs at the right speed.
|
||||||
|
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context))
|
||||||
|
{
|
||||||
|
context.PaceAdvance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "R7d0F1g2qsU",
|
||||||
|
ExportName = "sceAudioOut2ContextGetQueueLevel",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// The advance path paces synchronously, so the queue is always drained.
|
||||||
|
var levelAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (levelAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt64(ctx, levelAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JK2wamZPzwM",
|
Nid = "JK2wamZPzwM",
|
||||||
@@ -117,16 +263,23 @@ public static class AudioOut2Exports
|
|||||||
var contextAddress = ctx[CpuRegister.Rcx];
|
var contextAddress = ctx[CpuRegister.Rcx];
|
||||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8XTArSPyWHk",
|
||||||
|
ExportName = "sceAudioOut2PortSetAttributes",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gatEUKG+Ea4",
|
Nid = "gatEUKG+Ea4",
|
||||||
ExportName = "sceAudioOut2PortGetState",
|
ExportName = "sceAudioOut2PortGetState",
|
||||||
@@ -138,7 +291,7 @@ public static class AudioOut2Exports
|
|||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle == 0 || stateAddress == 0)
|
if (handle == 0 || stateAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = (int)((handle >> 16) & 0xFF);
|
var type = (int)((handle >> 16) & 0xFF);
|
||||||
@@ -151,8 +304,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(stateAddress, state)
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -165,7 +318,7 @@ public static class AudioOut2Exports
|
|||||||
var infoAddress = ctx[CpuRegister.Rdi];
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
if (infoAddress == 0)
|
if (infoAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[0x40];
|
Span<byte> info = stackalloc byte[0x40];
|
||||||
@@ -175,8 +328,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(infoAddress, info)
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -184,14 +337,14 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2PortDestroy",
|
ExportName = "sceAudioOut2PortDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "IaZXJ9M79uo",
|
Nid = "IaZXJ9M79uo",
|
||||||
ExportName = "sceAudioOut2UserDestroy",
|
ExportName = "sceAudioOut2UserDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "xywYcRB7nbQ",
|
Nid = "xywYcRB7nbQ",
|
||||||
@@ -202,14 +355,36 @@ public static class AudioOut2Exports
|
|||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
|
||||||
|
outUserAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceAudioOut2(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT2"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public static class AudioOutExports
|
|||||||
public int BytesPerSample { get; }
|
public int BytesPerSample { get; }
|
||||||
public bool IsFloat { get; }
|
public bool IsFloat { get; }
|
||||||
public IHostAudioStream? Backend { get; }
|
public IHostAudioStream? Backend { get; }
|
||||||
|
public volatile float Volume = 1.0f;
|
||||||
public int BufferByteLength =>
|
public int BufferByteLength =>
|
||||||
checked((int)BufferLength * Channels * BytesPerSample);
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
|
|
||||||
@@ -198,7 +199,8 @@ public static class AudioOutExports
|
|||||||
checked((int)port.BufferLength),
|
checked((int)port.BufferLength),
|
||||||
port.Channels,
|
port.Channels,
|
||||||
port.BytesPerSample,
|
port.BytesPerSample,
|
||||||
port.IsFloat);
|
port.IsFloat,
|
||||||
|
port.Volume);
|
||||||
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
@@ -225,10 +227,43 @@ public static class AudioOutExports
|
|||||||
public static int AudioOutSetVolume(CpuContext ctx)
|
public static int AudioOutSetVolume(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return ctx.SetReturn(
|
var channelFlags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
Ports.ContainsKey(handle)
|
var volumeArrayAddress = ctx[CpuRegister.Rdx];
|
||||||
? 0
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int unityVolume = 32768;
|
||||||
|
var maxVolume = 0;
|
||||||
|
var found = false;
|
||||||
|
if (volumeArrayAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> raw = stackalloc byte[sizeof(int)];
|
||||||
|
for (var channel = 0; channel < 8; channel++)
|
||||||
|
{
|
||||||
|
if ((channelFlags & (1u << channel)) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryRead(volumeArrayAddress + (ulong)(channel * sizeof(int)), raw))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(raw);
|
||||||
|
maxVolume = Math.Max(maxVolume, value);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
port.Volume = Math.Clamp(maxVolume / (float)unityVolume, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ShutdownAllPorts()
|
public static void ShutdownAllPorts()
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ internal static class AudioPcmConversion
|
|||||||
int frames,
|
int frames,
|
||||||
int channels,
|
int channels,
|
||||||
int bytesPerSample,
|
int bytesPerSample,
|
||||||
bool isFloat)
|
bool isFloat,
|
||||||
|
float volume)
|
||||||
{
|
{
|
||||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||||
for (var frame = 0; frame < frames; frame++)
|
for (var frame = 0; frame < frames; frame++)
|
||||||
@@ -31,6 +32,8 @@ internal static class AudioPcmConversion
|
|||||||
var right = channels == 1
|
var right = channels == 1
|
||||||
? left
|
? left
|
||||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||||
|
left = ApplyVolume(left, volume);
|
||||||
|
right = ApplyVolume(right, volume);
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||||
}
|
}
|
||||||
@@ -49,7 +52,24 @@ internal static class AudioPcmConversion
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
return ConvertFloatSample(BitConverter.Int32BitsToSingle(bits));
|
||||||
return checked((short)MathF.Round(value * short.MaxValue));
|
}
|
||||||
|
|
||||||
|
private static short ConvertFloatSample(float value)
|
||||||
|
{
|
||||||
|
if (float.IsNaN(value))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Math.Clamp(value, -1.0f, 1.0f);
|
||||||
|
var scale = value < 0.0f ? 32768.0f : short.MaxValue;
|
||||||
|
return checked((short)MathF.Round(value * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static short ApplyVolume(short sample, float volume)
|
||||||
|
{
|
||||||
|
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
|
||||||
|
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class FmodCompatExports
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<ulong, int> SetOutputCalls = new();
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uPLTdl3psGk",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libfmod")]
|
||||||
|
public static int FmodSystemSetOutput(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var system = ctx[CpuRegister.Rdi];
|
||||||
|
var output = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var callCount = SetOutputCalls.AddOrUpdate(system, 1, static (_, count) => count + 1);
|
||||||
|
var resetPrematureInit = callCount <= 2;
|
||||||
|
if (resetPrematureInit && system != 0)
|
||||||
|
{
|
||||||
|
Span<byte> zeroByte = stackalloc byte[1];
|
||||||
|
Span<byte> outputBytes = stackalloc byte[sizeof(int)];
|
||||||
|
Span<byte> zeroInt = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(outputBytes, output);
|
||||||
|
_ = ctx.Memory.TryWrite(system + 0x08, zeroByte);
|
||||||
|
_ = ctx.Memory.TryWrite(system + 0x116D0, outputBytes);
|
||||||
|
_ = ctx.Memory.TryWrite(system + 0x116D4, zeroInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FMOD"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fmod.system_set_output system=0x{system:X16} output={output} call={callCount} reset_premature_init={resetPrematureInit} -> 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PS5 FMOD object arrives with its initialized byte set before Unity has
|
||||||
|
// applied the startup output configuration. Preserve those settings while
|
||||||
|
// clearing the premature marker so Studio can execute the real core init.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,403 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Bink;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional host-side Bink 2 bridge for games that ship a static Bink player.
|
||||||
|
///
|
||||||
|
/// The game in that case never imports libSceVideodec, so an HLE video-decoder
|
||||||
|
/// export cannot see its movie frames. Kernel file opens identify the active
|
||||||
|
/// .bk2 file and the presenter requests BGRA frames from a tiny native adapter.
|
||||||
|
/// The adapter is deliberately a separate, user-supplied library: Bink 2 is a
|
||||||
|
/// proprietary SDK and SharpEmu must neither bundle it nor depend on its ABI.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Bink2MovieBridge
|
||||||
|
{
|
||||||
|
private const uint MaxDimension = 16384;
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static NativeAdapter? _adapter;
|
||||||
|
private static string? _activePath;
|
||||||
|
private static IntPtr _activeMovie;
|
||||||
|
private static Bink2MovieInfo _activeInfo;
|
||||||
|
private static byte[]? _frameBuffer;
|
||||||
|
private static bool _usingDummyMovie;
|
||||||
|
private static bool _loadAttempted;
|
||||||
|
private static bool _availabilityReported;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when the guest should receive a normal "file not found"
|
||||||
|
/// result for a Bink movie. This is the safe default without a decoder:
|
||||||
|
/// games that treat movies as optional fall through to their next state
|
||||||
|
/// rather than submitting an empty Bink GPU texture forever.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
||||||
|
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
ResolveMode() == MovieMode.Skip;
|
||||||
|
|
||||||
|
internal static void ObserveGuestMovie(string hostPath)
|
||||||
|
{
|
||||||
|
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!File.Exists(hostPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ResolveMode() == MovieMode.Dummy)
|
||||||
|
{
|
||||||
|
AttachDummyMovieLocked(hostPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var adapter = GetAdapterLocked();
|
||||||
|
if (adapter is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseActiveLocked();
|
||||||
|
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||||
|
Path.GetFileName(hostPath) + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValid(info))
|
||||||
|
{
|
||||||
|
adapter.Close(movie);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||||
|
Path.GetFileName(hostPath) + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePath = hostPath;
|
||||||
|
_activeMovie = movie;
|
||||||
|
_activeInfo = info;
|
||||||
|
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
|
info.Width + "x" + info.Height + " @ " +
|
||||||
|
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryDecodeNextFrame(
|
||||||
|
out byte[] pixels,
|
||||||
|
out uint width,
|
||||||
|
out uint height)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
pixels = [];
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
|
||||||
|
{
|
||||||
|
if (_usingDummyMovie && _frameBuffer is not null)
|
||||||
|
{
|
||||||
|
pixels = _frameBuffer;
|
||||||
|
width = _activeInfo.Width;
|
||||||
|
height = _activeInfo.Height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (byte* destination = _frameBuffer)
|
||||||
|
{
|
||||||
|
if (!_adapter.DecodeNextBgra(
|
||||||
|
_activeMovie,
|
||||||
|
(IntPtr)destination,
|
||||||
|
_activeInfo.Width * 4,
|
||||||
|
(uint)_frameBuffer.Length))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels = _frameBuffer;
|
||||||
|
width = _activeInfo.Width;
|
||||||
|
height = _activeInfo.Height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValid(Bink2MovieInfo info) =>
|
||||||
|
info.Width > 0 && info.Height > 0 &&
|
||||||
|
info.Width <= MaxDimension && info.Height <= MaxDimension &&
|
||||||
|
(ulong)info.Width * info.Height * 4 <= int.MaxValue;
|
||||||
|
|
||||||
|
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||||
|
checked((int)((ulong)info.Width * info.Height * 4));
|
||||||
|
|
||||||
|
private static MovieMode ResolveMode()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||||
|
if (string.Equals(configured, "dummy", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return MovieMode.Dummy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(configured, "native", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return MovieMode.Native;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(configured, "skip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return MovieMode.Skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no SDK adapter present, returning "not found" makes optional
|
||||||
|
// cinematics advance. Supplying either an explicit path or the normal
|
||||||
|
// side-by-side adapter enables native playback automatically.
|
||||||
|
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||||
|
EnumerateAdapterCandidates().Any(File.Exists))
|
||||||
|
{
|
||||||
|
return MovieMode.Native;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MovieMode.Skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AttachDummyMovieLocked(string hostPath)
|
||||||
|
{
|
||||||
|
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Bink dummy could not read movie header '" +
|
||||||
|
Path.GetFileName(hostPath) + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseActiveLocked();
|
||||||
|
_activePath = hostPath;
|
||||||
|
_activeInfo = info;
|
||||||
|
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||||
|
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||||
|
_usingDummyMovie = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
|
info.Width + "x" + info.Height + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
Span<byte> header = stackalloc byte[32];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(path);
|
||||||
|
if (stream.Read(header) != header.Length ||
|
||||||
|
!header[..4].SequenceEqual("KB2j"u8))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new Bink2MovieInfo(
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||||
|
1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FillDummyFrame(byte[] pixels, uint width, uint height)
|
||||||
|
{
|
||||||
|
for (var y = 0u; y < height; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0u; x < width; x++)
|
||||||
|
{
|
||||||
|
var offset = checked((int)(((ulong)y * width + x) * 4));
|
||||||
|
var band = ((x / 96) + (y / 96)) & 1;
|
||||||
|
pixels[offset] = band == 0 ? (byte)0x28 : (byte)0x18;
|
||||||
|
pixels[offset + 1] = band == 0 ? (byte)0x18 : (byte)0x28;
|
||||||
|
pixels[offset + 2] = 0x10;
|
||||||
|
pixels[offset + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeAdapter? GetAdapterLocked()
|
||||||
|
{
|
||||||
|
if (_loadAttempted)
|
||||||
|
{
|
||||||
|
return _adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadAttempted = true;
|
||||||
|
foreach (var candidate in EnumerateAdapterCandidates())
|
||||||
|
{
|
||||||
|
if (!NativeLibrary.TryLoad(candidate, out var library))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeAdapter.TryCreate(library, out var adapter))
|
||||||
|
{
|
||||||
|
_adapter = adapter;
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.Free(library);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_availabilityReported)
|
||||||
|
{
|
||||||
|
_availabilityReported = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EnumerateAdapterCandidates()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
|
||||||
|
if (!string.IsNullOrWhiteSpace(configured))
|
||||||
|
{
|
||||||
|
yield return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseDirectory = AppContext.BaseDirectory;
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CloseActiveLocked()
|
||||||
|
{
|
||||||
|
if (_activeMovie != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
_adapter?.Close(_activeMovie);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePath = null;
|
||||||
|
_activeMovie = IntPtr.Zero;
|
||||||
|
_activeInfo = default;
|
||||||
|
_frameBuffer = null;
|
||||||
|
_usingDummyMovie = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private readonly struct Bink2MovieInfo
|
||||||
|
{
|
||||||
|
public readonly uint Width;
|
||||||
|
public readonly uint Height;
|
||||||
|
public readonly uint FramesPerSecondNumerator;
|
||||||
|
public readonly uint FramesPerSecondDenominator;
|
||||||
|
|
||||||
|
internal Bink2MovieInfo(
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint framesPerSecondNumerator,
|
||||||
|
uint framesPerSecondDenominator)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||||
|
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MovieMode
|
||||||
|
{
|
||||||
|
Skip,
|
||||||
|
Dummy,
|
||||||
|
Native,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NativeAdapter
|
||||||
|
{
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void CloseDelegate(IntPtr movie);
|
||||||
|
|
||||||
|
private readonly OpenUtf8Delegate _openUtf8;
|
||||||
|
private readonly DecodeNextBgraDelegate _decodeNextBgra;
|
||||||
|
private readonly CloseDelegate _close;
|
||||||
|
|
||||||
|
private NativeAdapter(
|
||||||
|
OpenUtf8Delegate openUtf8,
|
||||||
|
DecodeNextBgraDelegate decodeNextBgra,
|
||||||
|
CloseDelegate close)
|
||||||
|
{
|
||||||
|
_openUtf8 = openUtf8;
|
||||||
|
_decodeNextBgra = decodeNextBgra;
|
||||||
|
_close = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
||||||
|
{
|
||||||
|
adapter = null;
|
||||||
|
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
||||||
|
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
||||||
|
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = new NativeAdapter(
|
||||||
|
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
||||||
|
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
||||||
|
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
||||||
|
{
|
||||||
|
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeCoTaskMem(utf8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
||||||
|
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
||||||
|
|
||||||
|
internal void Close(IntPtr movie) => _close(movie);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Codec;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// libSceVideodec / libSceAudiodec handle management. Actual H.264/HEVC and
|
||||||
|
/// AAC/AT9 decoding requires an external codec, which is out of scope; these
|
||||||
|
/// exports keep the decoder lifecycle resolvable (create/decode/flush/delete)
|
||||||
|
/// and report "no output produced" so guests advance instead of failing on
|
||||||
|
/// unresolved imports.
|
||||||
|
/// </summary>
|
||||||
|
public static class CodecExports
|
||||||
|
{
|
||||||
|
private const int Ok = 0;
|
||||||
|
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||||
|
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> AudioDecoders = new();
|
||||||
|
private static long _nextHandle = 1;
|
||||||
|
|
||||||
|
// ---- Video decoder ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "qkgRiwHyheU", ExportName = "sceVideodecCreateDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecCreateDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
||||||
|
VideoDecoders[handle] = 1;
|
||||||
|
return TryWriteHandle(ctx, outHandleAddress, handle) ? SetReturn(ctx, Ok) : SetReturn(ctx, VideodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "q0W5GJMovMs", ExportName = "sceVideodecDecode",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecDecode(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// No decoder is present: report success with no picture produced.
|
||||||
|
return SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "jeigLlKdp5I", ExportName = "sceVideodecFlush",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecFlush(CpuContext ctx) =>
|
||||||
|
SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "U0kpGF1cl90", ExportName = "sceVideodecDeleteDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecDeleteDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
VideoDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Audio decoder ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecCreateDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
||||||
|
AudioDecoders[handle] = 1;
|
||||||
|
// sceAudiodec returns the handle directly (>= 0) or a negative error.
|
||||||
|
ctx[CpuRegister.Rax] = handle;
|
||||||
|
return unchecked((int)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecDecode(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// No decoder present: report success with zero output samples so the
|
||||||
|
// caller treats the frame as silent rather than erroring.
|
||||||
|
return SetReturn(ctx, AudioDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : AudiodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecDeleteDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
AudioDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
|
||||||
|
ctx.TryWriteUInt64(address, handle);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// libSceErrorDialog. There is no host error dialog, so an opened dialog
|
||||||
|
/// immediately reports finished — this keeps guests that block on the dialog
|
||||||
|
/// status (a common fatal-error path) from spinning forever.
|
||||||
|
/// </summary>
|
||||||
|
public static class ErrorDialogExports
|
||||||
|
{
|
||||||
|
private const int AlreadyInitialized = unchecked((int)0x80ED0001);
|
||||||
|
private const int NotInitialized = unchecked((int)0x80ED0002);
|
||||||
|
private const int ArgNull = unchecked((int)0x80ED0005);
|
||||||
|
|
||||||
|
private const int StatusNone = 0;
|
||||||
|
private const int StatusInitialized = 1;
|
||||||
|
private const int StatusFinished = 3;
|
||||||
|
|
||||||
|
private static int _initialized;
|
||||||
|
private static int _status;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "I88KChlynSs",
|
||||||
|
ExportName = "sceErrorDialogInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = Interlocked.Exchange(ref _initialized, 1) == 0 ? 0 : AlreadyInitialized;
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, StatusInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "M2ZF-ClLhgY",
|
||||||
|
ExportName = "sceErrorDialogOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, ArgNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Volatile.Read(ref _initialized) == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
Volatile.Write(ref _status, StatusFinished);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "t2FvHRXzgqk",
|
||||||
|
ExportName = "sceErrorDialogGetStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WWiGuh9XfgQ",
|
||||||
|
ExportName = "sceErrorDialogUpdateStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ekXHb1kDBl0",
|
||||||
|
ExportName = "sceErrorDialogClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, StatusFinished);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9XAxK2PMwk8",
|
||||||
|
ExportName = "sceErrorDialogTerminate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogTerminate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, StatusNone);
|
||||||
|
Interlocked.Exchange(ref _initialized, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.ContentExport;
|
||||||
|
|
||||||
|
// No host media library exists to export captures into, so initialization
|
||||||
|
// reports success.
|
||||||
|
public static class ContentExportExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0GnN4QCgIfs",
|
||||||
|
ExportName = "sceContentExportInit2",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceContentExport")]
|
||||||
|
public static int ContentExportInit2(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ public static class FiberExports
|
|||||||
private const uint FiberStateIdle = 2;
|
private const uint FiberStateIdle = 2;
|
||||||
private const uint FiberStateTerminated = 3;
|
private const uint FiberStateTerminated = 3;
|
||||||
private const uint FiberFlagContextSizeCheck = 0x10;
|
private const uint FiberFlagContextSizeCheck = 0x10;
|
||||||
|
private const uint FiberFlagSetFpuRegs = 0x100;
|
||||||
|
private const uint Firmware350BuildVersion = 0x03500000;
|
||||||
|
private const ushort InitialFpuControlWord = 0x037F;
|
||||||
|
private const uint InitialMxcsr = 0x9FC0;
|
||||||
|
|
||||||
private const int FiberErrorNull = unchecked((int)0x80590001);
|
private const int FiberErrorNull = unchecked((int)0x80590001);
|
||||||
private const int FiberErrorAlignment = unchecked((int)0x80590002);
|
private const int FiberErrorAlignment = unchecked((int)0x80590002);
|
||||||
@@ -45,13 +50,26 @@ public static class FiberExports
|
|||||||
|
|
||||||
private static int _contextSizeCheck;
|
private static int _contextSizeCheck;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static ulong _currentFiberAddress;
|
|
||||||
|
|
||||||
private static readonly object _fiberGate = new();
|
private static readonly object _fiberGate = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
|
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, FiberThreadState> _threadStates = new();
|
||||||
|
|
||||||
|
static FiberExports()
|
||||||
|
{
|
||||||
|
RunFiberSelfChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ResetRuntimeState()
|
||||||
|
{
|
||||||
|
lock (_fiberGate)
|
||||||
|
{
|
||||||
|
_continuations.Clear();
|
||||||
|
_stackRanges.Clear();
|
||||||
|
_threadStates.Clear();
|
||||||
|
Volatile.Write(ref _contextSizeCheck, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hVYD7Ou2pCQ",
|
Nid = "hVYD7Ou2pCQ",
|
||||||
@@ -61,6 +79,7 @@ public static class FiberExports
|
|||||||
public static int FiberInitialize(CpuContext ctx)
|
public static int FiberInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var optParam = ReadStackArg64(ctx, 0);
|
var optParam = ReadStackArg64(ctx, 0);
|
||||||
|
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||||
return FiberInitializeCore(
|
return FiberInitializeCore(
|
||||||
ctx,
|
ctx,
|
||||||
ctx[CpuRegister.Rdi],
|
ctx[CpuRegister.Rdi],
|
||||||
@@ -70,7 +89,8 @@ public static class FiberExports
|
|||||||
ctx[CpuRegister.R8],
|
ctx[CpuRegister.R8],
|
||||||
ctx[CpuRegister.R9],
|
ctx[CpuRegister.R9],
|
||||||
optParam,
|
optParam,
|
||||||
flags: 0);
|
flags: 0,
|
||||||
|
buildVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -82,6 +102,7 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
var optParam = ReadStackArg64(ctx, 0);
|
var optParam = ReadStackArg64(ctx, 0);
|
||||||
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
|
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||||
|
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 2));
|
||||||
return FiberInitializeCore(
|
return FiberInitializeCore(
|
||||||
ctx,
|
ctx,
|
||||||
ctx[CpuRegister.Rdi],
|
ctx[CpuRegister.Rdi],
|
||||||
@@ -91,7 +112,8 @@ public static class FiberExports
|
|||||||
ctx[CpuRegister.R8],
|
ctx[CpuRegister.R8],
|
||||||
ctx[CpuRegister.R9],
|
ctx[CpuRegister.R9],
|
||||||
optParam,
|
optParam,
|
||||||
flags);
|
flags,
|
||||||
|
buildVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -104,17 +126,17 @@ public static class FiberExports
|
|||||||
var optParam = ctx[CpuRegister.Rdi];
|
var optParam = ctx[CpuRegister.Rdi];
|
||||||
if (optParam == 0)
|
if (optParam == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((optParam & 7) != 0)
|
if ((optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorAlignment);
|
return SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -127,24 +149,23 @@ public static class FiberExports
|
|||||||
var fiber = ctx[CpuRegister.Rdi];
|
var fiber = ctx[CpuRegister.Rdi];
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != FiberStateIdle)
|
if (state != FiberStateIdle)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations.TryRemove(fiber, out _);
|
_continuations.TryRemove(fiber, out _);
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
|
||||||
_stackRanges.TryRemove(fiber, out _);
|
_stackRanges.TryRemove(fiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -229,74 +250,47 @@ public static class FiberExports
|
|||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnArgument = ctx[CpuRegister.Rdi];
|
var returnArgument = ctx[CpuRegister.Rdi];
|
||||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
ulong previousFiber;
|
|
||||||
lock (_fiberGate)
|
lock (_fiberGate)
|
||||||
{
|
{
|
||||||
_continuations[fiberAddress] = new FiberContinuation(
|
_continuations[fiberAddress] = new FiberContinuation(
|
||||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||||
argOnRunAddress);
|
argOnRunAddress);
|
||||||
|
|
||||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
var threadKey = GetThreadKey(ctx);
|
||||||
|
if (!_threadStates.TryGetValue(threadKey, out var threadState) ||
|
||||||
|
!TryWriteResumeArgument(ctx, threadState.RootContinuation, returnArgument))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousFiber = returnTarget.PreviousFiber;
|
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||||
if (previousFiber != 0)
|
|
||||||
{
|
{
|
||||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
|
||||||
{
|
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
|
||||||
return ctx.SetReturn(FiberErrorState);
|
|
||||||
}
|
|
||||||
|
|
||||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!returnTarget.ThreadContinuation.HasValue ||
|
|
||||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
|
||||||
{
|
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
|
||||||
return ctx.SetReturn(FiberErrorState);
|
|
||||||
}
|
|
||||||
|
|
||||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
transferTarget = threadState.RootContinuation.Context with { Rax = 0 };
|
||||||
{
|
_threadStates.TryRemove(threadKey, out _);
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentFiberAddress = previousFiber;
|
_ = GuestThreadExecution.EnterFiber(0);
|
||||||
_ = GuestThreadExecution.EnterFiber(previousFiber);
|
|
||||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
$"return-to-thread fiber=0x{fiberAddress:X16} " +
|
||||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -309,18 +303,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -334,35 +328,35 @@ public static class FiberExports
|
|||||||
var info = ctx[CpuRegister.Rsi];
|
var info = ctx[CpuRegister.Rsi];
|
||||||
if (info == 0)
|
if (info == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -376,22 +370,22 @@ public static class FiberExports
|
|||||||
var nameAddress = ctx[CpuRegister.Rsi];
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -403,12 +397,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (ctx[CpuRegister.Rdi] != 0)
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorState);
|
: SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -419,8 +413,8 @@ public static class FiberExports
|
|||||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorState);
|
: SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -433,17 +427,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
if (ResolveCurrentFiberAddress(ctx) == 0 ||
|
||||||
|
!_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
return TryWriteUInt64(ctx, outAddress, threadState.RootContinuation.Context.Rbp)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberInitializeCore(
|
private static int FiberInitializeCore(
|
||||||
@@ -455,69 +450,70 @@ public static class FiberExports
|
|||||||
ulong contextAddress,
|
ulong contextAddress,
|
||||||
ulong contextSize,
|
ulong contextSize,
|
||||||
ulong optParam,
|
ulong optParam,
|
||||||
uint flags)
|
uint flags,
|
||||||
|
uint buildVersion)
|
||||||
{
|
{
|
||||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fiber & 7) != 0 ||
|
if ((fiber & 7) != 0 ||
|
||||||
(contextAddress & 15) != 0 ||
|
(contextAddress & 15) != 0 ||
|
||||||
(optParam & 7) != 0)
|
(optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorAlignment);
|
return SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorRange);
|
return SetReturn(ctx, FiberErrorRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((contextSize & 15) != 0 ||
|
if ((contextSize & 15) != 0 ||
|
||||||
(contextAddress == 0 && contextSize != 0) ||
|
(contextAddress == 0 && contextSize != 0) ||
|
||||||
(contextAddress != 0 && contextSize == 0))
|
(contextAddress != 0 && contextSize == 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optParam != 0 &&
|
if (optParam != 0 &&
|
||||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
flags = ApplyInitializationFlags(
|
||||||
{
|
flags,
|
||||||
flags |= FiberFlagContextSizeCheck;
|
buildVersion,
|
||||||
}
|
Volatile.Read(ref _contextSizeCheck) != 0);
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextAddress != 0)
|
if (contextAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||||
@@ -531,8 +527,10 @@ public static class FiberExports
|
|||||||
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
TraceFiber(
|
||||||
return ctx.SetReturn(0);
|
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
|
||||||
|
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberRunCore(
|
private static int FiberRunCore(
|
||||||
@@ -547,12 +545,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||||
@@ -560,7 +558,7 @@ public static class FiberExports
|
|||||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||||
if (attachResult != 0)
|
if (attachResult != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(attachResult);
|
return SetReturn(ctx, attachResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,78 +566,96 @@ public static class FiberExports
|
|||||||
if ((isSwitch && previousFiber == 0) ||
|
if ((isSwitch && previousFiber == 0) ||
|
||||||
(!isSwitch && previousFiber != 0))
|
(!isSwitch && previousFiber != 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
if (previousFiber == fiber)
|
if (previousFiber == fiber)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
var resumed = false;
|
var resumed = false;
|
||||||
lock (_fiberGate)
|
lock (_fiberGate)
|
||||||
{
|
{
|
||||||
|
var threadKey = GetThreadKey(ctx);
|
||||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
if (fields.State != FiberStateIdle)
|
if (fields.State != FiberStateIdle)
|
||||||
{
|
{
|
||||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
FiberContinuation targetContinuation;
|
FiberContinuation targetContinuation = default;
|
||||||
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
||||||
{
|
{
|
||||||
targetContinuation = savedContinuation;
|
targetContinuation = savedContinuation;
|
||||||
resumed = true;
|
resumed = true;
|
||||||
}
|
}
|
||||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
var callerContinuation = new FiberContinuation(
|
var callerContinuation = new FiberContinuation(
|
||||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||||
outArgumentAddress);
|
outArgumentAddress);
|
||||||
|
|
||||||
|
if (!resumed)
|
||||||
|
{
|
||||||
|
var rootStackTop = _threadStates.TryGetValue(threadKey, out var existingThreadState)
|
||||||
|
? existingThreadState.RootContinuation.Context.Rsp
|
||||||
|
: callerContinuation.Context.Rsp;
|
||||||
|
if (!TryCreateInitialContinuation(
|
||||||
|
ctx,
|
||||||
|
fields,
|
||||||
|
argOnRun,
|
||||||
|
rootStackTop,
|
||||||
|
out targetContinuation))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||||
previousState != FiberStateRun ||
|
previousState != FiberStateRun ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations[previousFiber] = callerContinuation;
|
_continuations[previousFiber] = callerContinuation;
|
||||||
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
if (_threadStates.ContainsKey(threadKey))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadStates[threadKey] = new FiberThreadState(callerContinuation, fiber, previousFiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(previousFiber, out _);
|
_continuations.TryRemove(previousFiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||||
}
|
}
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
else
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
{
|
||||||
|
_threadStates.TryRemove(threadKey, out _);
|
||||||
|
}
|
||||||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumed)
|
if (resumed)
|
||||||
@@ -648,35 +664,66 @@ public static class FiberExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = targetContinuation.Context with { Rax = 0 };
|
transferTarget = targetContinuation.Context with { Rax = 0 };
|
||||||
|
if (_threadStates.TryGetValue(threadKey, out var activeState))
|
||||||
|
{
|
||||||
|
_threadStates[threadKey] = activeState with
|
||||||
|
{
|
||||||
|
CurrentFiber = fiber,
|
||||||
|
PreviousFiber = previousFiber,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentFiberAddress = fiber;
|
|
||||||
_ = GuestThreadExecution.EnterFiber(fiber);
|
_ = GuestThreadExecution.EnterFiber(fiber);
|
||||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCreateInitialContinuation(
|
private static bool TryCreateInitialContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
FiberFields fields,
|
FiberFields fields,
|
||||||
ulong argOnRun,
|
ulong argOnRun,
|
||||||
|
ulong rootStackTop,
|
||||||
out FiberContinuation continuation)
|
out FiberContinuation continuation)
|
||||||
{
|
{
|
||||||
continuation = default;
|
continuation = default;
|
||||||
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
|
ulong stackEnd;
|
||||||
|
if (fields.ContextAddress == 0)
|
||||||
|
{
|
||||||
|
if (rootStackTop < 32)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contextless fibers are specified to execute on the root
|
||||||
|
// sceFiberRun stack. Keep the suspended import return record
|
||||||
|
// above the entry stack and use a synthetic transfer slot below it.
|
||||||
|
stackEnd = rootStackTop & ~15UL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (fields.ContextSize < FiberContextMinimumSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCalculateInitialStackLayout(stackEnd, out var entryRsp, out var transferSlot))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!TryWriteUInt64(ctx, transferSlot, fields.Entry) ||
|
||||||
|
!TryWriteUInt64(ctx, entryRsp, 0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
var setFpuRegisters = (fields.Flags & FiberFlagSetFpuRegs) != 0;
|
||||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
|
||||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation = new FiberContinuation(
|
continuation = new FiberContinuation(
|
||||||
new GuestCpuContinuation(
|
new GuestCpuContinuation(
|
||||||
@@ -698,7 +745,12 @@ public static class FiberExports
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0),
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
setFpuRegisters ? InitialFpuControlWord : ctx.FpuControlWord,
|
||||||
|
setFpuRegisters ? InitialMxcsr : ctx.Mxcsr,
|
||||||
|
RestoreFullFpuState: setFpuRegisters),
|
||||||
0);
|
0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -708,7 +760,7 @@ public static class FiberExports
|
|||||||
FiberContinuation continuation,
|
FiberContinuation continuation,
|
||||||
ulong argument) =>
|
ulong argument) =>
|
||||||
continuation.ArgOnRunAddress == 0 ||
|
continuation.ArgOnRunAddress == 0 ||
|
||||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||||
|
|
||||||
private static GuestCpuContinuation CaptureContinuation(
|
private static GuestCpuContinuation CaptureContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -731,26 +783,99 @@ public static class FiberExports
|
|||||||
ctx[CpuRegister.Rdi],
|
ctx[CpuRegister.Rdi],
|
||||||
ctx[CpuRegister.R8],
|
ctx[CpuRegister.R8],
|
||||||
ctx[CpuRegister.R9],
|
ctx[CpuRegister.R9],
|
||||||
|
ctx[CpuRegister.R10],
|
||||||
|
ctx[CpuRegister.R11],
|
||||||
ctx[CpuRegister.R12],
|
ctx[CpuRegister.R12],
|
||||||
ctx[CpuRegister.R13],
|
ctx[CpuRegister.R13],
|
||||||
ctx[CpuRegister.R14],
|
ctx[CpuRegister.R14],
|
||||||
ctx[CpuRegister.R15]);
|
ctx[CpuRegister.R15],
|
||||||
|
ctx.FpuControlWord,
|
||||||
|
ctx.Mxcsr,
|
||||||
|
RestoreFullFpuState: false);
|
||||||
|
|
||||||
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
|
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
|
||||||
{
|
{
|
||||||
if (_currentFiberAddress != 0)
|
|
||||||
{
|
|
||||||
return _currentFiberAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GuestThreadExecution.CurrentFiberAddress != 0)
|
if (GuestThreadExecution.CurrentFiberAddress != 0)
|
||||||
{
|
{
|
||||||
return GuestThreadExecution.CurrentFiberAddress;
|
return GuestThreadExecution.CurrentFiberAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState) &&
|
||||||
|
threadState.CurrentFiber != 0)
|
||||||
|
{
|
||||||
|
return threadState.CurrentFiber;
|
||||||
|
}
|
||||||
|
|
||||||
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
|
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong GetThreadKey(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
|
if (handle != 0)
|
||||||
|
{
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main guest thread does not always have a scheduler handle. Its
|
||||||
|
// ABI thread pointer is stable across native/managed transitions.
|
||||||
|
return ctx.FsBase != 0 ? ctx.FsBase : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ApplyInitializationFlags(
|
||||||
|
uint flags,
|
||||||
|
uint buildVersion,
|
||||||
|
bool contextSizeCheck)
|
||||||
|
{
|
||||||
|
if (buildVersion >= Firmware350BuildVersion)
|
||||||
|
{
|
||||||
|
flags |= FiberFlagSetFpuRegs;
|
||||||
|
}
|
||||||
|
if (contextSizeCheck)
|
||||||
|
{
|
||||||
|
flags |= FiberFlagContextSizeCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCalculateInitialStackLayout(
|
||||||
|
ulong stackEnd,
|
||||||
|
out ulong entryRsp,
|
||||||
|
out ulong transferSlot)
|
||||||
|
{
|
||||||
|
var alignedEnd = stackEnd & ~15UL;
|
||||||
|
if (alignedEnd < 2 * sizeof(ulong))
|
||||||
|
{
|
||||||
|
entryRsp = 0;
|
||||||
|
transferSlot = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryRsp = alignedEnd - sizeof(ulong);
|
||||||
|
transferSlot = entryRsp - sizeof(ulong);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void RunFiberSelfChecks()
|
||||||
|
{
|
||||||
|
Debug.Assert(
|
||||||
|
ApplyInitializationFlags(0, Firmware350BuildVersion - 1, false) == 0,
|
||||||
|
"Pre-3.50 fibers unexpectedly enable SetFpuRegs.");
|
||||||
|
Debug.Assert(
|
||||||
|
ApplyInitializationFlags(0, Firmware350BuildVersion, false) == FiberFlagSetFpuRegs,
|
||||||
|
"3.50+ fibers must initialize x87/MXCSR control state.");
|
||||||
|
Debug.Assert(
|
||||||
|
ApplyInitializationFlags(1, Firmware350BuildVersion, true) ==
|
||||||
|
(1u | FiberFlagSetFpuRegs | FiberFlagContextSizeCheck),
|
||||||
|
"Fiber initialization discarded caller/internal flags.");
|
||||||
|
Debug.Assert(
|
||||||
|
TryCalculateInitialStackLayout(0x1017, out var rsp, out var slot) &&
|
||||||
|
rsp == 0x1008 && slot == 0x1000 && (rsp & 15) == 8,
|
||||||
|
"Initial fiber transfer slot does not produce SysV entry alignment.");
|
||||||
|
}
|
||||||
|
|
||||||
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
|
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
|
||||||
ResolveCurrentFiberAddress(ctx);
|
ResolveCurrentFiberAddress(ctx);
|
||||||
|
|
||||||
@@ -779,11 +904,11 @@ public static class FiberExports
|
|||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
@@ -839,8 +964,8 @@ public static class FiberExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||||
magicStart != FiberSignature0 ||
|
magicStart != FiberSignature0 ||
|
||||||
magicEnd != FiberSignature1)
|
magicEnd != FiberSignature1)
|
||||||
{
|
{
|
||||||
@@ -855,12 +980,12 @@ public static class FiberExports
|
|||||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||||
{
|
{
|
||||||
fields = default;
|
fields = default;
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -898,6 +1023,46 @@ public static class FiberExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||||
@@ -925,6 +1090,37 @@ public static class FiberExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceFiber(string message)
|
private static void TraceFiber(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||||
@@ -946,9 +1142,10 @@ public static class FiberExports
|
|||||||
GuestCpuContinuation Context,
|
GuestCpuContinuation Context,
|
||||||
ulong ArgOnRunAddress);
|
ulong ArgOnRunAddress);
|
||||||
|
|
||||||
private readonly record struct FiberReturnTarget(
|
private sealed record FiberThreadState(
|
||||||
ulong PreviousFiber,
|
FiberContinuation RootContinuation,
|
||||||
FiberContinuation? ThreadContinuation);
|
ulong CurrentFiber,
|
||||||
|
ulong PreviousFiber);
|
||||||
|
|
||||||
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Font;
|
||||||
|
|
||||||
|
public static class FontExports
|
||||||
|
{
|
||||||
|
private static readonly object AllocationGate = new();
|
||||||
|
private static ulong _librarySelectionAddress;
|
||||||
|
private static ulong _rendererSelectionAddress;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "whrS4oksXc4",
|
||||||
|
ExportName = "sceFontMemoryInit",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int MemoryInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var descriptorAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var regionAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var regionSize = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
var interfaceAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var mspaceAddress = ctx[CpuRegister.R8];
|
||||||
|
var destroyCallback = ctx[CpuRegister.R9];
|
||||||
|
if (descriptorAddress == 0 ||
|
||||||
|
!TryWriteUInt32(ctx, descriptorAddress, 0x00000F00) ||
|
||||||
|
!TryWriteUInt32(ctx, descriptorAddress + 0x04, regionSize) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x08, regionAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x10, mspaceAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x18, interfaceAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x20, destroyCallback) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x28, 0) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x30, 0) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x38, mspaceAddress))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oM+XCzVG3oM",
|
||||||
|
ExportName = "sceFontSelectLibraryFt",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFontFt")]
|
||||||
|
public static int SelectLibraryFt(CpuContext ctx) =>
|
||||||
|
ReturnSelection(ctx, ref _librarySelectionAddress, 0x38);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Xx974EW-QFY",
|
||||||
|
ExportName = "sceFontSelectRendererFt",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFontFt")]
|
||||||
|
public static int SelectRendererFt(CpuContext ctx) =>
|
||||||
|
ReturnSelection(ctx, ref _rendererSelectionAddress, 0x100);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "n590hj5Oe-k",
|
||||||
|
ExportName = "sceFontCreateLibraryWithEdition",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int CreateLibraryWithEdition(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F01);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WaSFJoRWXaI",
|
||||||
|
ExportName = "sceFontCreateRendererWithEdition",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int CreateRendererWithEdition(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3OdRkSjOcog",
|
||||||
|
ExportName = "sceFontBindRenderer",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int BindRenderer(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "N1EBMeGhf7E",
|
||||||
|
ExportName = "sceFontSetScalePixel",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SetScalePixel(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "TMtqoFQjjbA",
|
||||||
|
ExportName = "sceFontSetEffectSlant",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SetEffectSlant(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "v0phZwa4R5o",
|
||||||
|
ExportName = "sceFontSetEffectWeight",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SetEffectWeight(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6vGCkkQJOcI",
|
||||||
|
ExportName = "sceFontSetupRenderScalePixel",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SetupRenderScalePixel(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lz9y9UFO2UU",
|
||||||
|
ExportName = "sceFontSetupRenderEffectSlant",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SetupRenderEffectSlant(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XIGorvLusDQ",
|
||||||
|
ExportName = "sceFontSetupRenderEffectWeight",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SetupRenderEffectWeight(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "imxVx8lm+KM",
|
||||||
|
ExportName = "sceFontGetHorizontalLayout",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int GetHorizontalLayout(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var layoutAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (layoutAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baseline, line advance, decoration extent: the same invented geometry
|
||||||
|
// as GetRenderCharGlyphMetrics.
|
||||||
|
var values = new[] { 12.0f, 16.0f, 0.0f };
|
||||||
|
for (var index = 0; index < values.Length; index++)
|
||||||
|
{
|
||||||
|
if (!TryWriteUInt32(
|
||||||
|
ctx,
|
||||||
|
layoutAddress + (ulong)(index * sizeof(float)),
|
||||||
|
BitConverter.SingleToUInt32Bits(values[index])))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cKYtVmeSTcw",
|
||||||
|
ExportName = "sceFontOpenFontSet",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int OpenFontSet(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KXUpebrFk1U",
|
||||||
|
ExportName = "sceFontOpenFontMemory",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int OpenFontMemory(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JzCH3SCFnAU",
|
||||||
|
ExportName = "sceFontOpenFontInstance",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int OpenFontInstance(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var sourceHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var setupHandle = ctx[CpuRegister.Rsi];
|
||||||
|
var outputAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (outputAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setupHandle != 0)
|
||||||
|
{
|
||||||
|
return ctx.TryWriteUInt64(outputAddress, setupHandle)
|
||||||
|
? SetSuccess(ctx)
|
||||||
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateOpaque(ctx, 0x100, out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceHandle != 0)
|
||||||
|
{
|
||||||
|
Span<byte> source = stackalloc byte[0x100];
|
||||||
|
if (ctx.Memory.TryRead(sourceHandle, source))
|
||||||
|
{
|
||||||
|
_ = ctx.Memory.TryWrite(handle, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryWriteUInt16(ctx, handle, 0x0F02);
|
||||||
|
return ctx.TryWriteUInt64(outputAddress, handle)
|
||||||
|
? SetSuccess(ctx)
|
||||||
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SsRbbCiWoGw",
|
||||||
|
ExportName = "sceFontSupportSystemFonts",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SupportSystemFonts(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mz2iTY0MK4A",
|
||||||
|
ExportName = "sceFontSupportExternalFonts",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SupportExternalFonts(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CUKn5pX-NVY",
|
||||||
|
ExportName = "sceFontAttachDeviceCacheBuffer",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int AttachDeviceCacheBuffer(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IQtleGLL5pQ",
|
||||||
|
ExportName = "sceFontGetRenderCharGlyphMetrics",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int GetRenderCharGlyphMetrics(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var metricsAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (metricsAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
|
||||||
|
for (var index = 0; index < values.Length; index++)
|
||||||
|
{
|
||||||
|
if (!TryWriteUInt32(
|
||||||
|
ctx,
|
||||||
|
metricsAddress + (ulong)(index * sizeof(float)),
|
||||||
|
BitConverter.SingleToUInt32Bits(values[index])))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gdUCnU0gHdI",
|
||||||
|
ExportName = "sceFontRenderSurfaceInit",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int RenderSurfaceInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var surfaceAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var widthBytes = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
var pixelBytes = (uint)ctx[CpuRegister.Rcx] & 0xFF;
|
||||||
|
var width = (uint)ctx[CpuRegister.R8];
|
||||||
|
var height = (uint)ctx[CpuRegister.R9];
|
||||||
|
if (surfaceAddress == 0 ||
|
||||||
|
!ctx.TryWriteUInt64(surfaceAddress, bufferAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x08, widthBytes) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x0C, pixelBytes) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x10, width) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x14, height) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x18, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x1C, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x20, width) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x24, height))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnSelection(CpuContext ctx, ref ulong selectionAddress, uint objectSize)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (AllocationGate)
|
||||||
|
{
|
||||||
|
if (selectionAddress == 0)
|
||||||
|
{
|
||||||
|
if (!TryAllocateOpaque(ctx, 0x20, out selectionAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, selectionAddress, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, selectionAddress + 4, objectSize))
|
||||||
|
{
|
||||||
|
selectionAddress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = selectionAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CreateOpaqueHandle(CpuContext ctx, ulong outputAddress, int size, ushort magic)
|
||||||
|
{
|
||||||
|
if (outputAddress == 0 || !TryAllocateOpaque(ctx, size, out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteUInt16(ctx, handle, magic) || !ctx.TryWriteUInt64(outputAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryAllocateOpaque(CpuContext ctx, int size, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
if (ctx.Memory is not IGuestMemoryAllocator allocator ||
|
||||||
|
!allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[size];
|
||||||
|
bytes.Clear();
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetSuccess(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ internal sealed record GuestDrawTexture(
|
|||||||
bool IsStorage,
|
bool IsStorage,
|
||||||
uint MipLevels = 1,
|
uint MipLevels = 1,
|
||||||
uint MipLevel = 0,
|
uint MipLevel = 0,
|
||||||
|
uint BaseMipLevel = 0,
|
||||||
|
uint ResourceMipLevels = 1,
|
||||||
uint Pitch = 0,
|
uint Pitch = 0,
|
||||||
uint TileMode = 0,
|
uint TileMode = 0,
|
||||||
uint DstSelect = 0xFAC,
|
uint DstSelect = 0xFAC,
|
||||||
@@ -36,7 +38,11 @@ internal readonly record struct GuestSampler(
|
|||||||
|
|
||||||
internal sealed record GuestMemoryBuffer(
|
internal sealed record GuestMemoryBuffer(
|
||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
byte[] Data);
|
byte[] Data,
|
||||||
|
int Length,
|
||||||
|
bool Pooled,
|
||||||
|
bool Writable = false,
|
||||||
|
bool WriteBackToGuest = true);
|
||||||
|
|
||||||
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
|
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
|
||||||
internal sealed record GuestVertexBuffer(
|
internal sealed record GuestVertexBuffer(
|
||||||
@@ -47,11 +53,15 @@ internal sealed record GuestVertexBuffer(
|
|||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
uint Stride,
|
uint Stride,
|
||||||
uint OffsetBytes,
|
uint OffsetBytes,
|
||||||
byte[] Data);
|
byte[] Data,
|
||||||
|
int Length,
|
||||||
|
bool Pooled);
|
||||||
|
|
||||||
internal sealed record GuestIndexBuffer(
|
internal sealed record GuestIndexBuffer(
|
||||||
byte[] Data,
|
byte[] Data,
|
||||||
bool Is32Bit);
|
int Length,
|
||||||
|
bool Is32Bit,
|
||||||
|
bool Pooled);
|
||||||
|
|
||||||
internal readonly record struct GuestRect(
|
internal readonly record struct GuestRect(
|
||||||
int X,
|
int X,
|
||||||
@@ -67,6 +77,26 @@ internal readonly record struct GuestViewport(
|
|||||||
float MinDepth,
|
float MinDepth,
|
||||||
float MaxDepth);
|
float MaxDepth);
|
||||||
|
|
||||||
|
internal readonly record struct GuestRasterState(
|
||||||
|
bool CullFront,
|
||||||
|
bool CullBack,
|
||||||
|
bool FrontFaceClockwise,
|
||||||
|
bool Wireframe)
|
||||||
|
{
|
||||||
|
public static GuestRasterState Default { get; } = new(false, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareOp uses the GCN DB_DEPTH_CONTROL ZFUNC encoding, which matches the
|
||||||
|
// Vulkan CompareOp ordering (0=Never through 7=Always).
|
||||||
|
internal readonly record struct GuestDepthState(
|
||||||
|
bool TestEnable,
|
||||||
|
bool WriteEnable,
|
||||||
|
uint CompareOp,
|
||||||
|
bool ClearEnable = false)
|
||||||
|
{
|
||||||
|
public static GuestDepthState Default { get; } = new(false, false, 7, false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
||||||
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
|
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
|
||||||
internal readonly record struct GuestBlendState(
|
internal readonly record struct GuestBlendState(
|
||||||
@@ -95,12 +125,16 @@ internal readonly record struct GuestBlendState(
|
|||||||
internal sealed record GuestRenderState(
|
internal sealed record GuestRenderState(
|
||||||
IReadOnlyList<GuestBlendState> Blends,
|
IReadOnlyList<GuestBlendState> Blends,
|
||||||
GuestRect? Scissor,
|
GuestRect? Scissor,
|
||||||
GuestViewport? Viewport)
|
GuestViewport? Viewport,
|
||||||
|
GuestRasterState Raster,
|
||||||
|
GuestDepthState Depth)
|
||||||
{
|
{
|
||||||
public static GuestRenderState Default { get; } = new(
|
public static GuestRenderState Default { get; } = new(
|
||||||
[GuestBlendState.Default],
|
[GuestBlendState.Default],
|
||||||
Scissor: null,
|
Scissor: null,
|
||||||
Viewport: null);
|
Viewport: null,
|
||||||
|
GuestRasterState.Default,
|
||||||
|
GuestDepthState.Default);
|
||||||
|
|
||||||
public GuestBlendState Blend =>
|
public GuestBlendState Blend =>
|
||||||
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
|
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
|
||||||
@@ -114,3 +148,17 @@ internal sealed record GuestRenderTarget(
|
|||||||
uint Format,
|
uint Format,
|
||||||
uint NumberType,
|
uint NumberType,
|
||||||
uint MipLevels = 1);
|
uint MipLevels = 1);
|
||||||
|
|
||||||
|
/// <summary>Guest DB surface bound alongside a color render target.</summary>
|
||||||
|
internal sealed record GuestDepthTarget(
|
||||||
|
ulong ReadAddress,
|
||||||
|
ulong WriteAddress,
|
||||||
|
uint Width,
|
||||||
|
uint Height,
|
||||||
|
uint GuestFormat,
|
||||||
|
uint SwizzleMode,
|
||||||
|
float ClearDepth,
|
||||||
|
bool ReadOnly)
|
||||||
|
{
|
||||||
|
public ulong Address => WriteAddress != 0 ? WriteAddress : ReadAddress;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ internal interface IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1);
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
int requiredVertexOutputCount = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1);
|
||||||
|
|
||||||
bool TryCompilePixelShader(
|
bool TryCompilePixelShader(
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
@@ -44,7 +46,10 @@ internal interface IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1);
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
uint pixelInputEnable = 0,
|
||||||
|
uint pixelInputAddress = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1);
|
||||||
|
|
||||||
bool TryCompileComputeShader(
|
bool TryCompileComputeShader(
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
@@ -53,7 +58,14 @@ internal interface IGuestGpuBackend
|
|||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
out IGuestCompiledShader? shader,
|
out IGuestCompiledShader? shader,
|
||||||
out string error);
|
out string error,
|
||||||
|
int totalGlobalBufferCount = -1,
|
||||||
|
int initialScalarBufferIndex = -1,
|
||||||
|
uint waveLaneCount = 32,
|
||||||
|
ulong storageBufferOffsetAlignment = 1);
|
||||||
|
|
||||||
|
/// <summary>Returns the backend's no-color-output fragment shader.</summary>
|
||||||
|
IGuestCompiledShader GetDepthOnlyFragmentShader();
|
||||||
|
|
||||||
void HideSplashScreen();
|
void HideSplashScreen();
|
||||||
|
|
||||||
@@ -78,6 +90,21 @@ internal interface IGuestGpuBackend
|
|||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
GuestRenderState? renderState = null);
|
GuestRenderState? renderState = null);
|
||||||
|
|
||||||
|
void SubmitDepthOnlyTranslatedDraw(
|
||||||
|
IGuestCompiledShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount,
|
||||||
|
GuestDepthTarget depthTarget,
|
||||||
|
IGuestCompiledShader? vertexShader = null,
|
||||||
|
uint vertexCount = 3,
|
||||||
|
uint instanceCount = 1,
|
||||||
|
uint primitiveType = 4,
|
||||||
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
|
GuestRenderState? renderState = null,
|
||||||
|
ulong shaderAddress = 0);
|
||||||
|
|
||||||
void SubmitOffscreenTranslatedDraw(
|
void SubmitOffscreenTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
@@ -90,7 +117,9 @@ internal interface IGuestGpuBackend
|
|||||||
uint primitiveType = 4,
|
uint primitiveType = 4,
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
GuestRenderState? renderState = null);
|
GuestRenderState? renderState = null,
|
||||||
|
GuestDepthTarget? depthTarget = null,
|
||||||
|
ulong shaderAddress = 0);
|
||||||
|
|
||||||
void SubmitStorageTranslatedDraw(
|
void SubmitStorageTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
@@ -98,16 +127,28 @@ internal interface IGuestGpuBackend
|
|||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint attributeCount,
|
uint attributeCount,
|
||||||
uint width,
|
uint width,
|
||||||
uint height);
|
uint height,
|
||||||
|
ulong shaderAddress = 0);
|
||||||
|
|
||||||
void SubmitComputeDispatch(
|
long SubmitComputeDispatch(
|
||||||
ulong shaderAddress,
|
ulong shaderAddress,
|
||||||
IGuestCompiledShader computeShader,
|
IGuestCompiledShader computeShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint groupCountX,
|
uint groupCountX,
|
||||||
uint groupCountY,
|
uint groupCountY,
|
||||||
uint groupCountZ);
|
uint groupCountZ,
|
||||||
|
uint baseGroupX,
|
||||||
|
uint baseGroupY,
|
||||||
|
uint baseGroupZ,
|
||||||
|
uint localSizeX,
|
||||||
|
uint localSizeY,
|
||||||
|
uint localSizeZ,
|
||||||
|
bool isIndirect,
|
||||||
|
bool writesGlobalMemory,
|
||||||
|
uint threadCountX = uint.MaxValue,
|
||||||
|
uint threadCountY = uint.MaxValue,
|
||||||
|
uint threadCountZ = uint.MaxValue);
|
||||||
|
|
||||||
bool TrySubmitGuestImage(
|
bool TrySubmitGuestImage(
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -115,6 +156,14 @@ internal interface IGuestGpuBackend
|
|||||||
uint height,
|
uint height,
|
||||||
uint pitchInPixel);
|
uint pitchInPixel);
|
||||||
|
|
||||||
|
bool TrySubmitOrderedGuestImageFlip(
|
||||||
|
int videoOutHandle,
|
||||||
|
int displayBufferIndex,
|
||||||
|
ulong address,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint pitchInPixel);
|
||||||
|
|
||||||
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
||||||
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
||||||
|
|
||||||
@@ -126,10 +175,12 @@ internal interface IGuestGpuBackend
|
|||||||
uint sourceWidth,
|
uint sourceWidth,
|
||||||
uint sourceHeight,
|
uint sourceHeight,
|
||||||
uint sourceFormat,
|
uint sourceFormat,
|
||||||
|
uint sourceNumberType,
|
||||||
ulong destinationAddress,
|
ulong destinationAddress,
|
||||||
uint destinationWidth,
|
uint destinationWidth,
|
||||||
uint destinationHeight,
|
uint destinationHeight,
|
||||||
uint destinationFormat);
|
uint destinationFormat,
|
||||||
|
uint destinationNumberType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the backend supports the guest render-target format, and how its pixel
|
/// Whether the backend supports the guest render-target format, and how its pixel
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ namespace SharpEmu.Libs.Gpu.Vulkan;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||||
{
|
{
|
||||||
|
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||||
|
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||||
|
|
||||||
public bool TryCompileVertexShader(
|
public bool TryCompileVertexShader(
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
Gen5ShaderEvaluation evaluation,
|
Gen5ShaderEvaluation evaluation,
|
||||||
@@ -23,7 +26,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1)
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
int requiredVertexOutputCount = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1)
|
||||||
{
|
{
|
||||||
shader = null;
|
shader = null;
|
||||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(
|
if (!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||||
@@ -34,7 +39,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase,
|
imageBindingBase,
|
||||||
scalarRegisterBufferIndex))
|
scalarRegisterBufferIndex,
|
||||||
|
requiredVertexOutputCount,
|
||||||
|
storageBufferOffsetAlignment))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -52,7 +59,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1)
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
uint pixelInputEnable = 0,
|
||||||
|
uint pixelInputAddress = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1)
|
||||||
{
|
{
|
||||||
shader = null;
|
shader = null;
|
||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
@@ -64,7 +74,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase,
|
imageBindingBase,
|
||||||
scalarRegisterBufferIndex))
|
scalarRegisterBufferIndex,
|
||||||
|
pixelInputEnable,
|
||||||
|
pixelInputAddress,
|
||||||
|
storageBufferOffsetAlignment))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -80,7 +93,11 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
out IGuestCompiledShader? shader,
|
out IGuestCompiledShader? shader,
|
||||||
out string error)
|
out string error,
|
||||||
|
int totalGlobalBufferCount = -1,
|
||||||
|
int initialScalarBufferIndex = -1,
|
||||||
|
uint waveLaneCount = 32,
|
||||||
|
ulong storageBufferOffsetAlignment = 1)
|
||||||
{
|
{
|
||||||
shader = null;
|
shader = null;
|
||||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(
|
if (!Gen5SpirvTranslator.TryCompileComputeShader(
|
||||||
@@ -90,7 +107,11 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
localSizeY,
|
localSizeY,
|
||||||
localSizeZ,
|
localSizeZ,
|
||||||
out var compiled,
|
out var compiled,
|
||||||
out error))
|
out error,
|
||||||
|
totalGlobalBufferCount,
|
||||||
|
initialScalarBufferIndex,
|
||||||
|
waveLaneCount,
|
||||||
|
storageBufferOffsetAlignment))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -99,6 +120,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IGuestCompiledShader GetDepthOnlyFragmentShader() =>
|
||||||
|
DepthOnlyFragmentShader;
|
||||||
|
|
||||||
public void EnsureStarted(uint width, uint height) =>
|
public void EnsureStarted(uint width, uint height) =>
|
||||||
VulkanVideoPresenter.EnsureStarted(width, height);
|
VulkanVideoPresenter.EnsureStarted(width, height);
|
||||||
|
|
||||||
@@ -140,6 +164,35 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
vertexBuffers,
|
vertexBuffers,
|
||||||
renderState);
|
renderState);
|
||||||
|
|
||||||
|
public void SubmitDepthOnlyTranslatedDraw(
|
||||||
|
IGuestCompiledShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount,
|
||||||
|
GuestDepthTarget depthTarget,
|
||||||
|
IGuestCompiledShader? vertexShader = null,
|
||||||
|
uint vertexCount = 3,
|
||||||
|
uint instanceCount = 1,
|
||||||
|
uint primitiveType = 4,
|
||||||
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
|
GuestRenderState? renderState = null,
|
||||||
|
ulong shaderAddress = 0) =>
|
||||||
|
VulkanVideoPresenter.SubmitDepthOnlyTranslatedDraw(
|
||||||
|
Spirv(pixelShader),
|
||||||
|
textures,
|
||||||
|
globalMemoryBuffers,
|
||||||
|
attributeCount,
|
||||||
|
depthTarget,
|
||||||
|
vertexShader is null ? null : Spirv(vertexShader),
|
||||||
|
vertexCount,
|
||||||
|
instanceCount,
|
||||||
|
primitiveType,
|
||||||
|
indexBuffer,
|
||||||
|
vertexBuffers,
|
||||||
|
renderState,
|
||||||
|
shaderAddress);
|
||||||
|
|
||||||
public void SubmitOffscreenTranslatedDraw(
|
public void SubmitOffscreenTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
@@ -152,7 +205,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint primitiveType = 4,
|
uint primitiveType = 4,
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
GuestRenderState? renderState = null) =>
|
GuestRenderState? renderState = null,
|
||||||
|
GuestDepthTarget? depthTarget = null,
|
||||||
|
ulong shaderAddress = 0) =>
|
||||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||||
Spirv(pixelShader),
|
Spirv(pixelShader),
|
||||||
textures,
|
textures,
|
||||||
@@ -165,7 +220,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
primitiveType,
|
primitiveType,
|
||||||
indexBuffer,
|
indexBuffer,
|
||||||
vertexBuffers,
|
vertexBuffers,
|
||||||
renderState);
|
renderState,
|
||||||
|
depthTarget,
|
||||||
|
shaderAddress);
|
||||||
|
|
||||||
public void SubmitStorageTranslatedDraw(
|
public void SubmitStorageTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
@@ -173,23 +230,36 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint attributeCount,
|
uint attributeCount,
|
||||||
uint width,
|
uint width,
|
||||||
uint height) =>
|
uint height,
|
||||||
|
ulong shaderAddress = 0) =>
|
||||||
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
||||||
Spirv(pixelShader),
|
Spirv(pixelShader),
|
||||||
textures,
|
textures,
|
||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
attributeCount,
|
attributeCount,
|
||||||
width,
|
width,
|
||||||
height);
|
height,
|
||||||
|
shaderAddress);
|
||||||
|
|
||||||
public void SubmitComputeDispatch(
|
public long SubmitComputeDispatch(
|
||||||
ulong shaderAddress,
|
ulong shaderAddress,
|
||||||
IGuestCompiledShader computeShader,
|
IGuestCompiledShader computeShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint groupCountX,
|
uint groupCountX,
|
||||||
uint groupCountY,
|
uint groupCountY,
|
||||||
uint groupCountZ) =>
|
uint groupCountZ,
|
||||||
|
uint baseGroupX,
|
||||||
|
uint baseGroupY,
|
||||||
|
uint baseGroupZ,
|
||||||
|
uint localSizeX,
|
||||||
|
uint localSizeY,
|
||||||
|
uint localSizeZ,
|
||||||
|
bool isIndirect,
|
||||||
|
bool writesGlobalMemory,
|
||||||
|
uint threadCountX = uint.MaxValue,
|
||||||
|
uint threadCountY = uint.MaxValue,
|
||||||
|
uint threadCountZ = uint.MaxValue) =>
|
||||||
VulkanVideoPresenter.SubmitComputeDispatch(
|
VulkanVideoPresenter.SubmitComputeDispatch(
|
||||||
shaderAddress,
|
shaderAddress,
|
||||||
Spirv(computeShader),
|
Spirv(computeShader),
|
||||||
@@ -197,7 +267,18 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
groupCountX,
|
groupCountX,
|
||||||
groupCountY,
|
groupCountY,
|
||||||
groupCountZ);
|
groupCountZ,
|
||||||
|
baseGroupX,
|
||||||
|
baseGroupY,
|
||||||
|
baseGroupZ,
|
||||||
|
localSizeX,
|
||||||
|
localSizeY,
|
||||||
|
localSizeZ,
|
||||||
|
isIndirect,
|
||||||
|
writesGlobalMemory,
|
||||||
|
threadCountX,
|
||||||
|
threadCountY,
|
||||||
|
threadCountZ);
|
||||||
|
|
||||||
public bool TrySubmitGuestImage(
|
public bool TrySubmitGuestImage(
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -206,6 +287,21 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint pitchInPixel) =>
|
uint pitchInPixel) =>
|
||||||
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||||
|
|
||||||
|
public bool TrySubmitOrderedGuestImageFlip(
|
||||||
|
int videoOutHandle,
|
||||||
|
int displayBufferIndex,
|
||||||
|
ulong address,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint pitchInPixel) =>
|
||||||
|
VulkanVideoPresenter.TrySubmitOrderedGuestImageFlip(
|
||||||
|
videoOutHandle,
|
||||||
|
displayBufferIndex,
|
||||||
|
address,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pitchInPixel);
|
||||||
|
|
||||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
||||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||||
|
|
||||||
@@ -217,19 +313,23 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint sourceWidth,
|
uint sourceWidth,
|
||||||
uint sourceHeight,
|
uint sourceHeight,
|
||||||
uint sourceFormat,
|
uint sourceFormat,
|
||||||
|
uint sourceNumberType,
|
||||||
ulong destinationAddress,
|
ulong destinationAddress,
|
||||||
uint destinationWidth,
|
uint destinationWidth,
|
||||||
uint destinationHeight,
|
uint destinationHeight,
|
||||||
uint destinationFormat) =>
|
uint destinationFormat,
|
||||||
|
uint destinationNumberType) =>
|
||||||
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
||||||
sourceAddress,
|
sourceAddress,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
sourceHeight,
|
sourceHeight,
|
||||||
sourceFormat,
|
sourceFormat,
|
||||||
|
sourceNumberType,
|
||||||
destinationAddress,
|
destinationAddress,
|
||||||
destinationWidth,
|
destinationWidth,
|
||||||
destinationHeight,
|
destinationHeight,
|
||||||
destinationFormat);
|
destinationFormat,
|
||||||
|
destinationNumberType);
|
||||||
|
|
||||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-resolution host sleeps for guest pacing. <see cref="Thread.Sleep(int)"/>
|
||||||
|
/// routinely overshoots by a scheduler quantum, which turns per-frame waits
|
||||||
|
/// (flip pacing, usleep-based game loops) into a hard frame-rate cap; these
|
||||||
|
/// helpers sleep coarsely for the bulk of the interval and yield-spin the
|
||||||
|
/// remainder so wakeups land within tens of microseconds of the target.
|
||||||
|
/// </summary>
|
||||||
|
internal static class HostTiming
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Blocks until <see cref="Stopwatch.GetTimestamp"/> reaches
|
||||||
|
/// <paramref name="targetTimestamp"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static void SleepUntil(long targetTimestamp)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var remainingTicks = targetTimestamp - Stopwatch.GetTimestamp();
|
||||||
|
if (remainingTicks <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTicks > Stopwatch.Frequency * 60)
|
||||||
|
{
|
||||||
|
// Far-future target: coarse sleep avoids overflowing the
|
||||||
|
// microsecond conversion below and needs no precision.
|
||||||
|
Thread.Sleep(30_000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingMicroseconds = remainingTicks * 1_000_000 / Stopwatch.Frequency;
|
||||||
|
if (remainingMicroseconds > 2200)
|
||||||
|
{
|
||||||
|
// Coarse sleep for the bulk; macOS overshoots ~0.5-1.5 ms.
|
||||||
|
Thread.Sleep((int)((remainingMicroseconds - 1200) / 1000));
|
||||||
|
}
|
||||||
|
else if (remainingMicroseconds > 1200)
|
||||||
|
{
|
||||||
|
// A 1 ms nap typically wakes within the margin and costs no
|
||||||
|
// CPU, unlike yield-spinning through the whole tail.
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
else if (remainingMicroseconds > 100)
|
||||||
|
{
|
||||||
|
Thread.Sleep(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.SpinWait(64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Blocks for the given number of microseconds.</summary>
|
||||||
|
public static void SleepMicroseconds(long microseconds)
|
||||||
|
{
|
||||||
|
if (microseconds <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microseconds >= 10_000_000)
|
||||||
|
{
|
||||||
|
// Long/sentinel sleeps (usleep(-1) parks): sub-millisecond
|
||||||
|
// precision is irrelevant and the tick conversion below would
|
||||||
|
// overflow, which used to turn the park into a hot spin.
|
||||||
|
Thread.Sleep((int)Math.Min(microseconds / 1000, int.MaxValue));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticks = microseconds * Stopwatch.Frequency / 1_000_000;
|
||||||
|
SleepUntil(Stopwatch.GetTimestamp() + ticks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,34 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Json;
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
public static class JsonExports
|
public static class JsonExports
|
||||||
{
|
{
|
||||||
|
private const int ValueObjectSize = 0x20;
|
||||||
|
private const int StringObjectSize = 0x08;
|
||||||
|
private const ulong MaximumJsonBufferSize = 16 * 1024 * 1024;
|
||||||
|
private const int SceJsonParserErrorInvalidToken = unchecked((int)0x80920101);
|
||||||
|
private const int SceJsonParserErrorEmptyBuffer = unchecked((int)0x80920105);
|
||||||
|
|
||||||
|
private sealed record JsonValueState(JsonElement Element);
|
||||||
|
|
||||||
|
private sealed record JsonStringState(
|
||||||
|
string Value,
|
||||||
|
ulong GuestBufferAddress = 0,
|
||||||
|
int GuestBufferCapacity = 0);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, JsonValueState> _values = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, JsonStringState> _strings = new();
|
||||||
|
private static readonly JsonElement _nullElement = CreateNullElement();
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "-hJRce8wn1U",
|
Nid = "-hJRce8wn1U",
|
||||||
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
|
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
|
||||||
@@ -78,9 +100,6 @@ public static class JsonExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
|
|
||||||
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
|
|
||||||
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "+drDFyAS6u4",
|
Nid = "+drDFyAS6u4",
|
||||||
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||||
@@ -91,26 +110,38 @@ public static class JsonExports
|
|||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
if (thisAddress == 0)
|
if (thisAddress == 0)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
||||||
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
||||||
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return SetReturn(ctx, 0);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WSOuge5IsCg",
|
Nid = "WSOuge5IsCg",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int InitParameter2Constructor(CpuContext ctx)
|
public static int InitParameter2Constructor(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PS5 ABI object occupies 0x28 bytes in the caller's frame. Its
|
||||||
|
// setters below replace the allocator and file-buffer fields.
|
||||||
|
Span<byte> parameter = stackalloc byte[0x28];
|
||||||
|
parameter.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(thisAddress, parameter))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceJson("InitParameter2.ctor", thisAddress, 0);
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -118,12 +149,24 @@ public static class JsonExports
|
|||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "I2QC8PYhJWY",
|
Nid = "I2QC8PYhJWY",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int InitParameter2SetAllocator(CpuContext ctx)
|
public static int InitParameter2SetAllocator(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> fields = stackalloc byte[sizeof(ulong) * 2];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(fields, ctx[CpuRegister.Rsi]);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(fields[sizeof(ulong)..], ctx[CpuRegister.Rdx]);
|
||||||
|
if (!ctx.Memory.TryWrite(thisAddress, fields))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -131,12 +174,16 @@ public static class JsonExports
|
|||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Eu95jmqn5Rw",
|
Nid = "Eu95jmqn5Rw",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
|
if (thisAddress == 0 || !ctx.TryWriteUInt64(thisAddress + 0x10, ctx[CpuRegister.Rsi]))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -144,14 +191,474 @@ public static class JsonExports
|
|||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "IXW-z8pggfg",
|
Nid = "IXW-z8pggfg",
|
||||||
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int Initializer2Constructor(CpuContext ctx)
|
public static int InitializerInitialize2(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
|
var initParameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thisAddress == 0 || initParameterAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceJson("Initializer.initialize2", thisAddress, initParameterAddress);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
public static int ValueConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ConstructValue(ctx);
|
||||||
|
return JsonValueExports.ValueDefaultConstructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-wa17B7TGnw",
|
||||||
|
ExportName = "_ZN3sce4Json5ValueC2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueBaseConstructor(CpuContext ctx) => ConstructValue(ctx);
|
||||||
|
public static int ValueDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = DestroyValue(ctx);
|
||||||
|
return JsonValueExports.ValueDestructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0eUrW9JAxM0",
|
||||||
|
ExportName = "_ZN3sce4Json5ValueD2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueBaseDestructor(CpuContext ctx) => DestroyValue(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "S5JxQnoGF3E",
|
||||||
|
ExportName = "_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ParserParseBuffer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var bufferSize = ctx[CpuRegister.Rdx];
|
||||||
|
if (valueAddress == 0 || bufferAddress == 0 || bufferSize == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SceJsonParserErrorEmptyBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferSize > MaximumJsonBufferSize || bufferSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[(int)bufferSize];
|
||||||
|
if (!ctx.Memory.TryRead(bufferAddress, buffer))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(buffer);
|
||||||
|
var element = document.RootElement.Clone();
|
||||||
|
StoreValue(ctx, valueAddress, element);
|
||||||
|
TraceJsonText("Parser.parse", valueAddress, Encoding.UTF8.GetString(buffer));
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SHtAad20YYM",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value7getTypeEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetType(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var element = GetValue(valueAddress);
|
||||||
|
ctx[CpuRegister.Rax] = (ulong)GetValueType(element);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RBw+4NukeGQ",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value5countEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueCount(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var element = GetValue(ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = element.ValueKind switch
|
||||||
|
{
|
||||||
|
System.Text.Json.JsonValueKind.Array => (ulong)element.GetArrayLength(),
|
||||||
|
System.Text.Json.JsonValueKind.Object => (ulong)element.EnumerateObject().Count(),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "zTwZdI8AZ5Y",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value10getBooleanEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetBoolean(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DIxvoy7Ngvk",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value10getIntegerEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetInteger(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sn4HNCtNRzY",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value11getUIntegerEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetUnsignedInteger(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3qrge7L-AU4",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value7getRealEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetReal(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HwDt5lD9Bfo",
|
||||||
|
ExportName = "_ZNK3sce4Json5ValueixEPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueIndexCString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var keyAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadUtf8CString(ctx, keyAddress, 4096, out var key) ||
|
||||||
|
!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = GetValue(valueAddress);
|
||||||
|
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Object &&
|
||||||
|
parent.TryGetProperty(key, out var property)
|
||||||
|
? property.Clone()
|
||||||
|
: _nullElement;
|
||||||
|
StoreValue(ctx, childAddress, child);
|
||||||
|
ctx[CpuRegister.Rax] = childAddress;
|
||||||
|
TraceJsonText("Value.index", valueAddress, key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XlWbvieLj2M",
|
||||||
|
ExportName = "_ZNK3sce4Json5ValueixEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueIndexPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0YqYAoO-+Uo",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value8getValueEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4zrm6VrgIAw",
|
||||||
|
ExportName = "_ZN3sce4Json5ValueaSERKS1_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueAssignment(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (destinationAddress != 0)
|
||||||
|
{
|
||||||
|
StoreValue(ctx, destinationAddress, GetValue(sourceAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destinationAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int StringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ConstructString(ctx);
|
||||||
|
return JsonValueExports.StringDefaultConstructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "eG9E9M6XvTM",
|
||||||
|
ExportName = "_ZN3sce4Json6StringC2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int StringBaseConstructor(CpuContext ctx) => ConstructString(ctx);
|
||||||
|
public static int StringDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = DestroyString(ctx);
|
||||||
|
return JsonValueExports.StringDestructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ui7YFnSTCBw",
|
||||||
|
ExportName = "_ZN3sce4Json6StringD2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int StringBaseDestructor(CpuContext ctx) => DestroyString(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ncel8t2Rrpc",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value8toStringERNS0_6StringE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueToString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var stringAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (stringAddress != 0)
|
||||||
|
{
|
||||||
|
var element = GetValue(valueAddress);
|
||||||
|
var value = element.ValueKind == System.Text.Json.JsonValueKind.String
|
||||||
|
? element.GetString() ?? string.Empty
|
||||||
|
: element.GetRawText();
|
||||||
|
_strings[stringAddress] = new JsonStringState(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "L1KAkYWml-M",
|
||||||
|
ExportName = "_ZNK3sce4Json6String5c_strEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int StringCStr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var stringAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (!_strings.TryGetValue(stringAddress, out var state))
|
||||||
|
{
|
||||||
|
state = new JsonStringState(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(state.Value + '\0');
|
||||||
|
var guestBufferAddress = state.GuestBufferAddress;
|
||||||
|
if (guestBufferAddress == 0 || state.GuestBufferCapacity < bytes.Length)
|
||||||
|
{
|
||||||
|
if (!TryAllocateGuestObject(ctx, bytes.Length, out guestBufferAddress))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(guestBufferAddress, bytes))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_strings[stringAddress] = state with
|
||||||
|
{
|
||||||
|
GuestBufferAddress = guestBufferAddress,
|
||||||
|
GuestBufferCapacity = bytes.Length,
|
||||||
|
};
|
||||||
|
ctx.TryWriteUInt64(stringAddress, guestBufferAddress);
|
||||||
|
ctx[CpuRegister.Rax] = guestBufferAddress;
|
||||||
|
TraceJsonText("String.c_str", stringAddress, state.Value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ConstructValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
StoreValue(ctx, thisAddress, _nullElement);
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnIndexedValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var position = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = GetValue(valueAddress);
|
||||||
|
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Array &&
|
||||||
|
position < (ulong)parent.GetArrayLength()
|
||||||
|
? parent[(int)position].Clone()
|
||||||
|
: _nullElement;
|
||||||
|
StoreValue(ctx, childAddress, child);
|
||||||
|
ctx[CpuRegister.Rax] = childAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnValueStorage(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress == 0 ? 0 : thisAddress + 0x10;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DestroyValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
_values.TryRemove(thisAddress, out _);
|
||||||
|
if (thisAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> empty = stackalloc byte[ValueObjectSize];
|
||||||
|
empty.Clear();
|
||||||
|
ctx.Memory.TryWrite(thisAddress, empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ConstructString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_strings[thisAddress] = new JsonStringState(string.Empty);
|
||||||
|
ctx.TryWriteUInt64(thisAddress, 0);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DestroyString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
_strings.TryRemove(thisAddress, out _);
|
||||||
|
if (thisAddress != 0)
|
||||||
|
{
|
||||||
|
ctx.TryWriteUInt64(thisAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement CreateNullElement()
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse("null");
|
||||||
|
return document.RootElement.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement GetValue(ulong address) =>
|
||||||
|
address != 0 && _values.TryGetValue(address, out var state)
|
||||||
|
? state.Element
|
||||||
|
: _nullElement;
|
||||||
|
|
||||||
|
private static void StoreValue(CpuContext ctx, ulong address, JsonElement element)
|
||||||
|
{
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clone = element.Clone();
|
||||||
|
_values[address] = new JsonValueState(clone);
|
||||||
|
|
||||||
|
Span<byte> mirror = stackalloc byte[ValueObjectSize];
|
||||||
|
mirror.Clear();
|
||||||
|
var type = GetValueType(clone);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(mirror[0x1C..], type);
|
||||||
|
switch (clone.ValueKind)
|
||||||
|
{
|
||||||
|
case System.Text.Json.JsonValueKind.True:
|
||||||
|
mirror[0x10] = 1;
|
||||||
|
break;
|
||||||
|
case System.Text.Json.JsonValueKind.Number when clone.TryGetInt64(out var integer):
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(mirror[0x10..], integer);
|
||||||
|
break;
|
||||||
|
case System.Text.Json.JsonValueKind.Number when clone.TryGetUInt64(out var unsignedInteger):
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(mirror[0x10..], unsignedInteger);
|
||||||
|
break;
|
||||||
|
case System.Text.Json.JsonValueKind.Number:
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(
|
||||||
|
mirror[0x10..],
|
||||||
|
BitConverter.DoubleToInt64Bits(clone.GetDouble()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Memory.TryWrite(address, mirror);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetValueType(JsonElement element) => element.ValueKind switch
|
||||||
|
{
|
||||||
|
System.Text.Json.JsonValueKind.True or System.Text.Json.JsonValueKind.False => 1,
|
||||||
|
System.Text.Json.JsonValueKind.Number when element.TryGetInt64(out _) => 2,
|
||||||
|
System.Text.Json.JsonValueKind.Number when element.TryGetUInt64(out _) => 3,
|
||||||
|
System.Text.Json.JsonValueKind.Number => 4,
|
||||||
|
System.Text.Json.JsonValueKind.String => 5,
|
||||||
|
System.Text.Json.JsonValueKind.Array => 6,
|
||||||
|
System.Text.Json.JsonValueKind.Object => 7,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool TryAllocateGuestObject(CpuContext ctx, int size, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
return size > 0 &&
|
||||||
|
ctx.Memory is IGuestMemoryAllocator allocator &&
|
||||||
|
allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUtf8CString(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong address,
|
||||||
|
int maximumLength,
|
||||||
|
out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0 || maximumLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[maximumLength];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
||||||
@@ -164,4 +671,16 @@ public static class JsonExports
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
|
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceJsonText(string operation, ulong thisAddress, string value)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var preview = value.Length <= 128 ? value : value[..128];
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} value={preview}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// 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 System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
@@ -25,6 +25,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
|
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
|
||||||
private static long _nextEventFlagHandle = 1;
|
private static long _nextEventFlagHandle = 1;
|
||||||
|
|
||||||
|
// Cached once: gating every call site avoids building the interpolated
|
||||||
|
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
|
||||||
|
private static readonly bool _traceEventFlag = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private sealed class EventFlagState
|
private sealed class EventFlagState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
@@ -34,45 +39,6 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
|
|
||||||
{
|
|
||||||
public required CpuContext Ctx { get; init; }
|
|
||||||
public required EventFlagState State { get; init; }
|
|
||||||
public required ulong Pattern { get; init; }
|
|
||||||
public required uint WaitMode { get; init; }
|
|
||||||
public required ulong ResultAddress { get; init; }
|
|
||||||
public bool Timed { get; init; }
|
|
||||||
|
|
||||||
// Timed-wait completion state; unused when Timed is false.
|
|
||||||
public ulong TimeoutAddress { get; init; }
|
|
||||||
public long DeadlineTimestamp { get; init; }
|
|
||||||
|
|
||||||
public OrbisGen2Result? Result { get; set; }
|
|
||||||
|
|
||||||
// Untimed waits stash the prepared result here at wake and return it at resume.
|
|
||||||
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
|
|
||||||
public int Resume() => Timed
|
|
||||||
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
|
|
||||||
: (int)_blockedResult;
|
|
||||||
|
|
||||||
public bool TryWake()
|
|
||||||
{
|
|
||||||
if (Timed)
|
|
||||||
{
|
|
||||||
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_blockedResult = preparedResult;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "BpFoboUJoZU",
|
Nid = "BpFoboUJoZU",
|
||||||
ExportName = "sceKernelCreateEventFlag",
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
@@ -91,17 +57,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
optionAddress != 0 ||
|
optionAddress != 0 ||
|
||||||
!IsValidAttributes(attributes))
|
!IsValidAttributes(attributes))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||||
@@ -115,11 +81,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
{
|
{
|
||||||
_eventFlags.TryRemove(handle, out _);
|
_eventFlags.TryRemove(handle, out _);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
if (_traceEventFlag) TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -132,7 +98,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var handle = ctx[CpuRegister.Rdi];
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
if (!_eventFlags.TryRemove(handle, out var state))
|
if (!_eventFlags.TryRemove(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -140,8 +106,8 @@ public static class KernelEventFlagCompatExports
|
|||||||
Monitor.PulseAll(state.Gate);
|
Monitor.PulseAll(state.Gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
if (_traceEventFlag) TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -156,18 +122,18 @@ public static class KernelEventFlagCompatExports
|
|||||||
var returnRip = GetCurrentReturnRip();
|
var returnRip = GetCurrentReturnRip();
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
state.Bits |= pattern;
|
state.Bits |= pattern;
|
||||||
Monitor.PulseAll(state.Gate);
|
Monitor.PulseAll(state.Gate);
|
||||||
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -181,16 +147,16 @@ public static class KernelEventFlagCompatExports
|
|||||||
var pattern = ctx[CpuRegister.Rsi];
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
state.Bits &= pattern;
|
state.Bits &= pattern;
|
||||||
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
if (_traceEventFlag) TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -207,29 +173,29 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyClearMode(state, pattern, waitMode);
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,18 +215,18 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
uint timeoutUsec = 0;
|
||||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Enter(state.Gate);
|
Monitor.Enter(state.Gate);
|
||||||
@@ -268,77 +234,76 @@ public static class KernelEventFlagCompatExports
|
|||||||
{
|
{
|
||||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(immediateWaitResult);
|
return SetReturn(ctx, immediateWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
// Timed waits block on a deadline instead of returning TIMED_OUT
|
||||||
{
|
// immediately; a zero-microsecond timeout still degrades to an
|
||||||
if (timeoutUsec == 0)
|
// instant poll because the deadline is already in the past.
|
||||||
{
|
var deadline = timeoutAddress != 0
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
: 0;
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
|
var hostDeadlineMs = timeoutAddress != 0
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
? Environment.TickCount64 + (timeoutUsec == 0
|
||||||
}
|
? 0L
|
||||||
|
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
: long.MaxValue;
|
||||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
|
||||||
var timedWaiter = new EventFlagWaiter
|
|
||||||
{
|
|
||||||
Ctx = ctx,
|
|
||||||
State = state,
|
|
||||||
Pattern = pattern,
|
|
||||||
WaitMode = waitMode,
|
|
||||||
ResultAddress = resultAddress,
|
|
||||||
Timed = true,
|
|
||||||
TimeoutAddress = timeoutAddress,
|
|
||||||
DeadlineTimestamp = deadline,
|
|
||||||
};
|
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelWaitEventFlag",
|
|
||||||
GetEventFlagWakeKey(handle),
|
|
||||||
timedWaiter,
|
|
||||||
blockDeadlineTimestamp: deadline))
|
|
||||||
{
|
|
||||||
state.WaitingThreads++;
|
|
||||||
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
|
||||||
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var managedThread = Environment.CurrentManagedThreadId;
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
|
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
var satisfied = false;
|
||||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
new EventFlagWaiter
|
() =>
|
||||||
{
|
{
|
||||||
Ctx = ctx,
|
if (satisfied)
|
||||||
State = state,
|
{
|
||||||
Pattern = pattern,
|
return (int)blockedWaitResult;
|
||||||
WaitMode = waitMode,
|
}
|
||||||
ResultAddress = resultAddress,
|
|
||||||
});
|
// Deadline expiry: report timeout with the current bits.
|
||||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
if (timeoutAddress != 0)
|
||||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
{
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!TryPrepareBlockedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
out var preparedResult))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedWaitResult = preparedResult;
|
||||||
|
satisfied = true;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deadline);
|
||||||
|
if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||||
|
if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
if (!requestedBlock)
|
if (!requestedBlock)
|
||||||
{
|
{
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is null)
|
if (scheduler is null)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||||
var releaseWaiter = true;
|
var releaseWaiter = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -358,11 +323,22 @@ public static class KernelEventFlagCompatExports
|
|||||||
{
|
{
|
||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
releaseWaiter = false;
|
releaseWaiter = false;
|
||||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||||
return ctx.SetReturn(pumpedWaitResult);
|
return SetReturn(ctx, pumpedWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
var remaining = hostDeadlineMs - Environment.TickCount64;
|
||||||
|
if (timeoutAddress != 0 && remaining <= 0)
|
||||||
|
{
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
releaseWaiter = false;
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -375,8 +351,8 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -396,24 +372,26 @@ public static class KernelEventFlagCompatExports
|
|||||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (waiterCountAddress != 0 &&
|
if (waiterCountAddress != 0 &&
|
||||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Bits = setPattern;
|
state.Bits = setPattern;
|
||||||
state.WaitingThreads = 0;
|
state.WaitingThreads = 0;
|
||||||
Monitor.PulseAll(state.Gate);
|
Monitor.PulseAll(state.Gate);
|
||||||
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
if (_traceEventFlag) TraceEventFlag(
|
||||||
|
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
|
||||||
|
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidAttributes(uint attributes)
|
private static bool IsValidAttributes(uint attributes)
|
||||||
@@ -503,107 +481,99 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
TraceEventFlag(
|
if (_traceEventFlag) TraceEventFlag(
|
||||||
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
|
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCompleteBlockedTimedWait(
|
|
||||||
CpuContext ctx,
|
|
||||||
EventFlagState state,
|
|
||||||
EventFlagWaiter waiter,
|
|
||||||
ulong pattern,
|
|
||||||
uint waitMode,
|
|
||||||
ulong resultAddress)
|
|
||||||
{
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
if (waiter.Result is not null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
|
||||||
? OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
|
||||||
ApplyClearMode(state, pattern, waitMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CompleteBlockedTimedWait(
|
|
||||||
CpuContext ctx,
|
|
||||||
EventFlagState state,
|
|
||||||
EventFlagWaiter waiter,
|
|
||||||
ulong pattern,
|
|
||||||
uint waitMode,
|
|
||||||
ulong resultAddress,
|
|
||||||
ulong timeoutAddress,
|
|
||||||
long deadlineTimestamp)
|
|
||||||
{
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
if (waiter.Result is null)
|
|
||||||
{
|
|
||||||
if (IsSatisfied(state.Bits, pattern, waitMode))
|
|
||||||
{
|
|
||||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
|
||||||
? OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
|
||||||
ApplyClearMode(state, pattern, waitMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
|
||||||
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
|
|
||||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
|
||||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
|
||||||
var remainingMicros = remainingTicks <= 0
|
|
||||||
? 0u
|
|
||||||
: (uint)Math.Min(
|
|
||||||
uint.MaxValue,
|
|
||||||
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)waiter.Result.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
$"event_flag:0x{handle:X16}";
|
$"event_flag:0x{handle:X16}";
|
||||||
|
|
||||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = buffer[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceEventFlag(string message)
|
private static void TraceEventFlag(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
if (_traceEventFlag)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
|
||||||
}
|
}
|
||||||
@@ -684,7 +654,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadByte(address, out var value))
|
if (TryReadByte(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X2}");
|
builder.Append($" {name}=0x{value:X2}");
|
||||||
}
|
}
|
||||||
@@ -692,7 +662,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadUInt32(address, out var value))
|
if (TryReadUInt32(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X8}");
|
builder.Append($" {name}=0x{value:X8}");
|
||||||
}
|
}
|
||||||
@@ -700,7 +670,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadUInt64(address, out var value))
|
if (TryReadUInt64(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X16}");
|
builder.Append($" {name}=0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,13 +395,13 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
uint timeoutUsec = 0;
|
||||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -649,6 +649,60 @@ public static class KernelEventQueueCompatExports
|
|||||||
return triggeredCount;
|
return triggeredCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queues one event for every registration using <paramref name="filter"/>.
|
||||||
|
/// Unlike <see cref="TriggerRegisteredEvents"/>, this preserves distinct
|
||||||
|
/// event identifiers registered on the same queue. AGC driver completion
|
||||||
|
/// queues use this form because the driver, rather than a packet-provided
|
||||||
|
/// identifier, announces that the whole submission reached end-of-pipe.
|
||||||
|
/// </summary>
|
||||||
|
public static int TriggerRegisteredEventsDistinct(short filter)
|
||||||
|
{
|
||||||
|
HashSet<ulong>? wakeHandles = null;
|
||||||
|
var triggeredCount = 0;
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
foreach (var (handle, registrations) in _registeredEvents)
|
||||||
|
{
|
||||||
|
foreach (var registration in registrations.Values)
|
||||||
|
{
|
||||||
|
if (registration.Filter != filter)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
|
{
|
||||||
|
queue = new KernelEventDeque();
|
||||||
|
_pendingEvents[handle] = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOrUpdateEvent(
|
||||||
|
queue,
|
||||||
|
new KernelQueuedEvent(
|
||||||
|
registration.Ident,
|
||||||
|
registration.Filter,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
registration.Ident,
|
||||||
|
registration.UserData));
|
||||||
|
(wakeHandles ??= []).Add(handle);
|
||||||
|
triggeredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakeHandles is not null)
|
||||||
|
{
|
||||||
|
foreach (var handle in wakeHandles)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggeredCount;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TriggerRegisteredEvent(
|
private static bool TriggerRegisteredEvent(
|
||||||
ulong handle,
|
ulong handle,
|
||||||
ulong ident,
|
ulong ident,
|
||||||
@@ -752,7 +806,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong outCountAddress)
|
ulong outCountAddress)
|
||||||
{
|
{
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -865,8 +919,29 @@ public static class KernelEventQueueCompatExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
var returnRip = 0UL;
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,75 @@ public static class KernelExceptionCompatExports
|
|||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "il03nluKfMk",
|
||||||
|
ExportName = "sceKernelRaiseException",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int RaiseException(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var targetThread = ctx[CpuRegister.Rdi];
|
||||||
|
var exceptionType = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (targetThread == 0 || exceptionType is < 0 or >= 128)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong handler;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_installedHandlers.TryGetValue(exceptionType, out handler))
|
||||||
|
{
|
||||||
|
// The kernel accepts a raise for a type with no process
|
||||||
|
// handler; there is simply no user callback to deliver.
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_GUEST_EXCEPTIONS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
string? error = null;
|
||||||
|
if (scheduler is null ||
|
||||||
|
!scheduler.TryRaiseGuestException(
|
||||||
|
ctx,
|
||||||
|
targetThread,
|
||||||
|
handler,
|
||||||
|
exceptionType,
|
||||||
|
out error))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] sceKernelRaiseException delivery failed: " +
|
||||||
|
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
|
||||||
|
$"error={error ?? "scheduler unavailable"}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_EXCEPTIONS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] guest_exception.raise " +
|
||||||
|
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
|
||||||
|
$"handler=0x{handler:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ public static class KernelExports
|
|||||||
private static readonly object _coredumpGate = new();
|
private static readonly object _coredumpGate = new();
|
||||||
private static ulong _coredumpHandler;
|
private static ulong _coredumpHandler;
|
||||||
private static ulong _coredumpHandlerContext;
|
private static ulong _coredumpHandlerContext;
|
||||||
private const uint Gen4CompiledSdkVersion = 0x05000000;
|
|
||||||
private const uint Gen5CompiledSdkVersion = 0x09000000;
|
|
||||||
|
|
||||||
private readonly record struct CxaDestructorEntry(
|
private readonly record struct CxaDestructorEntry(
|
||||||
ulong Function,
|
ulong Function,
|
||||||
@@ -28,24 +26,7 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var versionAddress = ctx[CpuRegister.Rdi];
|
_ = ctx;
|
||||||
if (versionAddress == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
|
|
||||||
? Gen5CompiledSdkVersion
|
|
||||||
: Gen4CompiledSdkVersion;
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,20 +52,12 @@ public static class KernelExports
|
|||||||
ExportName = "exit",
|
ExportName = "exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
public static int Exit(CpuContext ctx)
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "L1SBTkC+Cvw",
|
|
||||||
ExportName = "abort",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libc")]
|
|
||||||
public static int Abort(CpuContext ctx)
|
|
||||||
{
|
{
|
||||||
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +172,11 @@ public static class KernelExports
|
|||||||
ExportName = "_exit",
|
ExportName = "_exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
public static int UnderscoreExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -218,12 +195,14 @@ public static class KernelExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PthreadCreate(CpuContext ctx)
|
public static int PthreadCreate(CpuContext ctx)
|
||||||
|
=> PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
|
||||||
|
|
||||||
|
private static int PthreadCreateCore(CpuContext ctx, ulong nameAddress)
|
||||||
{
|
{
|
||||||
var threadIdAddress = ctx[CpuRegister.Rdi];
|
var threadIdAddress = ctx[CpuRegister.Rdi];
|
||||||
var attrAddress = ctx[CpuRegister.Rsi];
|
var attrAddress = ctx[CpuRegister.Rsi];
|
||||||
var entryAddress = ctx[CpuRegister.Rdx];
|
var entryAddress = ctx[CpuRegister.Rdx];
|
||||||
var argument = ctx[CpuRegister.Rcx];
|
var argument = ctx[CpuRegister.Rcx];
|
||||||
var nameAddress = ctx[CpuRegister.R8];
|
|
||||||
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
||||||
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
||||||
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
||||||
@@ -278,14 +257,20 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_create",
|
ExportName = "pthread_create",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
|
public static int PosixPthreadCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreateCore(ctx, nameAddress: 0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Jmi+9w9u0E4",
|
Nid = "Jmi+9w9u0E4",
|
||||||
ExportName = "pthread_create_name_np",
|
ExportName = "pthread_create_name_np",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
public static int PosixPthreadCreateNameNp(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3kg7rT0NQIs",
|
Nid = "3kg7rT0NQIs",
|
||||||
@@ -295,6 +280,9 @@ public static class KernelExports
|
|||||||
public static int PthreadExit(CpuContext ctx)
|
public static int PthreadExit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = ctx[CpuRegister.Rdi];
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
// Run cleanup on the still-executable thread before unwinding it.
|
||||||
|
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
|
||||||
|
KernelMemoryCompatExports.RunThreadDtors(ctx);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
||||||
ctx[CpuRegister.Rax] = value;
|
ctx[CpuRegister.Rax] = value;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -308,6 +296,8 @@ public static class KernelExports
|
|||||||
public static int PosixPthreadExit(CpuContext ctx)
|
public static int PosixPthreadExit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = ctx[CpuRegister.Rdi];
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
|
||||||
|
KernelMemoryCompatExports.RunThreadDtors(ctx);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
||||||
ctx[CpuRegister.Rax] = value;
|
ctx[CpuRegister.Rax] = value;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -362,7 +352,10 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_join",
|
ExportName = "pthread_join",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
public static int PosixPthreadJoin(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadJoin(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "wuCroIGjt2g",
|
Nid = "wuCroIGjt2g",
|
||||||
@@ -437,14 +430,21 @@ public static class KernelExports
|
|||||||
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
|
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
|
||||||
{
|
{
|
||||||
Span<byte> buf = stackalloc byte[maxLen];
|
Span<byte> buf = stackalloc byte[maxLen];
|
||||||
if (!ctx.Memory.TryRead(address, buf))
|
Span<byte> one = stackalloc byte[1];
|
||||||
return $"<unreadable 0x{address:X16}>";
|
var len = 0;
|
||||||
|
while (len < buf.Length)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)len, one))
|
||||||
|
return len == 0 ? $"<unreadable 0x{address:X16}>" : System.Text.Encoding.UTF8.GetString(buf[..len]);
|
||||||
|
|
||||||
int len = 0;
|
if (one[0] == 0)
|
||||||
while (len < buf.Length && buf[len] != 0) len++;
|
break;
|
||||||
|
|
||||||
try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); }
|
buf[len++] = one[0];
|
||||||
catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); }
|
}
|
||||||
|
|
||||||
|
try { return System.Text.Encoding.UTF8.GetString(buf[..len]); }
|
||||||
|
catch { return System.Text.Encoding.ASCII.GetString(buf[..len]); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldTracePthread()
|
private static bool ShouldTracePthread()
|
||||||
@@ -452,19 +452,18 @@ public static class KernelExports
|
|||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
[SysAbiExport(
|
||||||
|
Nid = "L1SBTkC+Cvw",
|
||||||
|
ExportName = "abort",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Abort(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
||||||
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
|
||||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
|
||||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
|
||||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
|
||||||
// shadowing them breaks every C++ static-init guard in the game.
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,662 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Positional and scatter/gather file I/O, synchronization, renaming, fcntl,
|
||||||
|
/// polling and asynchronous I/O (AIO) that complement the base file exports.
|
||||||
|
/// These share the same guest FD table and path-resolution helpers via the
|
||||||
|
/// partial <see cref="KernelMemoryCompatExports"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public static partial class KernelMemoryCompatExports
|
||||||
|
{
|
||||||
|
// fcntl commands (FreeBSD numbering used by the PS4/PS5 libc).
|
||||||
|
private const int F_DUPFD = 0;
|
||||||
|
private const int F_GETFD = 1;
|
||||||
|
private const int F_SETFD = 2;
|
||||||
|
private const int F_GETFL = 3;
|
||||||
|
private const int F_SETFL = 4;
|
||||||
|
|
||||||
|
// poll event bits.
|
||||||
|
private const short POLLIN = 0x0001;
|
||||||
|
private const short POLLOUT = 0x0004;
|
||||||
|
|
||||||
|
// AIO request state (SCE_KERNEL_AIO_STATE_*).
|
||||||
|
private const uint AioStateCompleted = 3;
|
||||||
|
private const int SizeofAioRwRequest = 40;
|
||||||
|
|
||||||
|
private static long _nextAioSubmitId = 1;
|
||||||
|
private static readonly ConcurrentDictionary<uint, int> _aioResults = new();
|
||||||
|
|
||||||
|
private static FileStream? GetOpenFile(int fd)
|
||||||
|
{
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
return _openFiles.TryGetValue(fd, out var stream) ? stream : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Positional read/write (do not move the file offset) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelPread(CpuContext ctx) => KernelPreadCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelPreadCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||||
|
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||||
|
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requested == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||||
|
int read;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
read = RandomAccess.Read(stream.SafeFileHandle, buffer, offset);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelPwriteCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||||
|
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||||
|
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requested == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = GC.AllocateUninitializedArray<byte>(requested);
|
||||||
|
if (!ctx.Memory.TryRead(bufferAddress, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RandomAccess.Write(stream.SafeFileHandle, payload, offset);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Synchronization ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFsync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelFsyncCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
if (fd is 0 or 1 or 2)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.Flush(flushToDisk: true);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "Y2OqwJQ3lr8", ExportName = "sync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixSync(CpuContext ctx)
|
||||||
|
{
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
foreach (var stream in _openFiles.Values)
|
||||||
|
{
|
||||||
|
try { stream.Flush(flushToDisk: true); } catch (IOException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Truncation ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelFtruncateCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var length = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (length < 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.SetLength(length);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelTruncateCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var length = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (length < 0 || !TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||||
|
stream.SetLength(length);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Rename ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelRename(CpuContext ctx) => KernelRenameCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelRenameCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rdi], MaxGuestStringLength, out var fromGuest) ||
|
||||||
|
!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rsi], MaxGuestStringLength, out var toGuest))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReadOnlyGuestMutationPath(fromGuest) || IsReadOnlyGuestMutationPath(toGuest))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromHost = ResolveGuestPath(fromGuest);
|
||||||
|
var toHost = ResolveGuestPath(toGuest);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(fromHost))
|
||||||
|
{
|
||||||
|
Directory.Move(fromHost, toHost);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(fromHost, toHost, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateNegativeStatCacheForPathAndAncestors(toGuest);
|
||||||
|
AddNegativeStatCacheForGuestPath(fromGuest);
|
||||||
|
InvalidateAprFileSizeCache(fromHost);
|
||||||
|
InvalidateAprFileSizeCache(toHost);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Descriptor duplication ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "iiQjzvfWDq0", ExportName = "dup",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixDup(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
if (!_openFiles.TryGetValue(fd, out var stream))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSIX dup shares the open file description (and offset), which is
|
||||||
|
// exactly the shared FileStream reference.
|
||||||
|
var newFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||||
|
_openFiles[newFd] = stream;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "wdUufa9g-D8", ExportName = "dup2",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixDup2(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var oldFd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var newFd = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
if (!_openFiles.TryGetValue(oldFd, out var stream))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFd == newFd)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If newFd names an open file, dup2 closes it first.
|
||||||
|
if (_openFiles.TryGetValue(newFd, out var existing) && !ReferenceEquals(existing, stream))
|
||||||
|
{
|
||||||
|
try { existing.Dispose(); } catch (IOException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
_openFiles[newFd] = stream;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- fcntl ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelFcntlCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var command = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var argument = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case F_DUPFD:
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
if (!_openFiles.TryGetValue(fd, out var stream))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newFd = Math.Max((int)Interlocked.Increment(ref _nextFileDescriptor), argument);
|
||||||
|
_openFiles[newFd] = stream;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
case F_GETFD:
|
||||||
|
case F_GETFL:
|
||||||
|
// No close-on-exec / status flags are tracked; report cleared.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
case F_SETFD:
|
||||||
|
case F_SETFL:
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
default:
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- poll / select ----
|
||||||
|
// Regular files are always ready for both read and write, so report every
|
||||||
|
// requested descriptor as immediately ready.
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ku7D4q1Y9PI", ExportName = "poll",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixPoll(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fdsAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var count = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
if (fdsAddress == 0 || count == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// struct pollfd { int fd; short events; short revents; } == 8 bytes.
|
||||||
|
var ready = 0;
|
||||||
|
Span<byte> buffer = stackalloc byte[8];
|
||||||
|
for (uint i = 0; i < count && i < 4096; i++)
|
||||||
|
{
|
||||||
|
var entry = fdsAddress + i * 8;
|
||||||
|
if (!ctx.Memory.TryRead(entry, buffer))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = BinaryPrimitives.ReadInt16LittleEndian(buffer[4..]);
|
||||||
|
var revents = (short)(events & (POLLIN | POLLOUT));
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(buffer[6..], revents);
|
||||||
|
if (revents != 0)
|
||||||
|
{
|
||||||
|
ready++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.Memory.TryWrite(entry, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)ready);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "T8fER+tIGgk", ExportName = "select",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixSelect(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// nfds in Rdi; the fd_sets are left as-is (all reported ready) and the
|
||||||
|
// ready count returned is nfds so callers proceed without blocking.
|
||||||
|
var nfds = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)Math.Max(nfds, 0));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Asynchronous I/O (executed synchronously) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "HgX7+AORI58", ExportName = "sceKernelAioSubmitReadCommands",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioSubmitReadCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "lXT0m3P-vs4", ExportName = "sceKernelAioSubmitReadCommandsMultiple",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioSubmitReadCommandsMultiple(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "XQ8C8y+de+E", ExportName = "sceKernelAioSubmitWriteCommands",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioSubmitWriteCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: true);
|
||||||
|
|
||||||
|
private static int KernelAioSubmit(CpuContext ctx, bool write)
|
||||||
|
{
|
||||||
|
var requestsAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var outIdAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (requestsAddress == 0 || count <= 0 || count > 0x10000)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each SceKernelAioRWRequest: offset(8) nbyte(8) buf(8) result(8) fd(4).
|
||||||
|
Span<byte> request = stackalloc byte[SizeofAioRwRequest];
|
||||||
|
Span<byte> result = stackalloc byte[16];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var entry = requestsAddress + (ulong)(i * SizeofAioRwRequest);
|
||||||
|
if (!ctx.Memory.TryRead(entry, request))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = BinaryPrimitives.ReadInt64LittleEndian(request[0..]);
|
||||||
|
var nbyte = (int)Math.Min(BinaryPrimitives.ReadUInt64LittleEndian(request[8..]), int.MaxValue);
|
||||||
|
var buf = BinaryPrimitives.ReadUInt64LittleEndian(request[16..]);
|
||||||
|
var resultPtr = BinaryPrimitives.ReadUInt64LittleEndian(request[24..]);
|
||||||
|
var fd = BinaryPrimitives.ReadInt32LittleEndian(request[32..]);
|
||||||
|
|
||||||
|
long transferred = KernelAioTransfer(ctx, fd, offset, nbyte, buf, write);
|
||||||
|
if (resultPtr != 0)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(result[0..], transferred);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(result[8..], AioStateCompleted);
|
||||||
|
_ = ctx.Memory.TryWrite(resultPtr, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var submitId = unchecked((uint)Interlocked.Increment(ref _nextAioSubmitId));
|
||||||
|
_aioResults[submitId] = 0;
|
||||||
|
if (outIdAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> idBuffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(idBuffer, submitId);
|
||||||
|
_ = ctx.Memory.TryWrite(outIdAddress, idBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long KernelAioTransfer(CpuContext ctx, int fd, long offset, int nbyte, ulong buf, bool write)
|
||||||
|
{
|
||||||
|
if (nbyte <= 0 || buf == 0 || offset < 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scratch = GC.AllocateUninitializedArray<byte>(nbyte);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(buf, scratch))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccess.Write(stream.SafeFileHandle, scratch, offset);
|
||||||
|
return nbyte;
|
||||||
|
}
|
||||||
|
|
||||||
|
var read = RandomAccess.Read(stream.SafeFileHandle, scratch, offset);
|
||||||
|
if (read > 0 && !ctx.Memory.TryWrite(buf, scratch.AsSpan(0, read)))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "o7O4z3jwKzo", ExportName = "sceKernelAioPollRequests",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioPollRequests(CpuContext ctx) => KernelAioComplete(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "lgK+oIWkJyA", ExportName = "sceKernelAioWaitRequests",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioWaitRequests(CpuContext ctx) => KernelAioComplete(ctx);
|
||||||
|
|
||||||
|
private static int KernelAioComplete(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Submission already performed the I/O synchronously, so every request
|
||||||
|
// reports completed. Rsi points at the state-out array, Rdx = count.
|
||||||
|
var statesAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (statesAddress != 0 && count > 0 && count <= 0x10000)
|
||||||
|
{
|
||||||
|
Span<byte> state = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
_ = ctx.Memory.TryWrite(statesAddress + (ulong)(i * sizeof(uint)), state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "fR521KIGgb8", ExportName = "sceKernelAioCancelRequest",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioCancelRequest(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Already completed; report the completed state if an out pointer given.
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (stateAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> state = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
|
||||||
|
_ = ctx.Memory.TryWrite(stateAddress, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "5TgME6AYty4", ExportName = "sceKernelAioDeleteRequest",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioDeleteRequest(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var submitId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
_aioResults.TryRemove(submitId, out _);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,16 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelModuleRegistry
|
public static class KernelModuleRegistry
|
||||||
{
|
{
|
||||||
|
public enum ModuleStartState
|
||||||
|
{
|
||||||
|
NotStarted,
|
||||||
|
Starting,
|
||||||
|
Started,
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
|
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
|
||||||
|
private static readonly Dictionary<int, Dictionary<string, ulong>> _symbolsByHandle = new();
|
||||||
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
|
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
|
||||||
@@ -32,6 +40,11 @@ public static class KernelModuleRegistry
|
|||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
ulong EndAddress,
|
ulong EndAddress,
|
||||||
ulong EntryPoint,
|
ulong EntryPoint,
|
||||||
|
ulong InitEntryPoint,
|
||||||
|
ulong EhFrameHeaderAddress,
|
||||||
|
ulong EhFrameAddress,
|
||||||
|
ulong EhFrameSize,
|
||||||
|
ModuleStartState StartState,
|
||||||
bool IsMain,
|
bool IsMain,
|
||||||
bool IsSystemModule);
|
bool IsSystemModule);
|
||||||
|
|
||||||
@@ -40,6 +53,7 @@ public static class KernelModuleRegistry
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_modulesByHandle.Clear();
|
_modulesByHandle.Clear();
|
||||||
|
_symbolsByHandle.Clear();
|
||||||
_handleByPath.Clear();
|
_handleByPath.Clear();
|
||||||
_handleByName.Clear();
|
_handleByName.Clear();
|
||||||
_sysmoduleHandleById.Clear();
|
_sysmoduleHandleById.Clear();
|
||||||
@@ -52,6 +66,10 @@ public static class KernelModuleRegistry
|
|||||||
ulong baseAddress,
|
ulong baseAddress,
|
||||||
ulong size,
|
ulong size,
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
|
ulong initEntryPoint,
|
||||||
|
ulong ehFrameHeaderAddress,
|
||||||
|
ulong ehFrameAddress,
|
||||||
|
ulong ehFrameSize,
|
||||||
bool isMain,
|
bool isMain,
|
||||||
bool isSystemModule = false)
|
bool isSystemModule = false)
|
||||||
{
|
{
|
||||||
@@ -67,6 +85,10 @@ public static class KernelModuleRegistry
|
|||||||
BaseAddress = baseAddress,
|
BaseAddress = baseAddress,
|
||||||
EndAddress = ComputeEnd(baseAddress, size),
|
EndAddress = ComputeEnd(baseAddress, size),
|
||||||
EntryPoint = entryPoint,
|
EntryPoint = entryPoint,
|
||||||
|
InitEntryPoint = initEntryPoint,
|
||||||
|
EhFrameHeaderAddress = ehFrameHeaderAddress,
|
||||||
|
EhFrameAddress = ehFrameAddress,
|
||||||
|
EhFrameSize = ehFrameSize,
|
||||||
IsMain = existing.IsMain || isMain,
|
IsMain = existing.IsMain || isMain,
|
||||||
IsSystemModule = existing.IsSystemModule || isSystemModule,
|
IsSystemModule = existing.IsSystemModule || isSystemModule,
|
||||||
};
|
};
|
||||||
@@ -84,6 +106,11 @@ public static class KernelModuleRegistry
|
|||||||
BaseAddress: baseAddress,
|
BaseAddress: baseAddress,
|
||||||
EndAddress: ComputeEnd(baseAddress, size),
|
EndAddress: ComputeEnd(baseAddress, size),
|
||||||
EntryPoint: entryPoint,
|
EntryPoint: entryPoint,
|
||||||
|
InitEntryPoint: initEntryPoint,
|
||||||
|
EhFrameHeaderAddress: ehFrameHeaderAddress,
|
||||||
|
EhFrameAddress: ehFrameAddress,
|
||||||
|
EhFrameSize: ehFrameSize,
|
||||||
|
StartState: ModuleStartState.NotStarted,
|
||||||
IsMain: isMain,
|
IsMain: isMain,
|
||||||
IsSystemModule: isSystemModule);
|
IsSystemModule: isSystemModule);
|
||||||
_modulesByHandle[handle] = entry;
|
_modulesByHandle[handle] = entry;
|
||||||
@@ -119,6 +146,11 @@ public static class KernelModuleRegistry
|
|||||||
BaseAddress: 0,
|
BaseAddress: 0,
|
||||||
EndAddress: 0,
|
EndAddress: 0,
|
||||||
EntryPoint: 0,
|
EntryPoint: 0,
|
||||||
|
InitEntryPoint: 0,
|
||||||
|
EhFrameHeaderAddress: 0,
|
||||||
|
EhFrameAddress: 0,
|
||||||
|
EhFrameSize: 0,
|
||||||
|
StartState: ModuleStartState.Started,
|
||||||
IsMain: false,
|
IsMain: false,
|
||||||
IsSystemModule: isSystemModule);
|
IsSystemModule: isSystemModule);
|
||||||
_modulesByHandle[handle] = entry;
|
_modulesByHandle[handle] = entry;
|
||||||
@@ -203,6 +235,97 @@ public static class KernelModuleRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically claims a module initializer. A module can be observed while
|
||||||
|
/// it is starting (for recursive loader calls), but its DT_INIT routine is
|
||||||
|
/// executed at most once after a successful start.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryBeginModuleStart(int handle, out ModuleEntry module)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modulesByHandle.TryGetValue(handle, out module))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.StartState != ModuleStartState.NotStarted)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.InitEntryPoint < 0x10000)
|
||||||
|
{
|
||||||
|
module = module with { StartState = ModuleStartState.Started };
|
||||||
|
_modulesByHandle[handle] = module;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module = module with { StartState = ModuleStartState.Starting };
|
||||||
|
_modulesByHandle[handle] = module;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CompleteModuleStart(int handle, bool succeeded)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modulesByHandle.TryGetValue(handle, out var module) ||
|
||||||
|
module.StartState != ModuleStartState.Starting)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_modulesByHandle[handle] = module with
|
||||||
|
{
|
||||||
|
StartState = succeeded ? ModuleStartState.Started : ModuleStartState.NotStarted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterModuleSymbols(int handle, IReadOnlyDictionary<string, ulong> symbols)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(symbols);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modulesByHandle.ContainsKey(handle))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_symbolsByHandle.TryGetValue(handle, out var destination))
|
||||||
|
{
|
||||||
|
destination = new Dictionary<string, ulong>(StringComparer.Ordinal);
|
||||||
|
_symbolsByHandle[handle] = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (name, address) in symbols)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(name) && address >= 0x10000)
|
||||||
|
{
|
||||||
|
destination.TryAdd(name, address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryResolveModuleSymbol(int handle, string symbolName, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(symbolName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _symbolsByHandle.TryGetValue(handle, out var symbols) &&
|
||||||
|
symbols.TryGetValue(symbolName, out address) &&
|
||||||
|
address >= 0x10000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
|
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
|
||||||
{
|
{
|
||||||
module = default;
|
module = default;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,10 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -15,6 +17,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private const int DefaultDetachState = 0;
|
private const int DefaultDetachState = 0;
|
||||||
private const ulong DefaultGuardSize = 0x1000UL;
|
private const ulong DefaultGuardSize = 0x1000UL;
|
||||||
private const ulong DefaultStackSize = 0x1_00000UL;
|
private const ulong DefaultStackSize = 0x1_00000UL;
|
||||||
|
private const ulong NativeGuestStackSize = 0x20_0000UL;
|
||||||
|
private const ulong NativeGuestStackStride = 0x100_0000UL;
|
||||||
private const int DefaultInheritSched = 4;
|
private const int DefaultInheritSched = 4;
|
||||||
private const int DefaultSchedPolicy = 1;
|
private const int DefaultSchedPolicy = 1;
|
||||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||||
@@ -223,6 +227,13 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+U1R4WtXvoc",
|
||||||
|
ExportName = "pthread_detach",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadDetach(CpuContext ctx) => PthreadDetach(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "How7B8Oet6k",
|
Nid = "How7B8Oet6k",
|
||||||
ExportName = "scePthreadGetname",
|
ExportName = "scePthreadGetname",
|
||||||
@@ -273,6 +284,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state.Attributes = state.Attributes with { AffinityMask = mask };
|
state.Attributes = state.Attributes with { AffinityMask = mask };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadAffinity(thread, mask);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -326,7 +338,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -354,6 +366,9 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
GetOrCreateThreadStateLocked(thread).Priority = priority;
|
GetOrCreateThreadStateLocked(thread).Priority = priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply to the live scheduler thread so runtime priority changes take
|
||||||
|
// effect, not just the local bookkeeping snapshot.
|
||||||
|
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadPriority(thread, priority);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -373,7 +388,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -408,9 +423,9 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
public static int PthreadGetschedparam(CpuContext ctx)
|
public static int PthreadGetschedparam(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thread = ctx[CpuRegister.Rdi];
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
var outPolicyAddress = ctx[CpuRegister.Rsi];
|
var policyAddress = ctx[CpuRegister.Rsi];
|
||||||
var outSchedParamAddress = ctx[CpuRegister.Rdx];
|
var schedParamAddress = ctx[CpuRegister.Rdx];
|
||||||
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
|
if (thread == 0 || policyAddress == 0 || schedParamAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
@@ -424,8 +439,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
priority = state.Priority;
|
priority = state.Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
|
if (!TryWriteInt32(ctx, policyAddress, policy) ||
|
||||||
!ctx.TryWriteInt32(outSchedParamAddress, priority))
|
!TryWriteInt32(ctx, schedParamAddress, priority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -528,6 +543,22 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
var threadState = GetOrCreateThreadStateLocked(thread);
|
var threadState = GetOrCreateThreadStateLocked(thread);
|
||||||
|
|
||||||
|
// The native executor maps guest pthread stacks itself, after the
|
||||||
|
// kernel-facing thread object has been created. Report that live
|
||||||
|
// mapping when a thread asks for its own attributes. IL2CPP's
|
||||||
|
// conservative collector uses these two fields to register the stack;
|
||||||
|
// returning the default null address lets it recycle objects that are
|
||||||
|
// still reachable only from guest registers/stack frames.
|
||||||
|
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
|
||||||
|
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out var stackAddress))
|
||||||
|
{
|
||||||
|
threadState.Attributes = threadState.Attributes with
|
||||||
|
{
|
||||||
|
StackAddress = stackAddress,
|
||||||
|
StackSize = NativeGuestStackSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
_attrStates[outAttrAddress] = threadState.Attributes;
|
_attrStates[outAttrAddress] = threadState.Attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +566,28 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress)
|
||||||
|
{
|
||||||
|
stackAddress = 0;
|
||||||
|
var candidate = stackPointer & ~(NativeGuestStackStride - 1);
|
||||||
|
if (stackPointer - candidate >= NativeGuestStackSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var highestStack = OperatingSystem.IsWindows()
|
||||||
|
? 0x00007FFF_F000_0000UL
|
||||||
|
: 0x00006FFF_F000_0000UL;
|
||||||
|
var lowestStack = highestStack - (63 * NativeGuestStackStride);
|
||||||
|
if (candidate < lowestStack || candidate > highestStack)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stackAddress = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "8+s5BzZjxSg",
|
Nid = "8+s5BzZjxSg",
|
||||||
ExportName = "scePthreadAttrGetaffinity",
|
ExportName = "scePthreadAttrGetaffinity",
|
||||||
@@ -584,7 +637,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -827,7 +880,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -850,7 +903,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1207,7 +1260,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outKeyAddress, key))
|
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1315,6 +1368,72 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int OrbisPthreadGetspecific(CpuContext ctx) => PosixPthreadGetspecific(ctx);
|
public static int OrbisPthreadGetspecific(CpuContext ctx) => PosixPthreadGetspecific(ctx);
|
||||||
|
|
||||||
|
private const int PthreadDestructorIterations = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the current thread's pthread TLS-key destructors, as POSIX
|
||||||
|
/// requires on thread exit. Each key holding a non-null value with a
|
||||||
|
/// registered destructor has its value cleared first and the destructor
|
||||||
|
/// then invoked with the previous value; this repeats up to
|
||||||
|
/// PTHREAD_DESTRUCTOR_ITERATIONS times so destructors that set new
|
||||||
|
/// thread-local values are themselves cleaned up. Called on the exiting
|
||||||
|
/// guest thread while it is still executable.
|
||||||
|
/// </summary>
|
||||||
|
public static void RunThreadLocalDestructors(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (scheduler is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var threadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
if (!_threadLocalSpecific.TryGetValue(threadHandle, out var values))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var iteration = 0; iteration < PthreadDestructorIterations; iteration++)
|
||||||
|
{
|
||||||
|
var ranAny = false;
|
||||||
|
foreach (var entry in values)
|
||||||
|
{
|
||||||
|
var value = entry.Value;
|
||||||
|
if (value == 0 ||
|
||||||
|
!_tlsKeys.TryGetValue(entry.Key, out var keyState) ||
|
||||||
|
keyState.Destructor == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear before invoking, per POSIX, so a destructor that
|
||||||
|
// re-sets the key is handled on the next iteration.
|
||||||
|
if (!values.TryUpdate(entry.Key, 0, value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ranAny = true;
|
||||||
|
_ = scheduler.TryCallGuestFunction(
|
||||||
|
ctx,
|
||||||
|
keyState.Destructor,
|
||||||
|
value,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"pthread_tls_destructor",
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ranAny)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadLocalSpecific.TryRemove(threadHandle, out _);
|
||||||
|
}
|
||||||
|
|
||||||
private static int PthreadRwlockLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
private static int PthreadRwlockLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
||||||
{
|
{
|
||||||
if (rwlockAddress == 0)
|
if (rwlockAddress == 0)
|
||||||
@@ -1680,4 +1799,24 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
payload[^1] = 0;
|
payload[^1] = 0;
|
||||||
return ctx.Memory.TryWrite(address, payload);
|
return ctx.Memory.TryWrite(address, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
// 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 System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -13,9 +14,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||||
private static int _nextSemaphoreHandle = 1;
|
private static int _nextSemaphoreHandle = 1;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static int _semaPollBackoffCount;
|
|
||||||
|
|
||||||
private sealed class KernelSemaphoreState
|
private sealed class KernelSemaphoreState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
@@ -25,35 +23,9 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public int WaitingThreads { get; set; }
|
public int WaitingThreads { get; set; }
|
||||||
public int CancelEpoch { get; set; }
|
|
||||||
public bool Deleted { get; set; }
|
|
||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
|
|
||||||
{
|
|
||||||
public required KernelSemaphoreState Semaphore { get; init; }
|
|
||||||
public required int NeedCount { get; init; }
|
|
||||||
public required int CancelEpochAtBlock { get; init; }
|
|
||||||
public bool Timed { get; init; }
|
|
||||||
|
|
||||||
// Timed-wait completion state; unused when Timed is false.
|
|
||||||
public CpuContext? Ctx { get; init; }
|
|
||||||
public ulong TimeoutAddress { get; init; }
|
|
||||||
public long DeadlineTimestamp { get; init; }
|
|
||||||
|
|
||||||
// Written and read only under the owning semaphore's Gate.
|
|
||||||
public int? Result { get; set; }
|
|
||||||
|
|
||||||
public int Resume() => Timed
|
|
||||||
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
|
|
||||||
: CompleteBlockedSemaWait(Semaphore, this);
|
|
||||||
|
|
||||||
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "188x57JYp0g",
|
Nid = "188x57JYp0g",
|
||||||
ExportName = "sceKernelCreateSema",
|
ExportName = "sceKernelCreateSema",
|
||||||
@@ -76,12 +48,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
initialCount > maxCount ||
|
initialCount > maxCount ||
|
||||||
optionAddress != 0)
|
optionAddress != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
@@ -90,7 +62,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = new KernelSemaphoreState
|
_semaphores[handle] = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
WakeKey = GetSemaphoreWakeKey(handle),
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
@@ -98,28 +70,15 @@ public static class KernelSemaphoreCompatExports
|
|||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
};
|
};
|
||||||
_semaphores[handle] = state;
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
{
|
{
|
||||||
_semaphores.TryRemove(handle, out _);
|
_semaphores.TryRemove(handle, out _);
|
||||||
// Handles are sequential and guest-predictable, so a hostile guest can
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
// race a WaitSema onto the handle between publication above and this
|
|
||||||
// rollback. Strand-proof that waiter exactly like DeleteSema does.
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
state.Deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceSema)
|
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||||
{
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -135,125 +94,144 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint timeoutUsec = 0;
|
||||||
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pollTimedOut = false;
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count >= needCount)
|
if (semaphore.Count >= needCount)
|
||||||
{
|
{
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
if (_traceSema)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
|
||||||
}
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
|
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
semaphore.WaitingThreads++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block cooperatively: the wake predicate atomically acquires the
|
||||||
|
// tokens (so a wake commits the acquisition), while the resume
|
||||||
|
// handler distinguishes a real acquisition from a deadline expiry.
|
||||||
|
var acquired = false;
|
||||||
|
var deadline = timeoutAddress != 0
|
||||||
|
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
bool WakePredicate()
|
||||||
|
{
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (semaphore.Count >= needCount)
|
||||||
|
{
|
||||||
|
semaphore.Count -= needCount;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
acquired = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ResumeWait()
|
||||||
|
{
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeoutMicros == 0)
|
|
||||||
{
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
pollTimedOut = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
|
||||||
var timedWaiter = new SemaphoreWaiter
|
|
||||||
{
|
|
||||||
Semaphore = semaphore,
|
|
||||||
NeedCount = needCount,
|
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
|
||||||
Timed = true,
|
|
||||||
Ctx = ctx,
|
|
||||||
TimeoutAddress = timeoutAddress,
|
|
||||||
DeadlineTimestamp = deadline,
|
|
||||||
};
|
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelWaitSema",
|
|
||||||
semaphore.WakeKey,
|
|
||||||
timedWaiter,
|
|
||||||
blockDeadlineTimestamp: deadline))
|
|
||||||
{
|
|
||||||
semaphore.WaitingThreads++;
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
|
||||||
// immediate-timeout poll the callers already tolerate.
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
pollTimedOut = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pollTimedOut)
|
if (acquired)
|
||||||
{
|
{
|
||||||
var waiter = new SemaphoreWaiter
|
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
Semaphore = semaphore,
|
|
||||||
NeedCount = needCount,
|
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
|
||||||
};
|
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelWaitSema",
|
|
||||||
semaphore.WakeKey,
|
|
||||||
waiter))
|
|
||||||
{
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
if ((++_semaPollBackoffCount & 255) == 0)
|
ctx,
|
||||||
|
"sceKernelWaitSema",
|
||||||
|
GetSemaphoreWakeKey(handle),
|
||||||
|
ResumeWait,
|
||||||
|
WakePredicate,
|
||||||
|
deadline))
|
||||||
{
|
{
|
||||||
Thread.Sleep(0);
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||||
}
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
else
|
|
||||||
{
|
|
||||||
Thread.Yield();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
// Not a guest thread (or no scheduler): fall back to a host-thread
|
||||||
|
// wait so the semantics still hold on non-cooperative callers.
|
||||||
|
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int WaitSemaphoreOnHostThread(
|
||||||
|
CpuContext ctx,
|
||||||
|
KernelSemaphoreState semaphore,
|
||||||
|
uint handle,
|
||||||
|
int needCount,
|
||||||
|
ulong timeoutAddress,
|
||||||
|
uint timeoutUsec)
|
||||||
|
{
|
||||||
|
var deadlineMs = timeoutAddress != 0
|
||||||
|
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
|
||||||
|
: long.MaxValue;
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
TraceSemaphore(
|
||||||
|
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
|
||||||
|
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
|
||||||
|
while (semaphore.Count < needCount)
|
||||||
|
{
|
||||||
|
var remaining = deadlineMs - Environment.TickCount64;
|
||||||
|
if (timeoutAddress != 0 && remaining <= 0)
|
||||||
|
{
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.Count -= needCount;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
TraceSemaphore(
|
||||||
|
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
|
||||||
|
if (timeoutAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "12wOHk8ywb0",
|
Nid = "12wOHk8ywb0",
|
||||||
ExportName = "sceKernelPollSema",
|
ExportName = "sceKernelPollSema",
|
||||||
@@ -263,31 +241,25 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count < needCount)
|
if (semaphore.Count < needCount)
|
||||||
{
|
{
|
||||||
if (_traceSema)
|
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
{
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
if (_traceSema)
|
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
{
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,34 +272,31 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signalCount <= 0)
|
if (signalCount <= 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count += signalCount;
|
semaphore.Count += signalCount;
|
||||||
if (_traceSema)
|
// Wake host-thread waiters parked in the fallback path.
|
||||||
{
|
Monitor.PulseAll(semaphore.Gate);
|
||||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
// Wake cooperatively-blocked guest threads; their wake predicate
|
||||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
// acquires the tokens atomically, so this respects the new count.
|
||||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
// waiter can proceed.
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -339,35 +308,29 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setCount > semaphore.MaxCount)
|
if (setCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||||
semaphore.CancelEpoch++;
|
semaphore.WaitingThreads = 0;
|
||||||
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
|
Monitor.PulseAll(semaphore.Gate);
|
||||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||||
// and silently absorb the increment of a waiter that parks between this
|
|
||||||
// gate release and the wake-all below.
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -380,138 +343,238 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete succeeds even with blocked waiters; they wake with the deleted
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
// result (the SCE kernel wakes waiters with the EACCES-class code).
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
lock (semaphore.Gate)
|
|
||||||
{
|
|
||||||
semaphore.Deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
|
[SysAbiExport(
|
||||||
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
|
Nid = "pDuPEf3m4fI",
|
||||||
// result and should be re-readied; false leaves it parked.
|
ExportName = "sem_init",
|
||||||
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemInit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
lock (semaphore.Gate)
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var initialCountValue = ctx[CpuRegister.Rdx];
|
||||||
|
if (semaphoreAddress == 0 || initialCountValue > int.MaxValue)
|
||||||
{
|
{
|
||||||
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialCount = unchecked((int)initialCountValue);
|
||||||
|
_semaphores[handle] = new KernelSemaphoreState
|
||||||
|
{
|
||||||
|
Name = $"posix@0x{semaphoreAddress:X16}",
|
||||||
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
|
InitialCount = initialCount,
|
||||||
|
MaxCount = int.MaxValue,
|
||||||
|
Count = initialCount,
|
||||||
|
};
|
||||||
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
|
{
|
||||||
|
_semaphores.TryRemove(handle, out _);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSemaphore($"posix-init address=0x{semaphoreAddress:X16} handle=0x{handle:X8} count={initialCount}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
[SysAbiExport(
|
||||||
|
Nid = "YCV5dGGBcCo",
|
||||||
|
ExportName = "sem_wait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemWait(CpuContext ctx)
|
||||||
{
|
{
|
||||||
if (waiter.Result is not null)
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
{
|
{
|
||||||
return true;
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (semaphore.Deleted)
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
{
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
ctx[CpuRegister.Rdx] = 0;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
return KernelWaitSema(ctx);
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
|
|
||||||
{
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (semaphore.Count >= waiter.NeedCount)
|
|
||||||
{
|
|
||||||
semaphore.Count -= waiter.NeedCount;
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume handler: runs on the woken guest thread outside the scheduler gate;
|
[SysAbiExport(
|
||||||
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
|
Nid = "WBWzsRifCEA",
|
||||||
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
ExportName = "sem_trywait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemTryWait(CpuContext ctx)
|
||||||
{
|
{
|
||||||
lock (semaphore.Gate)
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
{
|
{
|
||||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
{
|
|
||||||
// Nothing readies a parked semaphore waiter without the wake handler
|
|
||||||
// resolving it, so reaching here means the scheduler contract changed.
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return waiter.Result!.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
return KernelPollSema(ctx, handle, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CompleteBlockedTimedSemaWait(
|
[SysAbiExport(
|
||||||
CpuContext ctx,
|
Nid = "w5IHyvahg-o",
|
||||||
KernelSemaphoreState semaphore,
|
ExportName = "sem_timedwait",
|
||||||
SemaphoreWaiter waiter,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
ulong timeoutAddress,
|
LibraryName = "libKernel")]
|
||||||
long deadlineTimestamp)
|
public static int PosixSemTimedWait(CpuContext ctx)
|
||||||
{
|
{
|
||||||
int result;
|
var timeoutAddress = ctx[CpuRegister.Rsi];
|
||||||
lock (semaphore.Gate)
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
{
|
{
|
||||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
{
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = waiter.Result!.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
ctx[CpuRegister.Rdx] = timeoutAddress;
|
||||||
|
return KernelWaitSema(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IKP8typ0QUk",
|
||||||
|
ExportName = "sem_post",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemPost(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
return KernelSignalSema(ctx, handle, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Bq+LRV-N6Hk",
|
||||||
|
ExportName = "sem_getvalue",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemGetValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (valueAddress == 0 ||
|
||||||
|
!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle) ||
|
||||||
|
!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int count;
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
count = semaphore.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryWriteUInt32(ctx, valueAddress, unchecked((uint)count))
|
||||||
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cDW233RAwWo",
|
||||||
|
ExportName = "sem_destroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
var result = KernelDeleteSema(ctx);
|
||||||
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
_ = TryWriteUInt32(ctx, semaphoreAddress, 0);
|
||||||
var remainingMicros = remainingTicks <= 0
|
|
||||||
? 0u
|
|
||||||
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
||||||
|
{
|
||||||
|
handle = 0;
|
||||||
|
return semaphoreAddress != 0 &&
|
||||||
|
TryReadUInt32(ctx, semaphoreAddress, out handle) &&
|
||||||
|
handle != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0 || maxLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[i] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Call sites must check this before building the interpolated message; the trace
|
// Call sites must check this before building the interpolated message; the trace
|
||||||
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
||||||
private static readonly bool _traceSema =
|
private static readonly bool _traceSema =
|
||||||
@@ -521,4 +584,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FormatCallSite(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnAddress);
|
||||||
|
return $"guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{returnAddress:X16}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ public static class LibcStdioExports
|
|||||||
private const int MaxPathLength = 4096;
|
private const int MaxPathLength = 4096;
|
||||||
private const int MaxModeLength = 16;
|
private const int MaxModeLength = 16;
|
||||||
private const int ReadChunkSize = 1024 * 1024;
|
private const int ReadChunkSize = 1024 * 1024;
|
||||||
|
private const ulong GuestFileObjectSize = 0x100;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
||||||
private static long _nextHandle = 0x1000 - 8;
|
|
||||||
|
|
||||||
private static readonly bool _traceStdio =
|
private static readonly bool _traceStdio =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
||||||
@@ -96,7 +96,21 @@ public static class LibcStdioExports
|
|||||||
stream.Seek(0, SeekOrigin.End);
|
stream.Seek(0, SeekOrigin.End);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
|
// A FILE* can cross between HLE stdio and a title's bundled libc.
|
||||||
|
// Back the handle with zeroed guest memory so native helpers such
|
||||||
|
// as _Lockfilelock can safely inspect the FILE object instead of
|
||||||
|
// dereferencing the old small integer token (0x1000, 0x1008...).
|
||||||
|
if (!KernelMemoryCompatExports.TryAllocateHleData(
|
||||||
|
ctx,
|
||||||
|
GuestFileObjectSize,
|
||||||
|
alignment: 0x10,
|
||||||
|
out var handle))
|
||||||
|
{
|
||||||
|
stream.Dispose();
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
_fileHandles[handle] = stream;
|
_fileHandles[handle] = stream;
|
||||||
|
|
||||||
if (_traceStdio)
|
if (_traceStdio)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
@@ -12,13 +15,29 @@ public static class NetExports
|
|||||||
{
|
{
|
||||||
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
||||||
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
||||||
|
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
|
||||||
|
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
|
||||||
|
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
|
||||||
|
private const int NetErrnoBadFileDescriptor = 9;
|
||||||
|
private const int NetErrnoInvalidArgument = 22;
|
||||||
|
private const int NetErrnoWouldBlock = 35;
|
||||||
|
private const int NetErrnoAddressInUse = 48;
|
||||||
|
private const int NetErrnoNotInitialized = 200;
|
||||||
private const int MaxNameLength = 256;
|
private const int MaxNameLength = 256;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
||||||
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
||||||
|
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
|
||||||
private static int _nextPoolId;
|
private static int _nextPoolId;
|
||||||
private static int _nextResolverId = 0x2000;
|
private static int _nextResolverId = 0x2000;
|
||||||
private static bool _initialized;
|
private static int _nextSocketId = 0x4000;
|
||||||
|
// The platform networking module is usable immediately after it is loaded.
|
||||||
|
// Games and middleware (notably FMOD) can create internal sockets before an
|
||||||
|
// explicit sceNetInit call reaches application code.
|
||||||
|
private static bool _initialized = true;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static nint _errnoAddress;
|
||||||
|
|
||||||
private sealed record NetPool(string Name, int Size, int Flags);
|
private sealed record NetPool(string Name, int Size, int Flags);
|
||||||
|
|
||||||
@@ -46,10 +65,232 @@ public static class NetExports
|
|||||||
_initialized = false;
|
_initialized = false;
|
||||||
_pools.Clear();
|
_pools.Clear();
|
||||||
_resolvers.Clear();
|
_resolvers.Clear();
|
||||||
|
foreach (var socket in _sockets.Values)
|
||||||
|
{
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
_sockets.Clear();
|
||||||
TraceNet("term", 0, 0, 0, 0);
|
TraceNet("term", 0, 0, 0, 0);
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Q4qBuN-c0ZM",
|
||||||
|
ExportName = "sceNetSocket",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetSocket(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var family = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
var protocol = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||||
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||||
|
? value
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
if (!TryTranslateSocketParameters(family, type, protocol, out var addressFamily, out var socketType, out var protocolType))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var socket = new Socket(addressFamily, socketType, protocolType);
|
||||||
|
var id = Interlocked.Increment(ref _nextSocketId);
|
||||||
|
_sockets[id] = socket;
|
||||||
|
TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol));
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "45ggEzakPJQ",
|
||||||
|
ExportName = "sceNetSocketClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetSocketClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryRemove(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Dispose();
|
||||||
|
TraceNet("socket.close", id, 0, 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2mKX2Spso7I",
|
||||||
|
ExportName = "sceNetSetsockopt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetSetsockopt(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 valueLength = unchecked((int)ctx[CpuRegister.R8]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORBIS_NET_SOL_SOCKET / ORBIS_NET_SO_NBIO. This is the first option
|
||||||
|
// used by FMOD's discovery socket and maps directly to host blocking.
|
||||||
|
if (level == 0xFFFF && option == 0x1200)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(int)];
|
||||||
|
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Blocking = BinaryPrimitives.ReadInt32LittleEndian(value) == 0;
|
||||||
|
TraceNet("socket.nonblocking", id, socket.Blocking ? 0UL : 1UL, 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORBIS_NET_SO_REUSEADDR uses the BSD value 0x0004.
|
||||||
|
if (level == 0xFFFF && option == 0x0004)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(int)];
|
||||||
|
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.SetSocketOption(
|
||||||
|
SocketOptionLevel.Socket,
|
||||||
|
SocketOptionName.ReuseAddress,
|
||||||
|
BinaryPrimitives.ReadInt32LittleEndian(value) != 0);
|
||||||
|
TraceNet("socket.reuseaddr", id, BinaryPrimitives.ReadUInt32LittleEndian(value), 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bErx49PgxyY",
|
||||||
|
ExportName = "sceNetBind",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetBind(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
if (!TryReadSocketAddress(ctx, ctx[CpuRegister.Rsi], unchecked((int)ctx[CpuRegister.Rdx]), out var endpoint))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socket.Bind(endpoint);
|
||||||
|
TraceNet("socket.bind", id, unchecked((ulong)endpoint.Port), 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorAddressInUse, NetErrnoAddressInUse);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kOj1HiAGE54",
|
||||||
|
ExportName = "sceNetListen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetListen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socket.Listen(Math.Max(0, unchecked((int)ctx[CpuRegister.Rsi])));
|
||||||
|
TraceNet("socket.listen", id, ctx[CpuRegister.Rsi], 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PIWqhn9oSxc",
|
||||||
|
ExportName = "sceNetAccept",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetAccept(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var accepted = socket.Accept();
|
||||||
|
var acceptedId = Interlocked.Increment(ref _nextSocketId);
|
||||||
|
_sockets[acceptedId] = accepted;
|
||||||
|
TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HQOwnfMGipQ",
|
||||||
|
ExportName = "sceNetErrnoLoc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetErrnoLoc(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (_errnoAddress == 0)
|
||||||
|
{
|
||||||
|
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
|
||||||
|
Marshal.WriteInt32(_errnoAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)_errnoAddress);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "dgJBaeJnGpo",
|
Nid = "dgJBaeJnGpo",
|
||||||
ExportName = "sceNetPoolCreate",
|
ExportName = "sceNetPoolCreate",
|
||||||
@@ -103,8 +344,10 @@ public static class NetExports
|
|||||||
public static int NetHtonl(CpuContext ctx)
|
public static int NetHtonl(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||||
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
return ctx.SetReturn(0);
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -115,8 +358,10 @@ public static class NetExports
|
|||||||
public static int NetHtons(CpuContext ctx)
|
public static int NetHtons(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||||
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||||
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
return ctx.SetReturn(0);
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -127,8 +372,10 @@ public static class NetExports
|
|||||||
public static int NetNtohl(CpuContext ctx)
|
public static int NetNtohl(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||||
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
return ctx.SetReturn(0);
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -139,8 +386,10 @@ public static class NetExports
|
|||||||
public static int NetNtohs(CpuContext ctx)
|
public static int NetNtohs(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||||
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
||||||
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
||||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||||
return ctx.SetReturn(0);
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -207,6 +456,69 @@ public static class NetExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SetNetError(CpuContext ctx, int result, int errno)
|
||||||
|
{
|
||||||
|
if (_errnoAddress == 0)
|
||||||
|
{
|
||||||
|
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
|
||||||
|
}
|
||||||
|
Marshal.WriteInt32(_errnoAddress, errno);
|
||||||
|
return ctx.SetReturn(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryTranslateSocketParameters(
|
||||||
|
int family,
|
||||||
|
int type,
|
||||||
|
int protocol,
|
||||||
|
out AddressFamily addressFamily,
|
||||||
|
out SocketType socketType,
|
||||||
|
out ProtocolType protocolType)
|
||||||
|
{
|
||||||
|
addressFamily = family switch
|
||||||
|
{
|
||||||
|
2 => AddressFamily.InterNetwork,
|
||||||
|
28 => AddressFamily.InterNetworkV6,
|
||||||
|
_ => AddressFamily.Unspecified,
|
||||||
|
};
|
||||||
|
socketType = type switch
|
||||||
|
{
|
||||||
|
1 => SocketType.Stream,
|
||||||
|
2 => SocketType.Dgram,
|
||||||
|
_ => SocketType.Unknown,
|
||||||
|
};
|
||||||
|
protocolType = protocol switch
|
||||||
|
{
|
||||||
|
0 when socketType == SocketType.Stream => ProtocolType.Tcp,
|
||||||
|
0 when socketType == SocketType.Dgram => ProtocolType.Udp,
|
||||||
|
6 => ProtocolType.Tcp,
|
||||||
|
17 => ProtocolType.Udp,
|
||||||
|
_ => ProtocolType.Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return addressFamily != AddressFamily.Unspecified &&
|
||||||
|
socketType != SocketType.Unknown &&
|
||||||
|
protocolType != ProtocolType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadSocketAddress(CpuContext ctx, ulong address, int length, out IPEndPoint endpoint)
|
||||||
|
{
|
||||||
|
endpoint = new IPEndPoint(IPAddress.Any, 0);
|
||||||
|
if (address == 0 || length < 16)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[16];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes) || bytes[1] != 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = BinaryPrimitives.ReadUInt16BigEndian(bytes[2..4]);
|
||||||
|
endpoint = new IPEndPoint(new IPAddress(bytes[4..8]), port);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
{
|
{
|
||||||
value = string.Empty;
|
value = string.Empty;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Ngs2;
|
namespace SharpEmu.Libs.Ngs2;
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ public static class Ngs2Exports
|
|||||||
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
||||||
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
||||||
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
||||||
private const ulong HandleStorageSize = 0x1000;
|
private const ulong HandleStorageSize = 0x20;
|
||||||
private const int RenderBufferInfoSize = 0x18;
|
private const int RenderBufferInfoSize = 0x18;
|
||||||
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -38,13 +39,13 @@ public static class Ngs2Exports
|
|||||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -52,7 +53,7 @@ public static class Ngs2Exports
|
|||||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -67,7 +68,7 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.Remove(handle))
|
if (!Systems.Remove(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rackHandles = Racks
|
var rackHandles = Racks
|
||||||
@@ -80,7 +81,7 @@ public static class Ngs2Exports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -97,19 +98,19 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.ContainsKey(systemHandle))
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -117,7 +118,7 @@ public static class Ngs2Exports
|
|||||||
Racks[handle] = new RackState(systemHandle, rackId);
|
Racks[handle] = new RackState(systemHandle, rackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -132,13 +133,13 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Racks.ContainsKey(handle))
|
if (!Racks.ContainsKey(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveRackLocked(handle);
|
RemoveRackLocked(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -155,7 +156,7 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Racks.ContainsKey(rackHandle))
|
if (!Racks.ContainsKey(rackHandle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing = Voices.FirstOrDefault(
|
var existing = Voices.FirstOrDefault(
|
||||||
@@ -163,20 +164,20 @@ public static class Ngs2Exports
|
|||||||
if (existing.Key != 0)
|
if (existing.Key != 0)
|
||||||
{
|
{
|
||||||
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -184,7 +185,7 @@ public static class Ngs2Exports
|
|||||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -196,19 +197,12 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(
|
return SetReturn(
|
||||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
ctx,
|
||||||
? 0
|
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
|
|
||||||
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
|
|
||||||
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
|
|
||||||
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
|
|
||||||
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
|
|
||||||
// their real libraries; the NIDs below are verified against the hash.
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "AbYvTOZ8Pts",
|
Nid = "AbYvTOZ8Pts",
|
||||||
ExportName = "sceNgs2VoiceRunCommands",
|
ExportName = "sceNgs2VoiceRunCommands",
|
||||||
@@ -216,48 +210,6 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "-TOuuAQ-buE",
|
|
||||||
ExportName = "sceNgs2VoiceGetState",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "rEh728kXk3w",
|
|
||||||
ExportName = "sceNgs2VoiceGetStateFlags",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "xa8oL9dmXkM",
|
|
||||||
ExportName = "sceNgs2PanInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "1WsleK-MTkE",
|
|
||||||
ExportName = "sceNgs2GeomCalcListener",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "0lbbayqDNoE",
|
|
||||||
ExportName = "sceNgs2GeomResetSourceParam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7Lcfo8SmpsU",
|
|
||||||
ExportName = "sceNgs2GeomResetListenerParam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "i0VnXM-C9fc",
|
Nid = "i0VnXM-C9fc",
|
||||||
ExportName = "sceNgs2SystemRender",
|
ExportName = "sceNgs2SystemRender",
|
||||||
@@ -272,13 +224,13 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.ContainsKey(systemHandle))
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint i = 0; i < bufferInfoCount; i++)
|
for (uint i = 0; i < bufferInfoCount; i++)
|
||||||
@@ -287,14 +239,14 @@ public static class Ngs2Exports
|
|||||||
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
||||||
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferAddress != 0 && bufferSize != 0)
|
if (bufferAddress != 0 && bufferSize != 0)
|
||||||
{
|
{
|
||||||
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,11 +258,138 @@ public static class Ngs2Exports
|
|||||||
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pgFAiLR5qT4",
|
||||||
|
ExportName = "sceNgs2SystemQueryBufferSize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemQueryBufferSize(CpuContext ctx) => WriteBufferSize(ctx, ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0eFLVCfWVds",
|
||||||
|
ExportName = "sceNgs2RackQueryBufferSize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2RackQueryBufferSize(CpuContext ctx) => WriteBufferSize(ctx, ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
// Report a fixed working-memory footprint for the requested object. The
|
||||||
|
// out struct (SceNgs2BufferAllocator-style) begins with the size field.
|
||||||
|
private static int WriteBufferSize(CpuContext ctx, ulong outAddress)
|
||||||
|
{
|
||||||
|
if (outAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> info = stackalloc byte[RenderBufferInfoSize];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[0..8], 0x10000);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[8..16], 0x100);
|
||||||
|
return ctx.Memory.TryWrite(outAddress, info)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "l4Q2dWEH6UM",
|
||||||
|
ExportName = "sceNgs2SystemSetGrainSamples",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemSetGrainSamples(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-tbc2SxQD60",
|
||||||
|
ExportName = "sceNgs2SystemSetSampleRate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemSetSampleRate(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gThZqM5PYlQ",
|
||||||
|
ExportName = "sceNgs2SystemLock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemLock(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JXRC5n0RQls",
|
||||||
|
ExportName = "sceNgs2SystemUnlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemUnlock(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-TOuuAQ-buE",
|
||||||
|
ExportName = "sceNgs2VoiceGetState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceGetState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var voiceHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var stateSize = (int)Math.Min(ctx[CpuRegister.Rdx], 0x400);
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Voices.ContainsKey(voiceHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report an idle (not-in-use) voice: all-zero state block.
|
||||||
|
if (stateAddress != 0 && stateSize > 0)
|
||||||
|
{
|
||||||
|
if (!TryClearGuestBuffer(ctx, stateAddress, (ulong)stateSize))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rEh728kXk3w",
|
||||||
|
ExportName = "sceNgs2VoiceGetStateFlags",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceGetStateFlags(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var voiceHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var flagsAddress = ctx[CpuRegister.Rsi];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Voices.ContainsKey(voiceHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No flags set: voice is idle.
|
||||||
|
if (flagsAddress != 0 && !ctx.TryWriteUInt64(flagsAddress, 0))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ValidateSystem(CpuContext ctx)
|
||||||
|
{
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
Systems.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||||
{
|
{
|
||||||
|
handle = 0;
|
||||||
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -318,18 +397,11 @@ public static class Ngs2Exports
|
|||||||
|
|
||||||
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
||||||
data.Clear();
|
data.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
||||||
if (!ctx.Memory.TryWrite(handle, data))
|
return ctx.Memory.TryWrite(handle, data);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
|
|
||||||
// object; stamp it with the shared dummy vtable instead of a self-referential value.
|
|
||||||
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||||
@@ -367,4 +439,37 @@ public static class Ngs2Exports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xa8oL9dmXkM",
|
||||||
|
ExportName = "sceNgs2PanInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1WsleK-MTkE",
|
||||||
|
ExportName = "sceNgs2GeomCalcListener",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0lbbayqDNoE",
|
||||||
|
ExportName = "sceNgs2GeomResetSourceParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7Lcfo8SmpsU",
|
||||||
|
ExportName = "sceNgs2GeomResetListenerParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public static class NpManagerExports
|
|||||||
{
|
{
|
||||||
private const int NpTitleIdSize = 16;
|
private const int NpTitleIdSize = 16;
|
||||||
private const int NpTitleSecretSize = 128;
|
private const int NpTitleSecretSize = 128;
|
||||||
private const int NpReachabilityStateUnavailable = 0;
|
private const int NpErrorInvalidArgument = unchecked((int)0x80550003);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3Zl8BePTh9Y",
|
Nid = "3Zl8BePTh9Y",
|
||||||
@@ -58,35 +58,6 @@ public static class NpManagerExports
|
|||||||
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "rbknaUjpqWo",
|
|
||||||
ExportName = "sceNpGetAccountIdA",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpManager")]
|
|
||||||
public static int NpGetOnlineIdA(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// The A variant takes a user ID before OnlineId.
|
|
||||||
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "e-ZuhGEoeC4",
|
|
||||||
ExportName = "sceNpGetNpReachabilityState",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpManager")]
|
|
||||||
public static int NpGetNpReachabilityState(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (stateAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.TryWriteInt32(stateAddress, NpReachabilityStateUnavailable)
|
|
||||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "VfRSmPmj8Q8",
|
Nid = "VfRSmPmj8Q8",
|
||||||
ExportName = "sceNpRegisterStateCallback",
|
ExportName = "sceNpRegisterStateCallback",
|
||||||
@@ -140,6 +111,77 @@ public static class NpManagerExports
|
|||||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rbknaUjpqWo",
|
||||||
|
ExportName = "sceNpGetAccountIdA",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetAccountIdA(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var accountIdAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId == -1 || accountIdAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The offline profile exposed by sceNpGetState is signed in. Keep the
|
||||||
|
// account query consistent with that state: Unity's PSN integration
|
||||||
|
// treats SIGNED_OUT as an exceptional state and retries it every frame.
|
||||||
|
// A stable local-only id is sufficient for titles which only use the
|
||||||
|
// value as a profile key.
|
||||||
|
Span<byte> accountId = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(accountId, 1);
|
||||||
|
return ctx.Memory.TryWrite(accountIdAddress, accountId)
|
||||||
|
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JT+t00a3TxA",
|
||||||
|
ExportName = "sceNpGetAccountCountryA",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetAccountCountryA(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var countryAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId == -1 || countryAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> country = stackalloc byte[4];
|
||||||
|
country[0] = (byte)'U';
|
||||||
|
country[1] = (byte)'S';
|
||||||
|
country[2] = 0;
|
||||||
|
country[3] = 0;
|
||||||
|
return ctx.Memory.TryWrite(countryAddress, country)
|
||||||
|
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "e-ZuhGEoeC4",
|
||||||
|
ExportName = "sceNpGetNpReachabilityState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId == -1 || stateAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> state = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(state, 0); // Unavailable while offline.
|
||||||
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
|
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ec63y59l9tw",
|
Nid = "Ec63y59l9tw",
|
||||||
ExportName = "sceNpSetNpTitleId",
|
ExportName = "sceNpSetNpTitleId",
|
||||||
@@ -166,6 +208,12 @@ public static class NpManagerExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||||
{
|
{
|
||||||
var length = 0;
|
var length = 0;
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ public static class NpWebApi2Exports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sk54bi6FtYM",
|
||||||
|
ExportName = "sceNpWebApi2CreateUserContext",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpWebApi2")]
|
||||||
|
public static int NpWebApi2CreateUserContext(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// No PSN backend: refuse user-context creation so the title's online
|
||||||
|
// layer backs off instead of driving a half-created context handle.
|
||||||
|
TraceNpWebApi2("create-user-context", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
|
||||||
|
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bEvXpcEk200",
|
Nid = "bEvXpcEk200",
|
||||||
ExportName = "sceNpWebApi2Terminate",
|
ExportName = "sceNpWebApi2Terminate",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// 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 System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.Libs.Pad;
|
||||||
@@ -16,6 +18,19 @@ public static class BluetoothHidExports
|
|||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// EXPERIMENT: fire the registered callback once with a zeroed event struct.
|
||||||
|
// Direct execution shares the host address space with the guest, so an
|
||||||
|
// AllocHGlobal buffer is directly readable by guest code.
|
||||||
|
// SHARPEMU_BTHID_FIRE_CALLBACK=1 enables; SHARPEMU_BTHID_EVENT_CODE and
|
||||||
|
// SHARPEMU_BTHID_EVENT_SIZE (default 0 / 256) shape the synthetic event.
|
||||||
|
private static readonly bool _fireCallback = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_FIRE_CALLBACK"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static ulong _callbackFunction;
|
||||||
|
private static int _fired;
|
||||||
|
|
||||||
private static int Result(CpuContext ctx) =>
|
private static int Result(CpuContext ctx) =>
|
||||||
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
||||||
|
|
||||||
@@ -31,12 +46,101 @@ public static class BluetoothHidExports
|
|||||||
ExportName = "sceBluetoothHidRegisterDevice",
|
ExportName = "sceBluetoothHidRegisterDevice",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceBluetoothHid")]
|
LibraryName = "libSceBluetoothHid")]
|
||||||
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
|
public static int BluetoothHidRegisterDevice(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = Result(ctx);
|
||||||
|
if (_fireCallback && _callbackFunction >= 0x10000 &&
|
||||||
|
Interlocked.Exchange(ref _fired, 1) == 0)
|
||||||
|
{
|
||||||
|
FireEnumerationComplete(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "4Ypfo9RIwfM",
|
Nid = "4Ypfo9RIwfM",
|
||||||
ExportName = "sceBluetoothHidRegisterCallback",
|
ExportName = "sceBluetoothHidRegisterCallback",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceBluetoothHid")]
|
LibraryName = "libSceBluetoothHid")]
|
||||||
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
|
public static int BluetoothHidRegisterCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_callbackFunction = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
// EXPERIMENT: failing ONLY the callback registration (with a generic kernel
|
||||||
|
// error, unlike the BT-specific unavailable code) may make wheel/FFB
|
||||||
|
// middleware disable its Bluetooth search loop instead of polling forever.
|
||||||
|
// SHARPEMU_BTHID_CB_FAIL=nf -> NOT_FOUND, =ni -> NOT_IMPLEMENTED.
|
||||||
|
var mode = Environment.GetEnvironmentVariable("SHARPEMU_BTHID_CB_FAIL");
|
||||||
|
if (string.Equals(mode, "nf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(mode, "ni", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FireEnumerationComplete(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (scheduler is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventCode = ParseEnvUInt64("SHARPEMU_BTHID_EVENT_CODE", 0);
|
||||||
|
var eventSize = (int)ParseEnvUInt64("SHARPEMU_BTHID_EVENT_SIZE", 256);
|
||||||
|
if (eventSize < 1)
|
||||||
|
{
|
||||||
|
eventSize = 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaked by design: the guest may retain the pointer past the callback.
|
||||||
|
var eventStruct = Marshal.AllocHGlobal(eventSize);
|
||||||
|
for (var offset = 0; offset < eventSize; offset++)
|
||||||
|
{
|
||||||
|
Marshal.WriteByte(eventStruct, offset, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[BTHID][EXPERIMENT] firing callback=0x{_callbackFunction:X} code={eventCode} size={eventSize} struct=0x{eventStruct.ToInt64():X}");
|
||||||
|
if (!scheduler.TryCallGuestFunction(
|
||||||
|
ctx,
|
||||||
|
_callbackFunction,
|
||||||
|
unchecked((ulong)eventStruct.ToInt64()),
|
||||||
|
eventCode,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"sceBluetoothHid synthetic enumeration event",
|
||||||
|
out var error))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[BTHID][EXPERIMENT] callback failed: {error}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[BTHID][EXPERIMENT] callback returned OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ParseEnvUInt64(string name, ulong fallback)
|
||||||
|
{
|
||||||
|
var value = Environment.GetEnvironmentVariable(name);
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.Trim();
|
||||||
|
var hex = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return ulong.TryParse(
|
||||||
|
hex ? value[2..] : value,
|
||||||
|
hex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.Integer,
|
||||||
|
null,
|
||||||
|
out var parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ public static class HostWindowInput
|
|||||||
{
|
{
|
||||||
keyboard.KeyDown += (_, key, _) =>
|
keyboard.KeyDown += (_, key, _) =>
|
||||||
{
|
{
|
||||||
|
if (key == Key.F1)
|
||||||
|
{
|
||||||
|
VideoOut.PerfOverlay.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
Pressed.Add(key);
|
Pressed.Add(key);
|
||||||
@@ -248,9 +253,9 @@ public static class HostWindowInput
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte ToStickByte(float value)
|
internal static byte ToStickByte(float value)
|
||||||
{
|
{
|
||||||
return (byte)Math.Clamp((int)(128.0f + value * 127.0f), 0, 255);
|
return (byte)Math.Clamp((int)MathF.Round((value + 1.0f) * 127.5f), 0, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HostGamepadButtons MapButton(ButtonName name) => name switch
|
private static HostGamepadButtons MapButton(ButtonName name) => name switch
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
public static class ImeExports
|
||||||
|
{
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
|
private const int ImeErrorInvalidAddress = unchecked((int)0x80BC0001);
|
||||||
|
private const int ImeErrorInvalidUserId = unchecked((int)0x80BC0010);
|
||||||
|
private const int ImeErrorNotOpened = unchecked((int)0x80BC0005);
|
||||||
|
|
||||||
|
private static bool _keyboardOpen;
|
||||||
|
public static int ImeKeyboardOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, ImeErrorInvalidAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId != PrimaryUserId)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, ImeErrorInvalidUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_keyboardOpen = true;
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
public static int ImeUpdate(CpuContext ctx) =>
|
||||||
|
SetReturn(ctx, _keyboardOpen ? 0 : ImeErrorNotOpened);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
public static class MouseExports
|
||||||
|
{
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
|
private const int MouseDataSize = 0x28;
|
||||||
|
private const int MouseErrorInvalidArgument = unchecked((int)0x80020002);
|
||||||
|
private const int MouseErrorInvalidHandle = unchecked((int)0x80020003);
|
||||||
|
private const int MouseErrorNotInitialized = unchecked((int)0x80020005);
|
||||||
|
private const int MouseErrorAlreadyOpened = unchecked((int)0x80020008);
|
||||||
|
|
||||||
|
private static readonly bool[] OpenHandles = new bool[2];
|
||||||
|
private static bool _initialized;
|
||||||
|
public static int MouseInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_initialized = true;
|
||||||
|
Array.Clear(OpenHandles);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
public static int MouseOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (userId != PrimaryUserId || type != 0 || index is < 0 or > 1)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenHandles[index])
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorAlreadyOpened);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenHandles[index] = true;
|
||||||
|
return SetReturn(ctx, index);
|
||||||
|
}
|
||||||
|
public static int MouseRead(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (dataAddress == 0 || count is < 1 or > 64)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle is < 0 or > 1 || !OpenHandles[handle])
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> data = stackalloc byte[MouseDataSize];
|
||||||
|
data.Clear();
|
||||||
|
var ticks = Stopwatch.GetTimestamp();
|
||||||
|
var timestamp =
|
||||||
|
((ulong)(ticks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||||
|
((ulong)(ticks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(data, timestamp);
|
||||||
|
data[0x08] = 0; // No host mouse is exposed to the guest yet.
|
||||||
|
return ctx.Memory.TryWrite(dataAddress, data)
|
||||||
|
? SetReturn(ctx, 1)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cAnT0Rw-IwU",
|
||||||
|
ExportName = "sceMouseClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (handle is < 0 or > 1 || !OpenHandles[handle])
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenHandles[handle] = false;
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,20 @@ public static class PadExports
|
|||||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||||
private const int PrimaryUserId = 1000;
|
// Keep the pad session on the same retail user id returned by
|
||||||
|
// libSceUserService. A mismatched emulator-local id makes games pass a
|
||||||
|
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
|
||||||
|
// leaving every later keyboard/gamepad read on an invalid handle.
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
private const int StandardPortType = 0;
|
private const int StandardPortType = 0;
|
||||||
private const int PrimaryPadHandle = 1;
|
private const int PrimaryPadHandle = 1;
|
||||||
private const int ControllerInformationSize = 0x1C;
|
private const int ControllerInformationSize = 0x1C;
|
||||||
private const int PadDataSize = 0x78;
|
private const int PadDataSize = 0x78;
|
||||||
|
|
||||||
|
// Real firmware hands out small non-negative handles; 0 is valid. Some titles
|
||||||
|
// (Monster Truck Championship) read pad state with handle 0, and rejecting it
|
||||||
|
// leaves their controller/FFB init path polling a never-valid state forever.
|
||||||
|
private static bool IsPrimaryPadHandle(int handle) => handle is 0 or PrimaryPadHandle;
|
||||||
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
@@ -100,7 +109,7 @@ public static class PadExports
|
|||||||
public static int PadClose(CpuContext ctx)
|
public static int PadClose(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return handle == PrimaryPadHandle
|
return IsPrimaryPadHandle(handle)
|
||||||
? ctx.SetReturn(0)
|
? ctx.SetReturn(0)
|
||||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -113,7 +122,20 @@ public static class PadExports
|
|||||||
public static int PadSetMotionSensorState(CpuContext ctx)
|
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return handle == PrimaryPadHandle
|
return IsPrimaryPadHandle(handle)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "vDLMoJLde8I",
|
||||||
|
ExportName = "scePadSetTiltCorrectionState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadSetTiltCorrectionState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return IsPrimaryPadHandle(handle)
|
||||||
? ctx.SetReturn(0)
|
? ctx.SetReturn(0)
|
||||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -127,7 +149,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var informationAddress = ctx[CpuRegister.Rsi];
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -162,7 +184,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var informationAddress = ctx[CpuRegister.Rsi];
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -203,7 +225,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var dataAddress = ctx[CpuRegister.Rsi];
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -228,7 +250,7 @@ public static class PadExports
|
|||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var dataAddress = ctx[CpuRegister.Rsi];
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -262,7 +284,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -306,7 +328,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -336,7 +358,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
@@ -365,7 +387,7 @@ public static class PadExports
|
|||||||
public static int PadResetLightBar(CpuContext ctx)
|
public static int PadResetLightBar(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (handle != PrimaryPadHandle)
|
if (!IsPrimaryPadHandle(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.PlayGo;
|
namespace SharpEmu.Libs.PlayGo;
|
||||||
|
|
||||||
@@ -418,12 +419,21 @@ public static class PlayGoExports
|
|||||||
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
|
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
|
||||||
$"known=[{string.Join(',', knownChunkIds)}]");
|
$"known=[{string.Join(',', knownChunkIds)}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Real firmware rejects chunk ids outside the package's chunk set.
|
||||||
|
// Titles rely on this as an enumeration terminator: Monster Truck
|
||||||
|
// scans ids 0,1,2,... until BAD_CHUNK_ID, and answering OK for every
|
||||||
|
// id makes that scan wrap the ushort range and spin forever.
|
||||||
|
loci[i] = PlayGoLocusNotDownloaded;
|
||||||
|
return ctx.Memory.TryWrite(outLoci, loci)
|
||||||
|
? OrbisPlayGoErrorBadChunkId
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
loci[i] = PlayGoLocusLocalFast;
|
loci[i] = PlayGoLocusLocalFast;
|
||||||
}
|
}
|
||||||
|
|
||||||
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
|
TracePlayGoLocus(ctx, numberOfEntries, chunkIds, outLoci);
|
||||||
return ctx.Memory.TryWrite(outLoci, loci)
|
return ctx.Memory.TryWrite(outLoci, loci)
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -663,7 +673,8 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
return _metadata.ChunkIdKnowledge == PlayGoChunkIdKnowledge.Unknown ||
|
||||||
|
Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +683,12 @@ public static class PlayGoExports
|
|||||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
if (string.IsNullOrWhiteSpace(app0Root))
|
if (string.IsNullOrWhiteSpace(app0Root))
|
||||||
{
|
{
|
||||||
return PlayGoMetadata.Empty;
|
// No app0 override to probe for sidecar files: same fully-installed
|
||||||
|
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
|
||||||
|
return new PlayGoMetadata(
|
||||||
|
true,
|
||||||
|
[(ushort)0],
|
||||||
|
PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||||
@@ -682,13 +698,24 @@ public static class PlayGoExports
|
|||||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||||
if (!hasMetadata)
|
if (!hasMetadata)
|
||||||
{
|
{
|
||||||
// Full installs may omit PlayGo sidecar metadata.
|
// No PlayGo sidecar: report a fully-installed single chunk. Available must
|
||||||
TracePlayGo("metadata_missing; using fully-installed default chunk");
|
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
|
||||||
return new PlayGoMetadata(true, [(ushort)0]);
|
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
||||||
|
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
||||||
|
TracePlayGo("metadata_missing; fully-installed single chunk");
|
||||||
|
return new PlayGoMetadata(
|
||||||
|
true,
|
||||||
|
[(ushort)0],
|
||||||
|
PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||||
return new PlayGoMetadata(true, chunkIds);
|
return new PlayGoMetadata(
|
||||||
|
true,
|
||||||
|
chunkIds,
|
||||||
|
chunkIds.Length == 0
|
||||||
|
? PlayGoChunkIdKnowledge.Unknown
|
||||||
|
: PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||||
@@ -701,6 +728,8 @@ public static class PlayGoExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var xml = File.ReadAllText(chunkDefsXml);
|
var xml = File.ReadAllText(chunkDefsXml);
|
||||||
|
_ = XDocument.Parse(xml, LoadOptions.None);
|
||||||
|
|
||||||
var chunkIds = new HashSet<ushort>();
|
var chunkIds = new HashSet<ushort>();
|
||||||
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
||||||
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
||||||
@@ -717,6 +746,10 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
return Array.Empty<ushort>();
|
return Array.Empty<ushort>();
|
||||||
}
|
}
|
||||||
|
catch (System.Xml.XmlException)
|
||||||
|
{
|
||||||
|
return Array.Empty<ushort>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
||||||
@@ -738,7 +771,7 @@ public static class PlayGoExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
|
private static void TracePlayGoLocus(CpuContext ctx, uint entries, ulong chunkIds, ulong outLoci)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -748,13 +781,42 @@ public static class PlayGoExports
|
|||||||
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
|
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
|
||||||
if (entries != 1 || count <= 32 || count % 1000 == 0)
|
if (entries != 1 || count <= 32 || count % 1000 == 0)
|
||||||
{
|
{
|
||||||
|
_ = ctx.TryReadUInt16(chunkIds, out var firstChunkId);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
$"[LOADER][TRACE] playgo.get_locus entries={entries} first_chunk={firstChunkId} " +
|
||||||
|
$"chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
|
internal static void ResetForTests()
|
||||||
{
|
{
|
||||||
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_initialized = false;
|
||||||
|
_opened = false;
|
||||||
|
_metadata = PlayGoMetadata.Empty;
|
||||||
|
_installSpeed = PlayGoInstallSpeedTrickle;
|
||||||
|
_languageMask = ulong.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _unknownChunkDiagnostics, 0);
|
||||||
|
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PlayGoChunkIdKnowledge
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Authoritative,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PlayGoMetadata(
|
||||||
|
bool Available,
|
||||||
|
ushort[] ChunkIds,
|
||||||
|
PlayGoChunkIdKnowledge ChunkIdKnowledge)
|
||||||
|
{
|
||||||
|
public static readonly PlayGoMetadata Empty = new(
|
||||||
|
false,
|
||||||
|
Array.Empty<ushort>(),
|
||||||
|
PlayGoChunkIdKnowledge.Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,64 @@ public static class RtcExports
|
|||||||
LibraryName = "libSceRtc")]
|
LibraryName = "libSceRtc")]
|
||||||
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
// Diagnostic: middleware busy-wait loops typically poll sceRtcGetCurrentTick, so the
|
||||||
|
// caller's return address pinpoints the loop. SHARPEMU_RTC_PROBE_RANGE=<start>-<end>
|
||||||
|
// (hex guest addresses) dumps 0x100 bytes of code around the first matching caller,
|
||||||
|
// once, for offline disassembly. Costs nothing when the variable is unset.
|
||||||
|
private static readonly ulong[]? _rtcProbeRange = ParseRtcProbeRange();
|
||||||
|
private static int _rtcProbeDone;
|
||||||
|
|
||||||
|
private static ulong[]? ParseRtcProbeRange()
|
||||||
|
{
|
||||||
|
var value = Environment.GetEnvironmentVariable("SHARPEMU_RTC_PROBE_RANGE");
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = value.Split('-', 2, StringSplitOptions.TrimEntries);
|
||||||
|
return parts.Length == 2 &&
|
||||||
|
TryParseHexAddress(parts[0], out var start) &&
|
||||||
|
TryParseHexAddress(parts[1], out var end) &&
|
||||||
|
start < end
|
||||||
|
? [start, end]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseHexAddress(string value, out ulong address)
|
||||||
|
{
|
||||||
|
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = value[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ulong.TryParse(
|
||||||
|
value,
|
||||||
|
System.Globalization.NumberStyles.HexNumber,
|
||||||
|
null,
|
||||||
|
out address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProbeRtcCaller(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _rtcProbeDone) != 0 ||
|
||||||
|
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var ret) ||
|
||||||
|
ret < _rtcProbeRange![0] ||
|
||||||
|
ret >= _rtcProbeRange[1] ||
|
||||||
|
Interlocked.CompareExchange(ref _rtcProbeDone, 1, 0) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = ret - 0x60;
|
||||||
|
Span<byte> code = stackalloc byte[0x100];
|
||||||
|
if (ctx.Memory.TryRead(start, code))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][DIAG] rtc.caller_code ret=0x{ret:X} @0x{start:X}: {System.Convert.ToHexString(code)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "18B2NS1y9UU",
|
Nid = "18B2NS1y9UU",
|
||||||
ExportName = "sceRtcGetCurrentTick",
|
ExportName = "sceRtcGetCurrentTick",
|
||||||
@@ -254,6 +312,11 @@ public static class RtcExports
|
|||||||
return unchecked((int)0x80B50002);
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_rtcProbeRange is not null)
|
||||||
|
{
|
||||||
|
ProbeRtcCaller(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ public static class SaveDataExports
|
|||||||
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
||||||
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
||||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||||
|
private const int OrbisSaveDataErrorMemoryNotReady = unchecked((int)0x809F0012);
|
||||||
private const int SaveDataTitleIdSize = 10;
|
private const int SaveDataTitleIdSize = 10;
|
||||||
private const int SaveDataDirNameSize = 32;
|
private const int SaveDataDirNameSize = 32;
|
||||||
private const int SaveDataParamSize = 0x530;
|
private const int SaveDataParamSize = 0x530;
|
||||||
@@ -29,20 +31,19 @@ public static class SaveDataExports
|
|||||||
private const uint MountModeCreate = 1u << 2;
|
private const uint MountModeCreate = 1u << 2;
|
||||||
private const uint MountModeCreate2 = 1u << 5;
|
private const uint MountModeCreate2 = 1u << 5;
|
||||||
private const int MountResultSize = 0x40;
|
private const int MountResultSize = 0x40;
|
||||||
|
// Emulator guard against corrupt or misread sizes, not a platform limit.
|
||||||
|
private const ulong SaveDataMemoryMaxSize = 64UL * 1024 * 1024;
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly HashSet<int> _transactionResources = [];
|
private static readonly object _memoryGate = new();
|
||||||
private static readonly HashSet<int> _preparedTransactionResources = [];
|
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||||
private static string? _titleId;
|
private static string? _titleId;
|
||||||
private static int _nextTransactionResource;
|
|
||||||
|
|
||||||
public static void ConfigureApplicationInfo(string? titleId)
|
public static void ConfigureApplicationInfo(string? titleId)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
_transactionResources.Clear();
|
|
||||||
_preparedTransactionResources.Clear();
|
_preparedTransactionResources.Clear();
|
||||||
_nextTransactionResource = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,15 +57,15 @@ public static class SaveDataExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,18 +80,18 @@ public static class SaveDataExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rsi];
|
var resultAddress = ctx[CpuRegister.Rsi];
|
||||||
if (condAddress == 0 || resultAddress == 0)
|
if (condAddress == 0 || resultAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||||
!TryReadSearchResult(ctx, resultAddress, out var result))
|
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -102,7 +103,7 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||||
@@ -114,21 +115,21 @@ public static class SaveDataExports
|
|||||||
var setNum = result.DirNamesNum == 0
|
var setNum = result.DirNamesNum == 0
|
||||||
? 0
|
? 0
|
||||||
: Math.Min(result.DirNamesNum, entries.Count);
|
: Math.Min(result.DirNamesNum, entries.Count);
|
||||||
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setNum == 0)
|
if (setNum == 0)
|
||||||
{
|
{
|
||||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.DirNamesAddress == 0)
|
if (result.DirNamesAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < setNum; i++)
|
for (var i = 0; i < setNum; i++)
|
||||||
@@ -144,20 +145,20 @@ public static class SaveDataExports
|
|||||||
(result.InfosAddress != 0 &&
|
(result.InfosAddress != 0 &&
|
||||||
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,25 +173,25 @@ public static class SaveDataExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rsi];
|
var resultAddress = ctx[CpuRegister.Rsi];
|
||||||
if (mountAddress == 0 || resultAddress == 0)
|
if (mountAddress == 0 || resultAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadInt32(mountAddress, out var userId) ||
|
if (!TryReadInt32(ctx, mountAddress, out var userId) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x20, out var mountMode) ||
|
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x24, out var resource) ||
|
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x28, out var mode) ||
|
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
|
||||||
dirNameAddress == 0 ||
|
dirNameAddress == 0 ||
|
||||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -205,12 +206,12 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
if (!existed && !create && !createIfMissing)
|
if (!existed && !create && !createIfMissing)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
|
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existed && create)
|
if (existed && create)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorExists);
|
return SetReturn(ctx, OrbisSaveDataErrorExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existed)
|
if (!existed)
|
||||||
@@ -227,29 +228,30 @@ public static class SaveDataExports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
TraceSaveData(
|
||||||
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
||||||
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
||||||
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int _nextTransactionResource;
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gjRZNnw0JPE",
|
Nid = "gjRZNnw0JPE",
|
||||||
ExportName = "sceSaveDataCreateTransactionResource",
|
ExportName = "sceSaveDataCreateTransactionResource",
|
||||||
@@ -257,16 +259,39 @@ public static class SaveDataExports
|
|||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var memorySize = ctx[CpuRegister.Rdi];
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
int resource;
|
var reserved = ctx[CpuRegister.Rsi];
|
||||||
lock (_stateGate)
|
|
||||||
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
|
// The resource-out pointer's argument slot varies by SDK revision: some
|
||||||
|
// 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;
|
||||||
|
foreach (var candidate in new[]
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rdx],
|
||||||
|
ctx[CpuRegister.Rcx],
|
||||||
|
ctx[CpuRegister.R8],
|
||||||
|
ctx[CpuRegister.R9],
|
||||||
|
})
|
||||||
{
|
{
|
||||||
resource = ++_nextTransactionResource;
|
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
|
||||||
_transactionResources.Add(resource);
|
{
|
||||||
|
resourceAddress = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"create_transaction_resource memory_size=0x{memorySize:X} resource={resource}");
|
TraceSaveData(
|
||||||
return ctx.SetReturn(resource);
|
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -279,70 +304,11 @@ public static class SaveDataExports
|
|||||||
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
|
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_transactionResources.Remove(resource);
|
|
||||||
_preparedTransactionResources.Remove(resource);
|
_preparedTransactionResources.Remove(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"delete_transaction_resource resource={resource}");
|
TraceSaveData($"delete_transaction_resource resource={resource}");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "sDCBrmc61XU",
|
|
||||||
ExportName = "sceSaveDataPrepare",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataPrepare(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
if (mountPointAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
if (resource != 0)
|
|
||||||
{
|
|
||||||
_preparedTransactionResources.Add(resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "ie7qhZ4X0Cc",
|
|
||||||
ExportName = "sceSaveDataCommit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataCommit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var commitAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (commitAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
_preparedTransactionResources.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -352,40 +318,21 @@ public static class SaveDataExports
|
|||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataUmount2(CpuContext ctx)
|
public static int SaveDataUmount2(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
// Unmounting a save directory always succeeds in the stub filesystem;
|
||||||
if (mountPointAddress == 0)
|
// returning an error here makes the game's save flow stall before it
|
||||||
{
|
// hands control to the title/gameplay state.
|
||||||
mountPointAddress = ctx[CpuRegister.Rsi];
|
TraceSaveData($"umount2 user={unchecked((int)ctx[CpuRegister.Rdi])}");
|
||||||
}
|
return SetReturn(ctx, 0);
|
||||||
|
|
||||||
if (mountPointAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
var unmounted = KernelMemoryCompatExports.TryUnregisterGuestPathMount(mountPoint);
|
|
||||||
TraceSaveData($"umount2 mount_point={mountPoint} unregistered={unmounted}");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
{
|
{
|
||||||
cond = default;
|
cond = default;
|
||||||
if (!ctx.TryReadInt32(address, out var userId) ||
|
if (!TryReadInt32(ctx, address, out var userId) ||
|
||||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
|
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
|
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -408,7 +355,7 @@ public static class SaveDataExports
|
|||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
||||||
!ctx.TryReadUInt32(address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
||||||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||||
{
|
{
|
||||||
@@ -530,6 +477,19 @@ public static class SaveDataExports
|
|||||||
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||||
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||||
|
|
||||||
|
private static string ResolveSaveDataMemoryPath(int userId) =>
|
||||||
|
Path.Combine(ResolveTitleSaveRoot(userId, ResolveConfiguredTitleId()), "sce_sdmemory", "memory.dat");
|
||||||
|
|
||||||
|
private static bool TryReadMemoryData(
|
||||||
|
CpuContext ctx, ulong address, out ulong buffer, out ulong size, out ulong offset)
|
||||||
|
{
|
||||||
|
size = 0;
|
||||||
|
offset = 0;
|
||||||
|
return ctx.TryReadUInt64(address, out buffer) &&
|
||||||
|
ctx.TryReadUInt64(address + 0x08, out size) &&
|
||||||
|
ctx.TryReadUInt64(address + 0x10, out offset);
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveSaveDataRoot()
|
private static string ResolveSaveDataRoot()
|
||||||
{
|
{
|
||||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||||
@@ -609,6 +569,45 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceSaveData(string message)
|
private static void TraceSaveData(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||||
@@ -631,4 +630,255 @@ public static class SaveDataExports
|
|||||||
ulong InfosAddress);
|
ulong InfosAddress);
|
||||||
|
|
||||||
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sDCBrmc61XU",
|
||||||
|
ExportName = "sceSaveDataPrepare",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataPrepare(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var mountPointAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (resource != 0)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ie7qhZ4X0Cc",
|
||||||
|
ExportName = "sceSaveDataCommit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataCommit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commitAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (commitAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save data memory: a small per-user blob titles read and write without
|
||||||
|
// mounting anything, backed by one zero-filled file per user and title.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oQySEUfgXRA",
|
||||||
|
ExportName = "sceSaveDataSetupSaveDataMemory2",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataSetupSaveDataMemory2(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resultAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (paramAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadInt32(ctx, paramAddress + 0x04, out var userId) ||
|
||||||
|
!ctx.TryReadUInt64(paramAddress + 0x08, out var memorySize))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId < 0 || memorySize == 0 || memorySize > SaveDataMemoryMaxSize)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = ResolveSaveDataMemoryPath(userId);
|
||||||
|
lock (_memoryGate)
|
||||||
|
{
|
||||||
|
var backing = new FileInfo(path);
|
||||||
|
var existedSize = backing.Exists ? (ulong)backing.Length : 0;
|
||||||
|
|
||||||
|
// The result write comes first so a faulted result pointer
|
||||||
|
// cannot leave created or grown setup state behind.
|
||||||
|
if (resultAddress != 0 && !ctx.TryWriteUInt64(resultAddress, existedSize))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existedSize < memorySize)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
using var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||||
|
stream.SetLength((long)memorySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"memory-setup2 user={userId} size=0x{memorySize:X} existed=0x{existedSize:X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "QwOO7vegnV8",
|
||||||
|
ExportName = "sceSaveDataGetSaveDataMemory2",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataGetSaveDataMemory2(CpuContext ctx) =>
|
||||||
|
TransferSaveDataMemory(ctx, write: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cduy9v4YmT4",
|
||||||
|
ExportName = "sceSaveDataSetSaveDataMemory2",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataSetSaveDataMemory2(CpuContext ctx) =>
|
||||||
|
TransferSaveDataMemory(ctx, write: true);
|
||||||
|
|
||||||
|
// Writes go straight through to the backing file, so a ready state is
|
||||||
|
// all sync has to confirm.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wiT9jeC7xPw",
|
||||||
|
ExportName = "sceSaveDataSyncSaveDataMemory",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataSyncSaveDataMemory(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var syncAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (syncAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadInt32(ctx, syncAddress, out var userId))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId < 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(
|
||||||
|
File.Exists(ResolveSaveDataMemoryPath(userId)) ? 0 : OrbisSaveDataErrorMemoryNotReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TransferSaveDataMemory(CpuContext ctx, bool write)
|
||||||
|
{
|
||||||
|
var requestAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (requestAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadInt32(ctx, requestAddress, out var userId) ||
|
||||||
|
!ctx.TryReadUInt64(requestAddress + 0x08, out var dataAddress))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId < 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = ResolveSaveDataMemoryPath(userId);
|
||||||
|
lock (_memoryGate)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorMemoryNotReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadMemoryData(ctx, dataAddress, out var bufAddress, out var bufSize, out var offset))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = new FileStream(
|
||||||
|
path, FileMode.Open, write ? FileAccess.ReadWrite : FileAccess.Read);
|
||||||
|
var length = (ulong)stream.Length;
|
||||||
|
if (bufAddress == 0 || bufSize > length || offset > length - bufSize)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The guarded file length bounds bufSize, so one rented buffer
|
||||||
|
// covers the transfer and a guest fault never partially writes.
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Max(bufSize, 1));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var span = buffer.AsSpan(0, (int)bufSize);
|
||||||
|
stream.Seek((long)offset, SeekOrigin.Begin);
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(bufAddress, span))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Write(span);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stream.ReadExactly(span);
|
||||||
|
if (!ctx.Memory.TryWrite(bufAddress, span))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData(
|
||||||
|
$"memory-{(write ? "set2" : "get2")} user={userId} offset=0x{offset:X} size=0x{bufSize:X}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Stubs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Success stubs for trophy, character-encoding and telemetry ABI calls that
|
||||||
|
/// Void Terrarium (and other titles) invoke during startup. They were
|
||||||
|
/// previously unresolved and returned NOT_FOUND, and a title that gates its
|
||||||
|
/// UI/text initialization on these succeeding then skips ahead and never draws
|
||||||
|
/// its content (a black screen with only a clear pass). These return success
|
||||||
|
/// (and a non-zero handle where an out pointer is expected) so init proceeds.
|
||||||
|
/// </summary>
|
||||||
|
public static class GameServiceStubs
|
||||||
|
{
|
||||||
|
private static int Ok(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a small non-zero handle to the pointer in the given register so
|
||||||
|
// the caller treats the object as created; returns success.
|
||||||
|
private static int OkWithHandle(CpuContext ctx, CpuRegister outPointerRegister)
|
||||||
|
{
|
||||||
|
var outAddress = ctx[outPointerRegister];
|
||||||
|
if (outAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> handle = stackalloc byte[sizeof(int)];
|
||||||
|
System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(handle, 1);
|
||||||
|
_ = ctx.Memory.TryWrite(outAddress, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- NpTrophy2: trophy context/handle registration at boot ----
|
||||||
|
public static int NpTrophy2CreateContext(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||||
|
public static int NpTrophy2CreateHandle(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||||
|
public static int NpTrophy2RegisterContext(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "4IzqhhUQ3nk", ExportName = "sceNpTrophy2GetGameInfo",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2GetGameInfo(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
// ---- CES: Shift-JIS <-> Unicode conversion setup (Japanese text) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ZiDCxUUGbec", ExportName = "sceCesUcsProfileInitSJis1997Cp932",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceLibcInternal")]
|
||||||
|
public static int CesUcsProfileInitSJis1997Cp932(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "538bRGc6Zo8", ExportName = "sceCesMbcsUcsContextInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceLibcInternal")]
|
||||||
|
public static int CesMbcsUcsContextInit(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
// ---- NpUniversalDataSystem: gameplay telemetry events ----
|
||||||
|
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||||
|
public static int NpUniversalDataSystemPostEvent(CpuContext ctx) => Ok(ctx);
|
||||||
|
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx) => Ok(ctx);
|
||||||
|
}
|
||||||
@@ -13,6 +13,63 @@ public static class SystemServiceExports
|
|||||||
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
||||||
private const int SystemServiceStatusSize = 0x0C;
|
private const int SystemServiceStatusSize = 0x0C;
|
||||||
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
||||||
|
private const int HdrToneMapLuminanceSize = sizeof(float) * 3;
|
||||||
|
|
||||||
|
private const int TitleIdFieldSize = 0x10;
|
||||||
|
|
||||||
|
private static string? _mainAppTitleId;
|
||||||
|
|
||||||
|
public static void ConfigureApplicationInfo(string? titleId)
|
||||||
|
{
|
||||||
|
_mainAppTitleId = string.IsNullOrWhiteSpace(titleId) ? null : titleId.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3RQ5aQfnstU",
|
||||||
|
ExportName = "sceSystemServiceGetNoticeScreenSkipFlag",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceGetNoticeScreenSkipFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var flagAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (flagAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No system notice screen to skip in the emulator; report "do not skip".
|
||||||
|
Span<byte> flagBytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(flagBytes, 0);
|
||||||
|
return ctx.Memory.TryWrite(flagAddress, flagBytes)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4veE0XiIugA",
|
||||||
|
ExportName = "sceSystemServiceGetMainAppTitleId",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceGetMainAppTitleId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var titleIdAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (titleIdAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title IDs are a fixed 9-char format written into a 0x10-byte field;
|
||||||
|
// bound the length so a malformed param.json cannot drive an unbounded
|
||||||
|
// stack allocation or overrun the guest buffer.
|
||||||
|
var titleId = _mainAppTitleId ?? "PPSA00000";
|
||||||
|
var length = Math.Min(titleId.Length, TitleIdFieldSize - 1);
|
||||||
|
Span<byte> titleIdBytes = stackalloc byte[TitleIdFieldSize];
|
||||||
|
titleIdBytes.Clear();
|
||||||
|
System.Text.Encoding.ASCII.GetBytes(titleId.AsSpan(0, length), titleIdBytes);
|
||||||
|
return ctx.Memory.TryWrite(titleIdAddress, titleIdBytes[..(length + 1)])
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "fZo48un7LK4",
|
Nid = "fZo48un7LK4",
|
||||||
@@ -42,6 +99,35 @@ public static class SystemServiceExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SsC-m-S9JTA",
|
||||||
|
ExportName = "sceSystemServiceParamGetString",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceParamGetString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = unchecked((int)ctx[CpuRegister.Rdi]); // parameter id (nickname, etc.)
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var bufferSize = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (bufferAddress == 0 || bufferSize <= 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String params are typically the user nickname. Callers that gate UI
|
||||||
|
// or text setup on a successful read (and skip it on failure) stall on
|
||||||
|
// a black screen when this returns NOT_FOUND, so return a neutral
|
||||||
|
// non-empty default and success.
|
||||||
|
var value = System.Text.Encoding.UTF8.GetBytes("SharpEmu");
|
||||||
|
var writeLength = Math.Min(value.Length, bufferSize - 1);
|
||||||
|
Span<byte> output = stackalloc byte[writeLength + 1];
|
||||||
|
value.AsSpan(0, writeLength).CopyTo(output);
|
||||||
|
output[writeLength] = 0;
|
||||||
|
return ctx.Memory.TryWrite(bufferAddress, output)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "rPo6tV8D9bM",
|
Nid = "rPo6tV8D9bM",
|
||||||
ExportName = "sceSystemServiceGetStatus",
|
ExportName = "sceSystemServiceGetStatus",
|
||||||
@@ -87,6 +173,28 @@ public static class SystemServiceExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mPpPxv5CZt4",
|
||||||
|
ExportName = "sceSystemServiceGetHdrToneMapLuminance",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceGetHdrToneMapLuminance(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var luminanceAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (luminanceAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> luminance = stackalloc byte[HdrToneMapLuminanceSize];
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(luminance, 1000.0f);
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(luminance[sizeof(float)..], 1000.0f);
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(luminance[(sizeof(float) * 2)..], 0.01f);
|
||||||
|
return ctx.Memory.TryWrite(luminanceAddress, luminance)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Vo5V8KAwCmk",
|
Nid = "Vo5V8KAwCmk",
|
||||||
ExportName = "sceSystemServiceHideSplashScreen",
|
ExportName = "sceSystemServiceHideSplashScreen",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.UserService;
|
namespace SharpEmu.Libs.UserService;
|
||||||
|
|
||||||
@@ -13,15 +14,14 @@ public static class UserServiceExports
|
|||||||
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
||||||
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
||||||
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
||||||
// Retail user-service IDs begin at 0x3E8.
|
// Retail user ids encode their local user slot in the 0x10000000 range;
|
||||||
private const int PrimaryUserId = 1000;
|
// the first signed-in user maps to slot 0. Small emulator-local ids do
|
||||||
|
// not pass Unity's user-id-to-slot conversion and become slot -1.
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
private const int InvalidUserId = -1;
|
private const int InvalidUserId = -1;
|
||||||
private const string PrimaryUserName = "SharpEmu";
|
private const string PrimaryUserName = "SharpEmu";
|
||||||
private static int _loginEventDelivered;
|
private static int _loginEventDelivered;
|
||||||
|
|
||||||
private static readonly bool _traceUserService =
|
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"), "1", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "j3YMu1MVNNo",
|
Nid = "j3YMu1MVNNo",
|
||||||
ExportName = "sceUserServiceInitialize",
|
ExportName = "sceUserServiceInitialize",
|
||||||
@@ -29,6 +29,7 @@ public static class UserServiceExports
|
|||||||
LibraryName = "libSceUserService")]
|
LibraryName = "libSceUserService")]
|
||||||
public static int UserServiceInitialize(CpuContext ctx)
|
public static int UserServiceInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
Trace("initialize");
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -43,14 +44,12 @@ public static class UserServiceExports
|
|||||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||||
if (userIdAddress == 0)
|
if (userIdAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||||
? 0
|
? SetReturnWithTrace(ctx, 0, $"get_initial_user user={PrimaryUserId} out=0x{userIdAddress:X16}")
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
TraceUserService($"get_initial_user out=0x{userIdAddress:X16} value={PrimaryUserId} result=0x{result:X8}");
|
|
||||||
return ctx.SetReturn(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -63,7 +62,7 @@ public static class UserServiceExports
|
|||||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||||
if (userIdListAddress == 0)
|
if (userIdListAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||||
@@ -72,8 +71,12 @@ public static class UserServiceExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||||
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||||
? ctx.SetReturn(0)
|
? SetReturnWithTrace(
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
ctx,
|
||||||
|
0,
|
||||||
|
$"get_login_user_id_list users=[{PrimaryUserId},{InvalidUserId},{InvalidUserId},{InvalidUserId}] " +
|
||||||
|
$"out=0x{userIdListAddress:X16}")
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -86,20 +89,23 @@ public static class UserServiceExports
|
|||||||
var eventAddress = ctx[CpuRegister.Rdi];
|
var eventAddress = ctx[CpuRegister.Rdi];
|
||||||
if (eventAddress == 0)
|
if (eventAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
|
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
||||||
return ctx.Memory.TryWrite(eventAddress, payload)
|
return ctx.Memory.TryWrite(eventAddress, payload)
|
||||||
? ctx.SetReturn(0)
|
? SetReturnWithTrace(
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
ctx,
|
||||||
|
0,
|
||||||
|
$"get_event type=login user={PrimaryUserId} out=0x{eventAddress:X16}")
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -112,53 +118,30 @@ public static class UserServiceExports
|
|||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var nameAddress = ctx[CpuRegister.Rsi];
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
var capacity = ctx[CpuRegister.Rdx];
|
var capacity = ctx[CpuRegister.Rdx];
|
||||||
// Zero selects the current user.
|
if (userId != PrimaryUserId)
|
||||||
if (userId != 0 && userId != PrimaryUserId)
|
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||||
if (capacity <= (ulong)nameBytes.Length)
|
if (capacity <= (ulong)nameBytes.Length)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
|
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||||
nameBytes.CopyTo(output);
|
nameBytes.CopyTo(output);
|
||||||
var result = ctx.Memory.TryWrite(nameAddress, output)
|
return ctx.Memory.TryWrite(nameAddress, output)
|
||||||
? 0
|
? SetReturnWithTrace(
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
ctx,
|
||||||
TraceUserService(
|
0,
|
||||||
$"get_user_name user={userId} out=0x{nameAddress:X16} capacity=0x{capacity:X} value='{PrimaryUserName}' result=0x{result:X8}");
|
$"get_user_name user={userId} name='{PrimaryUserName}' out=0x{nameAddress:X16}")
|
||||||
return ctx.SetReturn(result);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "-sD02mFDBh4",
|
|
||||||
ExportName = "sceUserServiceGetGamePresets",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceUserService")]
|
|
||||||
public static int UserServiceGetGamePresets(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// Return deterministic defaults for the offline profile.
|
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var resultAddress = ctx[CpuRegister.Rcx];
|
|
||||||
Span<byte> defaults = stackalloc byte[0x18];
|
|
||||||
if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceUserService(
|
|
||||||
$"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " +
|
|
||||||
$"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
||||||
@@ -174,50 +157,165 @@ public static class UserServiceExports
|
|||||||
var valueAddress = ctx[CpuRegister.Rsi];
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (parameterId != 1000)
|
if (parameterId != 1000)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueAddress == 0)
|
if (valueAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteInt32(valueAddress, 0)
|
return TryWriteInt32(ctx, valueAddress, 0)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
#pragma warning restore SHEM006
|
#pragma warning restore SHEM006
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "woNpu+45RLk",
|
Nid = "woNpu+45RLk",
|
||||||
ExportName = "sceUserServiceGetAgeLevel",
|
ExportName = "sceUserServiceGetAgeLevel",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceUserService")]
|
LibraryName = "libSceUserService")]
|
||||||
public static int UserServiceGetAgeLevel(CpuContext ctx)
|
public static int UserServiceGetAgeLevel(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 18, "get_age_level");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-sD02mFDBh4",
|
||||||
|
ExportName = "sceUserServiceGetGamePresets",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetGamePresets(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var ageLevelAddress = ctx[CpuRegister.Rsi];
|
var presetsAddress = ctx[CpuRegister.Rsi];
|
||||||
if (userId != 1000)
|
if (userId != PrimaryUserId)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ageLevelAddress == 0)
|
if (presetsAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report an adult account so titles skip parental-restriction paths.
|
Span<byte> presets = stackalloc byte[0x28];
|
||||||
return ctx.TryWriteInt32(ageLevelAddress, 21)
|
presets.Clear();
|
||||||
? ctx.SetReturn(0)
|
BinaryPrimitives.WriteUInt64LittleEndian(presets, (ulong)presets.Length);
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
if (!ctx.Memory.TryWrite(presetsAddress, presets))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturnWithTrace(
|
||||||
|
ctx,
|
||||||
|
0,
|
||||||
|
$"get_game_presets user={userId} out=0x{presetsAddress:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceUserService(string message)
|
[SysAbiExport(
|
||||||
|
Nid = "rnEhHqG-4xo",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityChatTranscription",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityChatTranscription(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_chat_transcription");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ZKJtxdgvzwg",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityPressAndHoldDelay",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityPressAndHoldDelay(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_press_and_hold_delay");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-3Y5GO+-i78",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityTriggerEffect",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityTriggerEffect(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_trigger_effect");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "qWYHOFwqCxY",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityVibration",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityVibration(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 1, "get_accessibility_vibration");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hD-H81EN9Vg",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityZoomEnabled",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityZoomEnabled(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_zoom_enabled");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "O6IW1-Dwm-w",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityZoomFollowFocus",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityZoomFollowFocus(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_zoom_follow_focus");
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
{
|
{
|
||||||
if (_traceUserService)
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteUserSettingInt32(CpuContext ctx, int value, string operation)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId != PrimaryUserId)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, valueAddress, value))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturnWithTrace(
|
||||||
|
ctx,
|
||||||
|
0,
|
||||||
|
$"{operation} user={userId} value={value} out=0x{valueAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturnWithTrace(CpuContext ctx, int result, string message)
|
||||||
|
{
|
||||||
|
Trace(message);
|
||||||
|
return SetReturn(ctx, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
|
||||||
|
? frame.ReturnRip
|
||||||
|
: 0;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] user_service.{message} ret=0x{returnRip:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user