mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fdf76308d | |||
| 7b46921546 |
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 69 KiB |
@@ -7,8 +7,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
- "**/*.png"
|
- "**/*.png"
|
||||||
@@ -55,11 +53,6 @@ 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}"
|
||||||
@@ -210,9 +203,7 @@ jobs:
|
|||||||
- init
|
- init
|
||||||
- build
|
- build
|
||||||
- build-posix
|
- build-posix
|
||||||
# Versioned releases are immutable and tag-driven. Branch and manual runs
|
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
# 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
|
||||||
@@ -222,7 +213,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Create release
|
- name: Create or update release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_REPO: ${{ github.repository }}
|
GH_REPO: ${{ github.repository }}
|
||||||
@@ -240,11 +231,26 @@ 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
|
||||||
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
gh release upload "${RELEASE_TAG}" "${assets[@]}" --clobber
|
||||||
exit 1
|
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
|
||||||
|
else
|
||||||
|
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||||
|
--title "${RELEASE_NAME}" \
|
||||||
|
--notes "${notes}" \
|
||||||
|
--target "${GITHUB_SHA}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
keep=()
|
||||||
--verify-tag \
|
for asset_path in "${assets[@]}"; do
|
||||||
--title "${RELEASE_NAME}" \
|
keep+=("$(basename "${asset_path}")")
|
||||||
--notes "${notes}"
|
done
|
||||||
|
mapfile -t release_assets < <(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort)
|
||||||
|
for asset in "${release_assets[@]}"; do
|
||||||
|
case "${asset}" in
|
||||||
|
sharpemu-${VERSION}-*.zip|sharpemu-${VERSION}-*.tar.gz)
|
||||||
|
if ! printf '%s\n' "${keep[@]}" | grep -Fxq "${asset}"; then
|
||||||
|
gh release delete-asset "${RELEASE_TAG}" "${asset}" --yes
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|||||||
@@ -41,16 +41,6 @@ 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.
|
||||||
@@ -98,6 +88,17 @@ 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
|
||||||
|
|
||||||
|
| Demons Souls Remake | Dreaming Sarah |
|
||||||
|
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
| Void Terrarium | Dead Cells |
|
||||||
|
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
|
||||||
> [!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.
|
Before Width: | Height: | Size: 653 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,408 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
@@ -5127,51 +5127,64 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
BindTlsBase(context);
|
BindTlsBase(context);
|
||||||
byte* ptr2 = (byte*)ptr;
|
byte* ptr2 = (byte*)ptr;
|
||||||
ulong hostRspSlot = (ulong)hostRspStorage;
|
ulong hostRspSlot = (ulong)hostRspStorage;
|
||||||
var emitter = new NativeCodeEmitter(ptr2);
|
int offset = 0;
|
||||||
|
|
||||||
emitter.Emit(0x53); // push rbx
|
void Emit(byte value) => ptr2[offset++] = value;
|
||||||
emitter.Emit(0x55); // push rbp
|
void EmitU64(ulong value)
|
||||||
emitter.Emit(0x57); // push rdi
|
{
|
||||||
emitter.Emit(0x56); // push rsi
|
*(ulong*)(ptr2 + offset) = value;
|
||||||
emitter.Emit(0x41); emitter.Emit(0x54); // push r12
|
offset += sizeof(ulong);
|
||||||
emitter.Emit(0x41); emitter.Emit(0x55); // push r13
|
}
|
||||||
emitter.Emit(0x41); emitter.Emit(0x56); // push r14
|
void EmitMovR64Imm(byte rex, byte opcode, ulong value)
|
||||||
emitter.Emit(0x41); emitter.Emit(0x57); // push r15
|
{
|
||||||
EmitHostNonvolatileXmmSave(ptr2, ref emitter.Offset);
|
Emit(rex);
|
||||||
|
Emit(opcode);
|
||||||
|
EmitU64(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(0x53); // push rbx
|
||||||
|
Emit(0x55); // push rbp
|
||||||
|
Emit(0x57); // push rdi
|
||||||
|
Emit(0x56); // push rsi
|
||||||
|
Emit(0x41); Emit(0x54); // push r12
|
||||||
|
Emit(0x41); Emit(0x55); // push r13
|
||||||
|
Emit(0x41); Emit(0x56); // push r14
|
||||||
|
Emit(0x41); Emit(0x57); // push r15
|
||||||
|
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||||
// Restore the fiber's floating-point control environment before
|
// Restore the fiber's floating-point control environment before
|
||||||
// abandoning the host stack. This path is used when a blocked guest
|
// abandoning the host stack. This path is used when a blocked guest
|
||||||
// continuation migrates to another managed worker.
|
// continuation migrates to another managed worker.
|
||||||
emitter.Emit(0x48); emitter.Emit(0x83); emitter.Emit(0xEC); emitter.Emit(0x08); // sub rsp,8
|
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x08); // sub rsp,8
|
||||||
emitter.Emit(0xC7); emitter.Emit(0x04); emitter.Emit(0x24); // mov dword [rsp],imm32
|
Emit(0xC7); Emit(0x04); Emit(0x24); // mov dword [rsp],imm32
|
||||||
emitter.Emit(context.Mxcsr);
|
*(uint*)(ptr2 + offset) = context.Mxcsr; offset += sizeof(uint);
|
||||||
emitter.Emit(0x0F); emitter.Emit(0xAE); emitter.Emit(0x14); emitter.Emit(0x24); // ldmxcsr [rsp]
|
Emit(0x0F); Emit(0xAE); Emit(0x14); Emit(0x24); // ldmxcsr [rsp]
|
||||||
emitter.Emit(0x66); emitter.Emit(0xC7); emitter.Emit(0x04); emitter.Emit(0x24); // mov word [rsp],imm16
|
Emit(0x66); Emit(0xC7); Emit(0x04); Emit(0x24); // mov word [rsp],imm16
|
||||||
emitter.Emit(context.FpuControlWord);
|
*(ushort*)(ptr2 + offset) = context.FpuControlWord; offset += sizeof(ushort);
|
||||||
emitter.Emit(0xD9); emitter.Emit(0x2C); emitter.Emit(0x24); // fldcw [rsp]
|
Emit(0xD9); Emit(0x2C); Emit(0x24); // fldcw [rsp]
|
||||||
emitter.Emit(0x48); emitter.Emit(0x83); emitter.Emit(0xC4); emitter.Emit(0x08); // add rsp,8
|
Emit(0x48); Emit(0x83); Emit(0xC4); Emit(0x08); // add rsp,8
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBA, hostRspSlot); // mov r10, hostRspSlot
|
EmitMovR64Imm(0x49, 0xBA, hostRspSlot); // mov r10, hostRspSlot
|
||||||
emitter.Emit(0x49); emitter.Emit(0x89); emitter.Emit(0x22); // mov [r10], rsp
|
Emit(0x49); Emit(0x89); Emit(0x22); // mov [r10], rsp
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xB8, context[CpuRegister.Rsp]); // mov rax, guest rsp
|
EmitMovR64Imm(0x48, 0xB8, context[CpuRegister.Rsp]); // mov rax, guest rsp
|
||||||
emitter.Emit(0x48); emitter.Emit(0x89); emitter.Emit(0xC4); // mov rsp, rax
|
Emit(0x48); Emit(0x89); Emit(0xC4); // mov rsp, rax
|
||||||
emitter.Emit(0x48); emitter.Emit(0x83); emitter.Emit(0xEC); emitter.Emit(0x08); // reserve transfer slot
|
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x08); // reserve transfer slot
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xB8, entryPoint); // mov rax, entryPoint
|
EmitMovR64Imm(0x48, 0xB8, entryPoint); // mov rax, entryPoint
|
||||||
emitter.Emit(0x48); emitter.Emit(0x89); emitter.Emit(0x04); emitter.Emit(0x24); // mov [rsp],rax
|
Emit(0x48); Emit(0x89); Emit(0x04); Emit(0x24); // mov [rsp],rax
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xBB, context[CpuRegister.Rbx]); // mov rbx, imm64
|
EmitMovR64Imm(0x48, 0xBB, context[CpuRegister.Rbx]); // mov rbx, imm64
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xBD, context[CpuRegister.Rbp]); // mov rbp, imm64
|
EmitMovR64Imm(0x48, 0xBD, context[CpuRegister.Rbp]); // mov rbp, imm64
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xBF, context[CpuRegister.Rdi]); // mov rdi, imm64
|
EmitMovR64Imm(0x48, 0xBF, context[CpuRegister.Rdi]); // mov rdi, imm64
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xBE, context[CpuRegister.Rsi]); // mov rsi, imm64
|
EmitMovR64Imm(0x48, 0xBE, context[CpuRegister.Rsi]); // mov rsi, imm64
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xBA, context[CpuRegister.Rdx]); // mov rdx, imm64
|
EmitMovR64Imm(0x48, 0xBA, context[CpuRegister.Rdx]); // mov rdx, imm64
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xB9, context[CpuRegister.Rcx]); // mov rcx, imm64
|
EmitMovR64Imm(0x48, 0xB9, context[CpuRegister.Rcx]); // mov rcx, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xB8, context[CpuRegister.R8]); // mov r8, imm64
|
EmitMovR64Imm(0x49, 0xB8, context[CpuRegister.R8]); // mov r8, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xB9, context[CpuRegister.R9]); // mov r9, imm64
|
EmitMovR64Imm(0x49, 0xB9, context[CpuRegister.R9]); // mov r9, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBA, context[CpuRegister.R10]); // mov r10, imm64
|
EmitMovR64Imm(0x49, 0xBA, context[CpuRegister.R10]); // mov r10, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBC, context[CpuRegister.R12]); // mov r12, imm64
|
EmitMovR64Imm(0x49, 0xBC, context[CpuRegister.R12]); // mov r12, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBD, context[CpuRegister.R13]); // mov r13, imm64
|
EmitMovR64Imm(0x49, 0xBD, context[CpuRegister.R13]); // mov r13, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBE, context[CpuRegister.R14]); // mov r14, imm64
|
EmitMovR64Imm(0x49, 0xBE, context[CpuRegister.R14]); // mov r14, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBF, context[CpuRegister.R15]); // mov r15, imm64
|
EmitMovR64Imm(0x49, 0xBF, context[CpuRegister.R15]); // mov r15, imm64
|
||||||
emitter.EmitMovR64Immediate(0x49, 0xBB, context[CpuRegister.R11]); // mov r11, imm64
|
EmitMovR64Imm(0x49, 0xBB, context[CpuRegister.R11]); // mov r11, imm64
|
||||||
emitter.EmitMovR64Immediate(0x48, 0xB8, context[CpuRegister.Rax]); // mov rax, imm64
|
EmitMovR64Imm(0x48, 0xB8, context[CpuRegister.Rax]); // mov rax, imm64
|
||||||
emitter.Emit(0xC3); // ret through the synthetic transfer slot
|
Emit(0xC3); // ret through the synthetic transfer slot
|
||||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||||
if (returnSlotAddress == 0 || !context.TryWriteUInt64(returnSlotAddress, (ulong)_guestReturnStub))
|
if (returnSlotAddress == 0 || !context.TryWriteUInt64(returnSlotAddress, (ulong)_guestReturnStub))
|
||||||
{
|
{
|
||||||
@@ -5235,45 +5248,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The continuation trampoline is rebuilt on every blocked-thread resume.
|
|
||||||
// Keep its tiny writer on the stack: capturing local emit functions create a
|
|
||||||
// managed display-class allocation on this extremely hot path.
|
|
||||||
private unsafe ref struct NativeCodeEmitter(byte* code)
|
|
||||||
{
|
|
||||||
private readonly byte* _code = code;
|
|
||||||
public int Offset;
|
|
||||||
|
|
||||||
public void Emit(byte value)
|
|
||||||
{
|
|
||||||
_code[Offset++] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Emit(ushort value)
|
|
||||||
{
|
|
||||||
*(ushort*)(_code + Offset) = value;
|
|
||||||
Offset += sizeof(ushort);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Emit(uint value)
|
|
||||||
{
|
|
||||||
*(uint*)(_code + Offset) = value;
|
|
||||||
Offset += sizeof(uint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Emit(ulong value)
|
|
||||||
{
|
|
||||||
*(ulong*)(_code + Offset) = value;
|
|
||||||
Offset += sizeof(ulong);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EmitMovR64Immediate(byte rex, byte opcode, ulong value)
|
|
||||||
{
|
|
||||||
Emit(rex);
|
|
||||||
Emit(opcode);
|
|
||||||
Emit(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong AlignDown(ulong value, ulong alignment)
|
private static ulong AlignDown(ulong value, ulong alignment)
|
||||||
{
|
{
|
||||||
if (alignment == 0)
|
if (alignment == 0)
|
||||||
|
|||||||
@@ -197,9 +197,8 @@ 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}): {reason}.");
|
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
imageBase = allocatedBase;
|
imageBase = allocatedBase;
|
||||||
|
|||||||
@@ -198,24 +198,6 @@ 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)
|
||||||
|
|||||||
@@ -141,8 +141,6 @@
|
|||||||
"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!",
|
||||||
|
|||||||
@@ -131,8 +131,6 @@
|
|||||||
"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!"
|
||||||
|
|||||||
@@ -333,35 +333,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Classes="sectionTitle"
|
Classes="sectionTitle"
|
||||||
Text="ABOUT" />
|
Text="ABOUT" />
|
||||||
|
|
||||||
<!--Latest commit info-->
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
||||||
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
||||||
Width="24" Height="24" VerticalAlignment="Center" />
|
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
<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>
|
</StackPanel>
|
||||||
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
||||||
Content="Check for updates" VerticalAlignment="Center" />
|
Content="Check for updates" VerticalAlignment="Center" />
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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;
|
||||||
|
|
||||||
@@ -79,10 +78,6 @@ 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();
|
||||||
@@ -200,21 +195,6 @@ 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>
|
||||||
@@ -256,91 +236,6 @@ 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()
|
||||||
@@ -486,8 +381,6 @@ public partial class MainWindow : Window
|
|||||||
ApplySettingsToControls();
|
ApplySettingsToControls();
|
||||||
LocateEmulator();
|
LocateEmulator();
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
_ = LoadLatestCommitAsync();
|
|
||||||
|
|
||||||
if (_settings.CheckForUpdatesOnStartup)
|
if (_settings.CheckForUpdatesOnStartup)
|
||||||
{
|
{
|
||||||
_ = CheckForUpdatesAsync();
|
_ = CheckForUpdatesAsync();
|
||||||
@@ -623,8 +516,6 @@ public partial class MainWindow : Window
|
|||||||
GithubButton.Content = loc.Get("About.GithubButton");
|
GithubButton.Content = loc.Get("About.GithubButton");
|
||||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||||
UpdateLabel.Text = loc.Get("Updater.Label");
|
UpdateLabel.Text = loc.Get("Updater.Label");
|
||||||
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
|
|
||||||
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
|
|
||||||
RefreshUpdateText();
|
RefreshUpdateText();
|
||||||
|
|
||||||
UpdateEmptyStateTexts();
|
UpdateEmptyStateTexts();
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ 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>
|
||||||
|
|||||||
+100
-150
@@ -121,7 +121,6 @@ public static partial class AgcExports
|
|||||||
private const uint CbBlend0Control = 0x1E0;
|
private const uint CbBlend0Control = 0x1E0;
|
||||||
private const uint PaScModeCntl0 = 0x292;
|
private const uint PaScModeCntl0 = 0x292;
|
||||||
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
||||||
private const uint DbRenderControl = 0x000;
|
|
||||||
private const uint DbDepthView = 0x002;
|
private const uint DbDepthView = 0x002;
|
||||||
private const uint DbDepthSizeXy = 0x007;
|
private const uint DbDepthSizeXy = 0x007;
|
||||||
private const uint DbDepthClear = 0x00B;
|
private const uint DbDepthClear = 0x00B;
|
||||||
@@ -3729,16 +3728,13 @@ public static partial class AgcExports
|
|||||||
_labelProducers.Add(producer);
|
_labelProducers.Add(producer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceAgc)
|
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(memory, address, length))
|
||||||
{
|
{
|
||||||
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(memory, address, length))
|
TraceAgc(
|
||||||
{
|
$"agc.wait_producer_scheduled label=0x{waiting.Address:X16} " +
|
||||||
TraceAgc(
|
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
||||||
$"agc.wait_producer_scheduled label=0x{waiting.Address:X16} " +
|
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
||||||
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
$"packet=0x{packetAddress:X16} action='{debugName}'");
|
||||||
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
|
||||||
$"packet=0x{packetAddress:X16} action='{debugName}'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return producer;
|
return producer;
|
||||||
@@ -3756,19 +3752,16 @@ public static partial class AgcExports
|
|||||||
producer.Completed = true;
|
producer.Completed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceAgc)
|
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(
|
||||||
|
producer.Memory,
|
||||||
|
producer.Address,
|
||||||
|
producer.Length))
|
||||||
{
|
{
|
||||||
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(
|
TraceAgc(
|
||||||
producer.Memory,
|
$"agc.wait_producer_completed label=0x{waiting.Address:X16} " +
|
||||||
producer.Address,
|
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
||||||
producer.Length))
|
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
||||||
{
|
$"action='{producer.DebugName}'");
|
||||||
TraceAgc(
|
|
||||||
$"agc.wait_producer_completed label=0x{waiting.Address:X16} " +
|
|
||||||
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
|
||||||
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
|
||||||
$"action='{producer.DebugName}'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3813,14 +3806,6 @@ public static partial class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Producer-backed waits are trace-only. Keep the producer lookup above
|
|
||||||
// because producerless waits are always warned, but do not build the
|
|
||||||
// detailed condition strings when AGC tracing is disabled.
|
|
||||||
if (producer is not null && !_traceAgc)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefix = stale ? "agc.wait_stale" : "agc.wait_suspended";
|
var prefix = stale ? "agc.wait_stale" : "agc.wait_suspended";
|
||||||
var current = currentValue.HasValue
|
var current = currentValue.HasValue
|
||||||
? $"0x{currentValue.Value:X16}"
|
? $"0x{currentValue.Value:X16}"
|
||||||
@@ -5306,7 +5291,7 @@ public static partial class AgcExports
|
|||||||
var hasDepthOnlyCandidate = hasExportShader &&
|
var hasDepthOnlyCandidate = hasExportShader &&
|
||||||
!hasPixelShader &&
|
!hasPixelShader &&
|
||||||
depthTarget is not null &&
|
depthTarget is not null &&
|
||||||
(depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
|
(depthState.TestEnable || depthState.WriteEnable);
|
||||||
if (hasDepthOnlyCandidate &&
|
if (hasDepthOnlyCandidate &&
|
||||||
TryCreateTranslatedDepthOnlyGuestDraw(
|
TryCreateTranslatedDepthOnlyGuestDraw(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -5951,14 +5936,13 @@ public static partial class AgcExports
|
|||||||
// Every bound color target the shader exports to. Deferred renderers
|
// Every bound color target the shader exports to. Deferred renderers
|
||||||
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
||||||
// Fall back to slot 0 if we cannot match any export to a bound target.
|
// Fall back to slot 0 if we cannot match any export to a bound target.
|
||||||
var pixelColorExportMasks = pixelState.Program.PixelColorExportMasks;
|
|
||||||
var allBoundTargets = GetRenderTargets(state.CxRegisters);
|
var allBoundTargets = GetRenderTargets(state.CxRegisters);
|
||||||
// At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
|
// At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
|
||||||
// closure allocations. Slots are distinct, so sorting by slot is stable.
|
// closure allocations. Slots are distinct, so sorting by slot is stable.
|
||||||
var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
|
var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
|
||||||
foreach (var target in allBoundTargets)
|
foreach (var target in allBoundTargets)
|
||||||
{
|
{
|
||||||
if (GetPixelColorExportMask(pixelColorExportMasks, target.Slot) != 0)
|
if (HasPixelColorExport(pixelState, target.Slot))
|
||||||
{
|
{
|
||||||
selectedTargets.Add(target);
|
selectedTargets.Add(target);
|
||||||
}
|
}
|
||||||
@@ -5981,7 +5965,7 @@ public static partial class AgcExports
|
|||||||
{
|
{
|
||||||
TraceAgcShader(
|
TraceAgcShader(
|
||||||
$"agc.mrt_filter ps=0x{pixelShaderAddress:X16} " +
|
$"agc.mrt_filter ps=0x{pixelShaderAddress:X16} " +
|
||||||
$"bound=[{string.Join(",", allBoundTargets.Select(t => $"s{t.Slot}:0x{t.Address:X}:exp{(GetPixelColorExportMask(pixelColorExportMasks, t.Slot) != 0 ? 1 : 0)}"))}] " +
|
$"bound=[{string.Join(",", allBoundTargets.Select(t => $"s{t.Slot}:0x{t.Address:X}:exp{(HasPixelColorExport(pixelState, t.Slot) ? 1 : 0)}"))}] " +
|
||||||
$"kept={renderTargets.Length}");
|
$"kept={renderTargets.Length}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6102,39 +6086,54 @@ public static partial class AgcExports
|
|||||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var imageBindings = pixelEvaluation.ImageBindings
|
||||||
|
.Concat(exportEvaluation.ImageBindings);
|
||||||
var textures = new List<TranslatedImageBinding>(
|
var textures = new List<TranslatedImageBinding>(
|
||||||
pixelEvaluation.ImageBindings.Count +
|
pixelEvaluation.ImageBindings.Count +
|
||||||
exportEvaluation.ImageBindings.Count);
|
exportEvaluation.ImageBindings.Count);
|
||||||
if (!TryAppendTranslatedImageBindings(
|
foreach (var binding in imageBindings)
|
||||||
pixelEvaluation.ImageBindings,
|
|
||||||
textures,
|
|
||||||
pixelShaderAddress,
|
|
||||||
exportShaderAddress,
|
|
||||||
out error) ||
|
|
||||||
!TryAppendTranslatedImageBindings(
|
|
||||||
exportEvaluation.ImageBindings,
|
|
||||||
textures,
|
|
||||||
pixelShaderAddress,
|
|
||||||
exportShaderAddress,
|
|
||||||
out error))
|
|
||||||
{
|
{
|
||||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
if (!TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture))
|
||||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
{
|
||||||
return false;
|
// A garbage/zeroed texture descriptor (from a per-draw descriptor
|
||||||
|
// setup race — the same root as scalar-load-failed) would drop
|
||||||
|
// the whole draw, so Demon's Souls' deferred-lighting/composite
|
||||||
|
// passes that produce the composite's feeder targets never run
|
||||||
|
// and the frame stays black. Substitute a 1x1 fallback binding so
|
||||||
|
// the pass still renders (that one sampled texture reads black)
|
||||||
|
// rather than dropping it entirely. STRICT reverts.
|
||||||
|
if (_strictShaderDescriptors)
|
||||||
|
{
|
||||||
|
error = $"invalid texture descriptor at pc=0x{binding.Pc:X}";
|
||||||
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||||
|
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture = new TextureDescriptor(
|
||||||
|
0, 1, 1, Gen5TextureFormatR8G8B8A8Unorm, 0, 0, 0, 0, 0, 1, 0xFAC);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_traceAgcShader || _tracePixelShaderAddress == pixelShaderAddress)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][TRACE] " +
|
||||||
|
$"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " +
|
||||||
|
$"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode) ? 1 : 0)} " +
|
||||||
|
$"decoded={FormatTextureDescriptor(texture)} " +
|
||||||
|
$"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}");
|
||||||
|
}
|
||||||
|
textures.Add(
|
||||||
|
new TranslatedImageBinding(
|
||||||
|
texture,
|
||||||
|
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode),
|
||||||
|
binding.MipLevel ?? 0,
|
||||||
|
binding.SamplerDescriptor));
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalMemoryBindings = new Gen5GlobalMemoryBinding[
|
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||||
exportEvaluation.GlobalMemoryBindings.Count];
|
.ToArray();
|
||||||
for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++)
|
|
||||||
{
|
|
||||||
globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index];
|
|
||||||
}
|
|
||||||
for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++)
|
|
||||||
{
|
|
||||||
globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] =
|
|
||||||
exportEvaluation.GlobalMemoryBindings[index];
|
|
||||||
}
|
|
||||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||||
exportEvaluation.VertexInputs ?? [];
|
exportEvaluation.VertexInputs ?? [];
|
||||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||||
@@ -6149,13 +6148,6 @@ public static partial class AgcExports
|
|||||||
renderTargets[index].NumberType);
|
renderTargets[index].NumberType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pixelUserDataCount = Math.Min(pixelEvaluation.InitialScalarRegisters.Count, 8);
|
|
||||||
var pixelUserData = new uint[pixelUserDataCount];
|
|
||||||
for (var index = 0; index < pixelUserDataCount; index++)
|
|
||||||
{
|
|
||||||
pixelUserData[index] = pixelEvaluation.InitialScalarRegisters[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
draw = new TranslatedGuestDraw(
|
draw = new TranslatedGuestDraw(
|
||||||
exportShaderAddress,
|
exportShaderAddress,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
@@ -6173,11 +6165,11 @@ public static partial class AgcExports
|
|||||||
DecodeDepthTarget(state.CxRegisters),
|
DecodeDepthTarget(state.CxRegisters),
|
||||||
guestTargets,
|
guestTargets,
|
||||||
ApplyTransparentPremultipliedFillClear(
|
ApplyTransparentPremultipliedFillClear(
|
||||||
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||||
textures,
|
textures,
|
||||||
vertexInputs,
|
vertexInputs,
|
||||||
pixelEvaluation.InitialScalarRegisters),
|
pixelEvaluation.InitialScalarRegisters),
|
||||||
pixelUserData,
|
pixelEvaluation.InitialScalarRegisters.Take(8).ToArray(),
|
||||||
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
||||||
state.CxRegisters.TryGetValue(
|
state.CxRegisters.TryGetValue(
|
||||||
CbColor0Info + renderTargets.FirstOrDefault().Slot * CbColorRegisterStride,
|
CbColor0Info + renderTargets.FirstOrDefault().Slot * CbColorRegisterStride,
|
||||||
@@ -6189,55 +6181,6 @@ public static partial class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryAppendTranslatedImageBindings(
|
|
||||||
IReadOnlyList<Gen5ImageBinding> bindings,
|
|
||||||
List<TranslatedImageBinding> textures,
|
|
||||||
ulong pixelShaderAddress,
|
|
||||||
ulong exportShaderAddress,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
foreach (var binding in bindings)
|
|
||||||
{
|
|
||||||
if (!TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture))
|
|
||||||
{
|
|
||||||
// A garbage/zeroed texture descriptor (from a per-draw descriptor
|
|
||||||
// setup race — the same root as scalar-load-failed) would drop
|
|
||||||
// the whole draw, so deferred-lighting/composite passes that
|
|
||||||
// produce the composite's feeder targets never run. Keep the
|
|
||||||
// existing 1x1 fallback unless strict diagnostics are requested.
|
|
||||||
if (_strictShaderDescriptors)
|
|
||||||
{
|
|
||||||
error = $"invalid texture descriptor at pc=0x{binding.Pc:X}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
texture = new TextureDescriptor(
|
|
||||||
0, 1, 1, Gen5TextureFormatR8G8B8A8Unorm, 0, 0, 0, 0, 0, 1, 0xFAC);
|
|
||||||
}
|
|
||||||
|
|
||||||
var isStorage =
|
|
||||||
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
|
||||||
if (_traceAgcShader || _tracePixelShaderAddress == pixelShaderAddress)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][TRACE] " +
|
|
||||||
$"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " +
|
|
||||||
$"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(isStorage ? 1 : 0)} " +
|
|
||||||
$"decoded={FormatTextureDescriptor(texture)} " +
|
|
||||||
$"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}");
|
|
||||||
}
|
|
||||||
textures.Add(
|
|
||||||
new TranslatedImageBinding(
|
|
||||||
texture,
|
|
||||||
isStorage,
|
|
||||||
binding.MipLevel ?? 0,
|
|
||||||
binding.SamplerDescriptor));
|
|
||||||
}
|
|
||||||
|
|
||||||
error = string.Empty;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int _tracedAstroTitlePixelGlobals;
|
private static int _tracedAstroTitlePixelGlobals;
|
||||||
private static int _tracedAstroTitlePixelGlobalProbe;
|
private static int _tracedAstroTitlePixelGlobalProbe;
|
||||||
|
|
||||||
@@ -6466,10 +6409,26 @@ public static partial class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint GetPixelColorExportMask(uint packedMasks, uint target) =>
|
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||||
target < ColorTargetCount
|
GetPixelColorExportMask(state, target) != 0;
|
||||||
? (packedMasks >> (int)(target * 4)) & 0xFu
|
|
||||||
: 0;
|
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target)
|
||||||
|
{
|
||||||
|
// Called per render target (twice per draw via CreateRenderState +
|
||||||
|
// HasPixelColorExport); a manual scan avoids the per-call LINQ iterator
|
||||||
|
// and closure allocations. Same result as the previous
|
||||||
|
// Select/OfType/Where/Aggregate chain.
|
||||||
|
var mask = 0u;
|
||||||
|
foreach (var instruction in state.Program.Instructions)
|
||||||
|
{
|
||||||
|
if (instruction.Control is Gen5ExportControl export && export.Target == target)
|
||||||
|
{
|
||||||
|
mask |= export.EnableMask & 0xFu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||||
{
|
{
|
||||||
@@ -6667,7 +6626,7 @@ public static partial class AgcExports
|
|||||||
private static GuestRenderState CreateRenderState(
|
private static GuestRenderState CreateRenderState(
|
||||||
IReadOnlyDictionary<uint, uint> registers,
|
IReadOnlyDictionary<uint, uint> registers,
|
||||||
IReadOnlyList<RenderTargetDescriptor> targets,
|
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||||
uint pixelColorExportMasks)
|
Gen5ShaderState pixelState)
|
||||||
{
|
{
|
||||||
if (targets.Count == 0)
|
if (targets.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -6676,21 +6635,15 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
var target = targets[0];
|
var target = targets[0];
|
||||||
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||||
var blends = new GuestBlendState[targets.Count];
|
|
||||||
for (var index = 0; index < targets.Count; index++)
|
|
||||||
{
|
|
||||||
var blend = DecodeBlendState(registers, targets[index].Slot);
|
|
||||||
blends[index] = blend with
|
|
||||||
{
|
|
||||||
WriteMask = blend.WriteMask &
|
|
||||||
GetPixelColorExportMask(
|
|
||||||
pixelColorExportMasks,
|
|
||||||
targets[index].Slot),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GuestRenderState(
|
return new GuestRenderState(
|
||||||
blends,
|
targets.Select(target =>
|
||||||
|
{
|
||||||
|
var blend = DecodeBlendState(registers, target.Slot);
|
||||||
|
return blend with
|
||||||
|
{
|
||||||
|
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
|
||||||
|
};
|
||||||
|
}).ToArray(),
|
||||||
scissor,
|
scissor,
|
||||||
DecodeViewport(registers, target.Width, target.Height, scissor),
|
DecodeViewport(registers, target.Width, target.Height, scissor),
|
||||||
DecodeRasterState(registers),
|
DecodeRasterState(registers),
|
||||||
@@ -6699,30 +6652,27 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
|
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
|
||||||
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering).
|
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering).
|
||||||
// DB_RENDER_CONTROL (context register 0x000): DEPTH_CLEAR_ENABLE bit0.
|
|
||||||
private const uint DbDepthControl = 0x200;
|
private const uint DbDepthControl = 0x200;
|
||||||
|
|
||||||
internal static GuestDepthState DecodeDepthState(
|
private static GuestDepthState DecodeDepthState(
|
||||||
IReadOnlyDictionary<uint, uint> registers)
|
IReadOnlyDictionary<uint, uint> registers)
|
||||||
{
|
{
|
||||||
var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
|
if (!registers.TryGetValue(DbDepthControl, out var control))
|
||||||
registers.TryGetValue(DbRenderControl, out var renderControl);
|
{
|
||||||
|
return GuestDepthState.Default;
|
||||||
|
}
|
||||||
|
|
||||||
var testEnable = (control & 0x2u) != 0;
|
var testEnable = (control & 0x2u) != 0;
|
||||||
var writeEnable = (control & 0x4u) != 0;
|
var writeEnable = (control & 0x4u) != 0;
|
||||||
var compareOp = hasDepthControl
|
var compareOp = (control >> 4) & 0x7u;
|
||||||
? (control >> 4) & 0x7u
|
return new GuestDepthState(testEnable, writeEnable, compareOp);
|
||||||
: GuestDepthState.Default.CompareOp;
|
|
||||||
var clearEnable = (renderControl & 0x1u) != 0;
|
|
||||||
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GuestDepthTarget? DecodeDepthTarget(
|
private static GuestDepthTarget? DecodeDepthTarget(
|
||||||
IReadOnlyDictionary<uint, uint> registers)
|
IReadOnlyDictionary<uint, uint> registers)
|
||||||
{
|
{
|
||||||
var depthState = DecodeDepthState(registers);
|
var depthState = DecodeDepthState(registers);
|
||||||
if (!depthState.TestEnable &&
|
if (!depthState.TestEnable && !depthState.WriteEnable)
|
||||||
!depthState.WriteEnable &&
|
|
||||||
!depthState.ClearEnable)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,19 +52,8 @@ internal static class AudioPcmConversion
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||||
return ConvertFloatSample(BitConverter.Int32BitsToSingle(bits));
|
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||||
}
|
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)
|
private static short ApplyVolume(short sample, float volume)
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@@ -74,85 +74,6 @@ public static class FontExports
|
|||||||
public static int CreateRendererWithEdition(CpuContext ctx) =>
|
public static int CreateRendererWithEdition(CpuContext ctx) =>
|
||||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "cKYtVmeSTcw",
|
Nid = "cKYtVmeSTcw",
|
||||||
ExportName = "sceFontOpenFontSet",
|
ExportName = "sceFontOpenFontSet",
|
||||||
|
|||||||
@@ -91,10 +91,9 @@ internal readonly record struct GuestRasterState(
|
|||||||
internal readonly record struct GuestDepthState(
|
internal readonly record struct GuestDepthState(
|
||||||
bool TestEnable,
|
bool TestEnable,
|
||||||
bool WriteEnable,
|
bool WriteEnable,
|
||||||
uint CompareOp,
|
uint CompareOp)
|
||||||
bool ClearEnable = false)
|
|
||||||
{
|
{
|
||||||
public static GuestDepthState Default { get; } = new(false, false, 7, false);
|
public static GuestDepthState Default { get; } = new(false, false, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
||||||
|
|||||||
@@ -127,19 +127,6 @@ public static class PadExports
|
|||||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
: 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(OrbisPadErrorInvalidHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gjP9-KQzoUk",
|
Nid = "gjP9-KQzoUk",
|
||||||
ExportName = "scePadGetControllerInformation",
|
ExportName = "scePadGetControllerInformation",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ 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;
|
||||||
@@ -31,10 +29,7 @@ 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 object _memoryGate = new();
|
|
||||||
private static readonly HashSet<int> _preparedTransactionResources = [];
|
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||||
private static string? _titleId;
|
private static string? _titleId;
|
||||||
|
|
||||||
@@ -477,19 +472,6 @@ 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");
|
||||||
@@ -688,197 +670,4 @@ public static class SaveDataExports
|
|||||||
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||||
return ctx.SetReturn(0);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1646,12 +1646,8 @@ public static class VideoOutExports
|
|||||||
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||||
// the backend keys its guest-image registry on (see VulkanVideoPresenter.
|
// the backend keys its guest-image registry on (see VulkanVideoPresenter.
|
||||||
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
||||||
// Unknown formats default to 56 (8-bit RGBA) with a logged warning so games
|
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
|
||||||
// display something rather than silently failing the flip pipeline.
|
NormalizePixelFormat(pixelFormat) switch
|
||||||
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat)
|
|
||||||
{
|
|
||||||
var normalized = NormalizePixelFormat(pixelFormat);
|
|
||||||
var result = normalized switch
|
|
||||||
{
|
{
|
||||||
SceVideoOutPixelFormatA8R8G8B8Srgb or
|
SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||||
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||||
@@ -1669,17 +1665,6 @@ public static class VideoOutExports
|
|||||||
_ => 0u,
|
_ => 0u,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result == 0u)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] vk: unknown pixel format 0x{pixelFormat:X16} (normalized=0x{normalized:X16}) " +
|
|
||||||
$"— falling back to format 56 (8-bit RGBA). Report this format to the project.");
|
|
||||||
result = 56u;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryPackRgba8Pixel(
|
internal static bool TryPackRgba8Pixel(
|
||||||
ulong pixelFormat,
|
ulong pixelFormat,
|
||||||
byte red,
|
byte red,
|
||||||
|
|||||||
@@ -2213,12 +2213,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool ShouldAttachGuestDepth(
|
|
||||||
GuestDepthTarget? target,
|
|
||||||
GuestDepthState state) =>
|
|
||||||
target is not null &&
|
|
||||||
(state.TestEnable || state.WriteEnable || state.ClearEnable);
|
|
||||||
|
|
||||||
private readonly record struct Presentation(
|
private readonly record struct Presentation(
|
||||||
byte[]? Pixels,
|
byte[]? Pixels,
|
||||||
uint Width,
|
uint Width,
|
||||||
@@ -2775,11 +2769,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void LoadDebugUtilsCommands()
|
private void LoadDebugUtilsCommands()
|
||||||
{
|
{
|
||||||
if (!_vulkanDebugUtilsEnabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var setObjectName = _vk.GetDeviceProcAddr(_device, "vkSetDebugUtilsObjectNameEXT");
|
var setObjectName = _vk.GetDeviceProcAddr(_device, "vkSetDebugUtilsObjectNameEXT");
|
||||||
var beginLabel = _vk.GetDeviceProcAddr(_device, "vkCmdBeginDebugUtilsLabelEXT");
|
var beginLabel = _vk.GetDeviceProcAddr(_device, "vkCmdBeginDebugUtilsLabelEXT");
|
||||||
var endLabel = _vk.GetDeviceProcAddr(_device, "vkCmdEndDebugUtilsLabelEXT");
|
var endLabel = _vk.GetDeviceProcAddr(_device, "vkCmdEndDebugUtilsLabelEXT");
|
||||||
@@ -2882,48 +2871,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " +
|
$"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " +
|
||||||
$"fmt{texture.Format}/{format}";
|
$"fmt{texture.Format}/{format}";
|
||||||
|
|
||||||
private static bool IsTitleDraw(IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
|
||||||
{
|
|
||||||
foreach (var buffer in vertexBuffers)
|
|
||||||
{
|
|
||||||
if (buffer.Location == 0 &&
|
|
||||||
buffer.ComponentCount == 4 &&
|
|
||||||
buffer.DataFormat == 10 &&
|
|
||||||
buffer.NumberFormat == 0 &&
|
|
||||||
buffer.Stride == 16 &&
|
|
||||||
buffer.OffsetBytes == 12 &&
|
|
||||||
buffer.Length == 67568)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool AnyTargetAddressMatches(
|
|
||||||
IReadOnlyList<GuestImageResource>? targets,
|
|
||||||
string environmentVariable)
|
|
||||||
{
|
|
||||||
if (targets is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var target in targets)
|
|
||||||
{
|
|
||||||
if (AddressListContains(environmentVariable, target.Address))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateInstance()
|
private void CreateInstance()
|
||||||
{
|
{
|
||||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||||
|
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
||||||
byte* validationLayerName = null;
|
byte* validationLayerName = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -2949,8 +2900,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
enabledExtensions[index] = extensions[index];
|
enabledExtensions[index] = extensions[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_vulkanDebugUtilsEnabled &&
|
if (IsInstanceExtensionAvailable(DebugUtilsExtensionName))
|
||||||
IsInstanceExtensionAvailable(DebugUtilsExtensionName))
|
|
||||||
{
|
{
|
||||||
debugUtilsExtension = (byte*)SilkMarshal.StringToPtr(DebugUtilsExtensionName);
|
debugUtilsExtension = (byte*)SilkMarshal.StringToPtr(DebugUtilsExtensionName);
|
||||||
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
||||||
@@ -2965,12 +2915,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
instanceCreateFlags |= InstanceCreateFlags.EnumeratePortabilityBitKhr;
|
instanceCreateFlags |= InstanceCreateFlags.EnumeratePortabilityBitKhr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_vulkanValidationEnabled &&
|
if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
||||||
IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
|
||||||
{
|
{
|
||||||
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
|
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
|
||||||
}
|
}
|
||||||
else if (_vulkanValidationEnabled)
|
else if (enableValidation)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][WARN] SHARPEMU_VK_VALIDATION=1 but VK_LAYER_KHRONOS_validation not found (Vulkan SDK installed?).");
|
Console.Error.WriteLine("[LOADER][WARN] SHARPEMU_VK_VALIDATION=1 but VK_LAYER_KHRONOS_validation not found (Vulkan SDK installed?).");
|
||||||
}
|
}
|
||||||
@@ -4450,14 +4399,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
|
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
|
||||||
1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
||||||
if (_traceVulkanShaderEnabled)
|
TraceVulkanShader(
|
||||||
{
|
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
||||||
TraceVulkanShader(
|
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||||
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
|
||||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
$"waited_ms={waitedMs:F3}");
|
||||||
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
|
|
||||||
$"waited_ms={waitedMs:F3}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
|
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
|
||||||
@@ -4465,13 +4411,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
||||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||||
work.Action();
|
work.Action();
|
||||||
if (_traceVulkanShaderEnabled)
|
TraceVulkanShader(
|
||||||
{
|
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
|
||||||
TraceVulkanShader(
|
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||||
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
|
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
||||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
|
||||||
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
||||||
@@ -4943,13 +4886,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
IReadOnlyList<GuestImageResource>? renderTargets = null,
|
IReadOnlyList<GuestImageResource>? renderTargets = null,
|
||||||
ulong shaderAddress = 0)
|
ulong shaderAddress = 0)
|
||||||
{
|
{
|
||||||
if (!_traceGuestImagesEnabled &&
|
|
||||||
!_traceGuestImageAddressFilterEnabled &&
|
|
||||||
GuestImageTraceInterval() is null)
|
|
||||||
{
|
|
||||||
return Array.Empty<GuestImageResource>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidates = new HashSet<GuestImageResource>();
|
var candidates = new HashSet<GuestImageResource>();
|
||||||
foreach (var renderTarget in renderTargets ?? [])
|
foreach (var renderTarget in renderTargets ?? [])
|
||||||
{
|
{
|
||||||
@@ -5192,35 +5128,47 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
bool hasDepthAttachment = false,
|
bool hasDepthAttachment = false,
|
||||||
GuestDepthResource? feedbackDepth = null)
|
GuestDepthResource? feedbackDepth = null)
|
||||||
{
|
{
|
||||||
var isTitleDraw = IsTitleDraw(draw.VertexBuffers);
|
var isTitleDraw = draw.VertexBuffers.Any(buffer =>
|
||||||
var forceFullscreenVertex = _forceFullscreenPipeline ||
|
buffer.Location == 0 &&
|
||||||
_forceFullscreenVertex ||
|
buffer.ComponentCount == 4 &&
|
||||||
isTitleDraw && _forceTitleFullscreenVertex ||
|
buffer.DataFormat == 10 &&
|
||||||
AnyTargetAddressMatches(
|
buffer.NumberFormat == 0 &&
|
||||||
feedbackTargets,
|
buffer.Stride == 16 &&
|
||||||
"SHARPEMU_FORCE_FULLSCREEN_VERTEX_TARGETS");
|
buffer.OffsetBytes == 12 &&
|
||||||
var forceRasterState = _forceFullscreenPipeline ||
|
buffer.Length == 67568);
|
||||||
_forceDefaultRasterState ||
|
var forceFullscreenPipeline = Environment.GetEnvironmentVariable(
|
||||||
isTitleDraw && _forceTitleDefaultRasterState ||
|
"SHARPEMU_FORCE_FULLSCREEN_PIPELINE") == "1";
|
||||||
AnyTargetAddressMatches(
|
var forceFullscreenVertex = forceFullscreenPipeline ||
|
||||||
feedbackTargets,
|
Environment.GetEnvironmentVariable(
|
||||||
"SHARPEMU_FORCE_DEFAULT_RASTER_STATE_TARGETS");
|
"SHARPEMU_FORCE_FULLSCREEN_VERTEX") == "1" ||
|
||||||
|
isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX") == "1" ||
|
||||||
|
feedbackTargets?.Any(target => AddressListContains(
|
||||||
|
"SHARPEMU_FORCE_FULLSCREEN_VERTEX_TARGETS",
|
||||||
|
target.Address)) == true;
|
||||||
|
var forceRasterState = forceFullscreenPipeline ||
|
||||||
|
Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_DEFAULT_RASTER_STATE") == "1" ||
|
||||||
|
isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE") == "1" ||
|
||||||
|
feedbackTargets?.Any(target => AddressListContains(
|
||||||
|
"SHARPEMU_FORCE_DEFAULT_RASTER_STATE_TARGETS",
|
||||||
|
target.Address)) == true;
|
||||||
var forceTitleSolidFragment =
|
var forceTitleSolidFragment =
|
||||||
_forceTitleSolidFragment &&
|
Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT") == "1" &&
|
||||||
isTitleDraw;
|
isTitleDraw;
|
||||||
var forceSolidFragment = forceTitleSolidFragment ||
|
var forceSolidFragment = forceTitleSolidFragment || forceFullscreenPipeline ||
|
||||||
_forceFullscreenPipeline ||
|
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_SOLID_FRAGMENT") == "1" ||
|
||||||
_forceSolidFragment ||
|
feedbackTargets?.Any(target => AddressListContains(
|
||||||
AnyTargetAddressMatches(
|
"SHARPEMU_FORCE_SOLID_FRAGMENT_TARGETS",
|
||||||
feedbackTargets,
|
target.Address)) == true;
|
||||||
"SHARPEMU_FORCE_SOLID_FRAGMENT_TARGETS");
|
var forceAttributeFragment = uint.TryParse(
|
||||||
var attributeFragmentLocation =
|
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT"),
|
||||||
_forceAttributeFragmentLocation.GetValueOrDefault();
|
out var attributeFragmentLocation) &&
|
||||||
var forceAttributeFragment =
|
feedbackTargets?.Any(target => AddressListContains(
|
||||||
_forceAttributeFragmentLocation.HasValue &&
|
"SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT_TARGETS",
|
||||||
AnyTargetAddressMatches(
|
target.Address)) == true;
|
||||||
feedbackTargets,
|
|
||||||
"SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT_TARGETS");
|
|
||||||
var vertexSpirv = forceFullscreenVertex
|
var vertexSpirv = forceFullscreenVertex
|
||||||
? SpirvFixedShaders.CreateFullscreenVertex(0)
|
? SpirvFixedShaders.CreateFullscreenVertex(0)
|
||||||
: draw.VertexSpirv;
|
: draw.VertexSpirv;
|
||||||
@@ -5229,9 +5177,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
: forceAttributeFragment
|
: forceAttributeFragment
|
||||||
? SpirvFixedShaders.CreateAttributeFragment(attributeFragmentLocation)
|
? SpirvFixedShaders.CreateAttributeFragment(attributeFragmentLocation)
|
||||||
: draw.PixelSpirv;
|
: draw.PixelSpirv;
|
||||||
if (forceSolidFragment && !string.IsNullOrWhiteSpace(_fixedFragmentDumpPath))
|
var fixedFragmentDumpPath = Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT");
|
||||||
|
if (forceSolidFragment && !string.IsNullOrWhiteSpace(fixedFragmentDumpPath))
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(_fixedFragmentDumpPath, fragmentSpirv);
|
File.WriteAllBytes(fixedFragmentDumpPath, fragmentSpirv);
|
||||||
}
|
}
|
||||||
if (draw.RenderState.Blends.Count != renderTargetFormats.Count)
|
if (draw.RenderState.Blends.Count != renderTargetFormats.Count)
|
||||||
{
|
{
|
||||||
@@ -5281,18 +5231,21 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
resources.Raster = GuestRasterState.Default;
|
resources.Raster = GuestRasterState.Default;
|
||||||
resources.Depth = GuestDepthState.Default;
|
resources.Depth = GuestDepthState.Default;
|
||||||
}
|
}
|
||||||
if (isTitleDraw && _forceTitleDefaultBlend)
|
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_DEFAULT_BLEND") == "1")
|
||||||
{
|
{
|
||||||
resources.Blends = Enumerable.Repeat(
|
resources.Blends = Enumerable.Repeat(
|
||||||
GuestBlendState.Default,
|
GuestBlendState.Default,
|
||||||
renderTargetFormats.Count).ToArray();
|
renderTargetFormats.Count).ToArray();
|
||||||
}
|
}
|
||||||
if (isTitleDraw && _forceTitleDefaultViewportScissor)
|
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_DEFAULT_VIEWPORT_SCISSOR") == "1")
|
||||||
{
|
{
|
||||||
resources.Scissor = null;
|
resources.Scissor = null;
|
||||||
resources.Viewport = null;
|
resources.Viewport = null;
|
||||||
}
|
}
|
||||||
if (isTitleDraw && _forceTitleDisableCull)
|
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_DISABLE_CULL") == "1")
|
||||||
{
|
{
|
||||||
resources.Raster = resources.Raster with
|
resources.Raster = resources.Raster with
|
||||||
{
|
{
|
||||||
@@ -5300,11 +5253,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CullBack = false,
|
CullBack = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isTitleDraw && _forceTitleDisableDepth)
|
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_FORCE_TITLE_DISABLE_DEPTH") == "1")
|
||||||
{
|
{
|
||||||
resources.Depth = GuestDepthState.Default;
|
resources.Depth = GuestDepthState.Default;
|
||||||
}
|
}
|
||||||
if (isTitleDraw && _traceTitleState)
|
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_TRACE_TITLE_STATE") == "1")
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.title_state " +
|
$"[LOADER][TRACE] vk.title_state " +
|
||||||
@@ -7568,43 +7523,21 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
||||||
var endAddress = checked(guestBuffer.BaseAddress + size);
|
var endAddress = checked(guestBuffer.BaseAddress + size);
|
||||||
GuestBufferAllocation? allocation = null;
|
var allocation = _guestBufferAllocations
|
||||||
var allocationPriority = -1;
|
.Where(candidate =>
|
||||||
// This runs for every bound global buffer. Preserve the previous
|
candidate.BaseAddress <= guestBuffer.BaseAddress &&
|
||||||
// stable queue preference without allocating LINQ sort state.
|
candidate.BaseAddress + candidate.Size >= endAddress)
|
||||||
foreach (var candidate in _guestBufferAllocations)
|
.OrderByDescending(candidate =>
|
||||||
{
|
string.Equals(
|
||||||
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
|
|
||||||
candidate.BaseAddress + candidate.Size < endAddress)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidatePriority = string.Equals(
|
|
||||||
candidate.QueueName,
|
candidate.QueueName,
|
||||||
_activeGuestQueue.Name,
|
_activeGuestQueue.Name,
|
||||||
StringComparison.Ordinal)
|
StringComparison.Ordinal))
|
||||||
? 2
|
.ThenByDescending(candidate =>
|
||||||
: string.Equals(
|
string.Equals(
|
||||||
candidate.QueueName,
|
candidate.QueueName,
|
||||||
SharedReadOnlyGuestBufferQueue,
|
SharedReadOnlyGuestBufferQueue,
|
||||||
StringComparison.Ordinal)
|
StringComparison.Ordinal))
|
||||||
? 1
|
.First();
|
||||||
: 0;
|
|
||||||
if (candidatePriority > allocationPriority)
|
|
||||||
{
|
|
||||||
allocation = candidate;
|
|
||||||
allocationPriority = candidatePriority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocation is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"no Vulkan guest buffer allocation covers " +
|
|
||||||
$"0x{guestBuffer.BaseAddress:X16}-0x{endAddress:X16}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress;
|
var guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress;
|
||||||
var descriptorOffset = guestOffset &
|
var descriptorOffset = guestOffset &
|
||||||
~(GuestStorageBufferOffsetAlignment - 1);
|
~(GuestStorageBufferOffsetAlignment - 1);
|
||||||
@@ -7653,15 +7586,16 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
||||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||||
}
|
}
|
||||||
var mapped = new Span<byte>(
|
var live = new byte[guestBuffer.Length];
|
||||||
(void*)(allocation.Mapped + checked((nint)guestOffset)),
|
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, live) == true)
|
||||||
guestBuffer.Length);
|
|
||||||
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, mapped) != true)
|
|
||||||
{
|
{
|
||||||
source.CopyTo(mapped);
|
source = live;
|
||||||
}
|
}
|
||||||
|
|
||||||
mapped.CopyTo(shadow);
|
source.CopyTo(new Span<byte>(
|
||||||
|
(void*)(allocation.Mapped + checked((nint)guestOffset)),
|
||||||
|
source.Length));
|
||||||
|
source.CopyTo(shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTraceVulkanResources() &&
|
if (ShouldTraceVulkanResources() &&
|
||||||
@@ -7671,6 +7605,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
||||||
$"bytes={guestBuffer.Length}");
|
$"bytes={guestBuffer.Length}");
|
||||||
}
|
}
|
||||||
|
if (_setDebugUtilsObjectName is not null)
|
||||||
|
{
|
||||||
|
SetDebugName(
|
||||||
|
ObjectType.Buffer,
|
||||||
|
allocation.Buffer.Handle,
|
||||||
|
$"SharpEmu global 0x{guestBuffer.BaseAddress:X16} {guestBuffer.Length}b");
|
||||||
|
}
|
||||||
|
|
||||||
if (guestBuffer.Pooled)
|
if (guestBuffer.Pooled)
|
||||||
{
|
{
|
||||||
GuestDataPool.Return(guestBuffer.Data);
|
GuestDataPool.Return(guestBuffer.Data);
|
||||||
@@ -7907,7 +7849,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
ReadOnlySpan<byte> source = guestBuffer.Data.AsSpan(0, guestBuffer.Length);
|
ReadOnlySpan<byte> source = guestBuffer.Data.AsSpan(0, guestBuffer.Length);
|
||||||
byte[]? forcedVertexColors = null;
|
byte[]? forcedVertexColors = null;
|
||||||
if (_forceTitleVertexColorWhite &&
|
if (Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_VERTEX_COLOR_WHITE") == "1" &&
|
||||||
guestBuffer.Location == 0 &&
|
guestBuffer.Location == 0 &&
|
||||||
guestBuffer.ComponentCount == 4 &&
|
guestBuffer.ComponentCount == 4 &&
|
||||||
guestBuffer.DataFormat == 10 &&
|
guestBuffer.DataFormat == 10 &&
|
||||||
@@ -9433,67 +9375,37 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var index = 0; index < targetFormats.Length; index++)
|
if (targetFormats
|
||||||
|
.Select((format, index) => format.IsInteger && work.Draw.RenderState.Blends[index].Enable)
|
||||||
|
.Any(invalid => invalid))
|
||||||
{
|
{
|
||||||
if (targetFormats[index].IsInteger &&
|
Console.Error.WriteLine(
|
||||||
work.Draw.RenderState.Blends[index].Enable)
|
"[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment.");
|
||||||
{
|
ReturnPooledGuestData(work.Draw);
|
||||||
Console.Error.WriteLine(
|
return;
|
||||||
"[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment.");
|
|
||||||
ReturnPooledGuestData(work.Draw);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_supportsIndependentBlend)
|
if (!_supportsIndependentBlend &&
|
||||||
|
work.Draw.RenderState.Blends.Skip(1).Any(blend => blend != work.Draw.RenderState.Blends[0]))
|
||||||
{
|
{
|
||||||
for (var index = 1; index < work.Draw.RenderState.Blends.Count; index++)
|
Console.Error.WriteLine(
|
||||||
{
|
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
|
||||||
if (work.Draw.RenderState.Blends[index] !=
|
ReturnPooledGuestData(work.Draw);
|
||||||
work.Draw.RenderState.Blends[0])
|
return;
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
|
|
||||||
ReturnPooledGuestData(work.Draw);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var formats = new Format[targetFormats.Length];
|
var formats = targetFormats.Select(target => target.Format).ToArray();
|
||||||
for (var index = 0; index < targetFormats.Length; index++)
|
|
||||||
{
|
|
||||||
formats[index] = targetFormats[index].Format;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasStorageFeedback = false;
|
var targetAddresses = work.Targets
|
||||||
foreach (var texture in work.Draw.Textures)
|
.Where(target => target.Address != 0)
|
||||||
{
|
.Select(target => target.Address)
|
||||||
if (!texture.IsStorage || texture.Address == 0)
|
.ToHashSet();
|
||||||
{
|
if (work.Draw.Textures.Any(texture =>
|
||||||
continue;
|
texture.IsStorage && targetAddresses.Contains(texture.Address)))
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var target in work.Targets)
|
|
||||||
{
|
|
||||||
if (target.Address != 0 && target.Address == texture.Address)
|
|
||||||
{
|
|
||||||
hasStorageFeedback = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasStorageFeedback)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasStorageFeedback)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][WARN] Vulkan skipped storage render-target feedback loop " +
|
$"[LOADER][WARN] Vulkan skipped storage render-target feedback loop " +
|
||||||
$"targets={string.Join(',', work.Targets.Where(target => target.Address != 0).Select(target => $"0x{target.Address:X16}"))}; " +
|
$"targets={string.Join(',', targetAddresses.Select(address => $"0x{address:X16}"))}; " +
|
||||||
"sampled aliases use ordered snapshots");
|
"sampled aliases use ordered snapshots");
|
||||||
ReturnPooledGuestData(work.Draw);
|
ReturnPooledGuestData(work.Draw);
|
||||||
return;
|
return;
|
||||||
@@ -9528,7 +9440,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
|
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
|
||||||
var draw = work.Draw;
|
var draw = work.Draw;
|
||||||
var clearDepthForDraw = draw.RenderState.Depth.ClearEnable;
|
|
||||||
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
|
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
|
||||||
{
|
{
|
||||||
draw = draw with
|
draw = draw with
|
||||||
@@ -9541,10 +9452,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
GuestDepthResource? depth = null;
|
GuestDepthResource? depth = null;
|
||||||
DepthFramebufferResource? depthFramebuffer = null;
|
DepthFramebufferResource? depthFramebuffer = null;
|
||||||
if (ShouldAttachGuestDepth(
|
if (work.DepthTarget is { } depthTarget &&
|
||||||
work.DepthTarget,
|
(draw.RenderState.Depth.TestEnable ||
|
||||||
draw.RenderState.Depth) &&
|
draw.RenderState.Depth.WriteEnable))
|
||||||
work.DepthTarget is { } depthTarget)
|
|
||||||
{
|
{
|
||||||
var effectiveDepthTarget = depthTarget;
|
var effectiveDepthTarget = depthTarget;
|
||||||
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
|
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
|
||||||
@@ -9591,45 +9501,21 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
depth = GetOrCreateGuestDepth(effectiveDepthTarget);
|
depth = GetOrCreateGuestDepth(effectiveDepthTarget);
|
||||||
PrepareFirstUseDepth(depth, draw.RenderState.Depth);
|
PrepareFirstUseDepth(depth, draw.RenderState.Depth);
|
||||||
if (clearDepthForDraw)
|
|
||||||
{
|
|
||||||
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
|
|
||||||
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
|
|
||||||
}
|
|
||||||
if (targets.Length == 1)
|
if (targets.Length == 1)
|
||||||
{
|
{
|
||||||
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
|
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clearDepthForDraw)
|
|
||||||
{
|
|
||||||
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
|
|
||||||
// clear operation. The draw still produces color, but its
|
|
||||||
// interpolated vertex Z is not the guest clear value.
|
|
||||||
draw = draw with
|
|
||||||
{
|
|
||||||
RenderState = draw.RenderState with
|
|
||||||
{
|
|
||||||
Depth = draw.RenderState.Depth with
|
|
||||||
{
|
|
||||||
TestEnable = false,
|
|
||||||
WriteEnable = false,
|
|
||||||
ClearEnable = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var renderPass = depthFramebuffer is null
|
var renderPass = depthFramebuffer is null
|
||||||
? firstTarget.Initialized
|
? firstTarget.Initialized
|
||||||
? firstTarget.RenderPass
|
? firstTarget.RenderPass
|
||||||
: firstTarget.InitialRenderPass
|
: firstTarget.InitialRenderPass
|
||||||
: firstTarget.Initialized
|
: firstTarget.Initialized
|
||||||
? depth!.Initialized && !clearDepthForDraw
|
? depth!.Initialized
|
||||||
? depthFramebuffer.LoadRenderPass
|
? depthFramebuffer.LoadRenderPass
|
||||||
: depthFramebuffer.DepthClearRenderPass
|
: depthFramebuffer.DepthClearRenderPass
|
||||||
: depth!.Initialized && !clearDepthForDraw
|
: depth!.Initialized
|
||||||
? depthFramebuffer.ColorClearRenderPass
|
? depthFramebuffer.ColorClearRenderPass
|
||||||
: depthFramebuffer.BothClearRenderPass;
|
: depthFramebuffer.BothClearRenderPass;
|
||||||
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
|
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
|
||||||
@@ -9645,7 +9531,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
targets.Select(target =>
|
targets.Select(target =>
|
||||||
target.Initialized || target.InitialUploadPending).ToArray(),
|
target.Initialized || target.InitialUploadPending).ToArray(),
|
||||||
depth,
|
depth,
|
||||||
depth?.Initialized == true && !clearDepthForDraw);
|
depth?.Initialized == true);
|
||||||
transientRenderPass = renderPass;
|
transientRenderPass = renderPass;
|
||||||
transientFramebuffer = framebuffer;
|
transientFramebuffer = framebuffer;
|
||||||
}
|
}
|
||||||
@@ -9833,12 +9719,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (depth is not null)
|
if (depth is not null)
|
||||||
{
|
{
|
||||||
depth.Initialized = true;
|
depth.Initialized = true;
|
||||||
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
|
if (draw.RenderState.Depth.WriteEnable)
|
||||||
if (clearDepthForDraw)
|
|
||||||
{
|
|
||||||
depth.InitializationSource = "guest-depth-clear";
|
|
||||||
}
|
|
||||||
else if (draw.RenderState.Depth.WriteEnable)
|
|
||||||
{
|
{
|
||||||
depth.InitializationSource = "translated-depth-write";
|
depth.InitializationSource = "translated-depth-write";
|
||||||
}
|
}
|
||||||
@@ -9865,34 +9746,64 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var guestWritesMode = Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_TRACE_GUEST_WRITES");
|
||||||
|
var traceAddressWriteOrdinal = 0L;
|
||||||
|
_ = long.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_WRITE_ORDINAL"),
|
||||||
|
out traceAddressWriteOrdinal);
|
||||||
|
var traceLargeWriteOrdinal = 0L;
|
||||||
|
if (guestWritesMode is not null &&
|
||||||
|
guestWritesMode.StartsWith("large@", StringComparison.Ordinal) &&
|
||||||
|
long.TryParse(
|
||||||
|
guestWritesMode.AsSpan("large@".Length),
|
||||||
|
out var parsedTraceLargeWriteOrdinal) &&
|
||||||
|
parsedTraceLargeWriteOrdinal > 0)
|
||||||
|
{
|
||||||
|
traceLargeWriteOrdinal = parsedTraceLargeWriteOrdinal;
|
||||||
|
}
|
||||||
|
|
||||||
var tracePixelSpirv = false;
|
var tracePixelSpirv = false;
|
||||||
if (_tracePixelSpirvBytes > 0 &&
|
if (int.TryParse(
|
||||||
_tracePixelSpirvBytes == work.Draw.PixelSpirv.Length)
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PIXEL_SPIRV_BYTES"),
|
||||||
|
out var tracePixelSpirvBytes) &&
|
||||||
|
tracePixelSpirvBytes == work.Draw.PixelSpirv.Length)
|
||||||
{
|
{
|
||||||
var pixelWriteCount = _pixelSpirvWriteCounts.TryGetValue(
|
var pixelWriteCount = _pixelSpirvWriteCounts.TryGetValue(
|
||||||
_tracePixelSpirvBytes,
|
tracePixelSpirvBytes,
|
||||||
out var previousPixelWriteCount)
|
out var previousPixelWriteCount)
|
||||||
? previousPixelWriteCount + 1
|
? previousPixelWriteCount + 1
|
||||||
: 1;
|
: 1;
|
||||||
_pixelSpirvWriteCounts[_tracePixelSpirvBytes] = pixelWriteCount;
|
_pixelSpirvWriteCounts[tracePixelSpirvBytes] = pixelWriteCount;
|
||||||
|
var tracePixelSpirvOccurrence = 1;
|
||||||
|
_ = int.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_TRACE_PIXEL_SPIRV_OCCURRENCE"),
|
||||||
|
out tracePixelSpirvOccurrence);
|
||||||
tracePixelSpirv =
|
tracePixelSpirv =
|
||||||
pixelWriteCount == _tracePixelSpirvOccurrence;
|
pixelWriteCount == Math.Max(tracePixelSpirvOccurrence, 1);
|
||||||
}
|
}
|
||||||
var traceTitleDraw =
|
var traceTitleDraw =
|
||||||
!_tracedTitleDraw &&
|
!_tracedTitleDraw &&
|
||||||
_traceTitleDrawEnabled &&
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_DRAW") == "1" &&
|
||||||
IsTitleDraw(work.Draw.VertexBuffers);
|
work.Draw.VertexBuffers.Any(buffer =>
|
||||||
|
buffer.Location == 0 &&
|
||||||
|
buffer.ComponentCount == 4 &&
|
||||||
|
buffer.DataFormat == 10 &&
|
||||||
|
buffer.NumberFormat == 0 &&
|
||||||
|
buffer.Stride == 16 &&
|
||||||
|
buffer.OffsetBytes == 12 &&
|
||||||
|
buffer.Length == 67568);
|
||||||
_tracedTitleDraw |= traceTitleDraw;
|
_tracedTitleDraw |= traceTitleDraw;
|
||||||
|
|
||||||
foreach (var target in targets)
|
foreach (var target in targets)
|
||||||
{
|
{
|
||||||
var traceAddressWrite =
|
var traceAddressWrite =
|
||||||
ShouldTraceGuestImageWriteForDiagnostics(target.Address);
|
ShouldTraceGuestImageWriteForDiagnostics(target.Address);
|
||||||
var traceSmallWrites = _traceGuestWritesMode == "small" &&
|
var traceSmallWrites = guestWritesMode == "small" &&
|
||||||
target.Width <= 512 && target.Height <= 256;
|
target.Width <= 512 && target.Height <= 256;
|
||||||
var traceLargeWrites =
|
var traceLargeWrites =
|
||||||
(_traceGuestWritesMode == "large" ||
|
(guestWritesMode == "large" || traceLargeWriteOrdinal != 0) &&
|
||||||
_traceLargeGuestWriteOrdinal != 0) &&
|
|
||||||
target.Width >= 2560 && target.Height >= 1440;
|
target.Width >= 2560 && target.Height >= 1440;
|
||||||
if (traceAddressWrite || traceSmallWrites ||
|
if (traceAddressWrite || traceSmallWrites ||
|
||||||
traceLargeWrites || tracePixelSpirv || traceTitleDraw)
|
traceLargeWrites || tracePixelSpirv || traceTitleDraw)
|
||||||
@@ -9905,10 +9816,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_tracedGuestWriteCounts[target.Address] = writeCount;
|
_tracedGuestWriteCounts[target.Address] = writeCount;
|
||||||
var shouldTraceWrite = tracePixelSpirv || traceTitleDraw
|
var shouldTraceWrite = tracePixelSpirv || traceTitleDraw
|
||||||
? true
|
? true
|
||||||
: traceAddressWrite && _traceGuestWriteOrdinal > 0
|
: traceAddressWrite && traceAddressWriteOrdinal > 0
|
||||||
? writeCount == _traceGuestWriteOrdinal
|
? writeCount == traceAddressWriteOrdinal
|
||||||
: _traceLargeGuestWriteOrdinal != 0
|
: traceLargeWriteOrdinal != 0
|
||||||
? writeCount == _traceLargeGuestWriteOrdinal
|
? writeCount == traceLargeWriteOrdinal
|
||||||
: writeCount <=
|
: writeCount <=
|
||||||
(traceLargeWrites ? 2 : traceSmallWrites ? 48 : 3);
|
(traceLargeWrites ? 2 : traceSmallWrites ? 48 : 3);
|
||||||
if (traceAddressWrite || shouldTraceWrite)
|
if (traceAddressWrite || shouldTraceWrite)
|
||||||
@@ -9942,13 +9853,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_traceVulkanShaderEnabled)
|
TraceVulkanShader(
|
||||||
{
|
$"vk.offscreen_draw mrt={targets.Length} " +
|
||||||
TraceVulkanShader(
|
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
||||||
$"vk.offscreen_draw mrt={targets.Length} " +
|
$"textures={work.Draw.Textures.Count}");
|
||||||
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
|
||||||
$"textures={work.Draw.Textures.Count}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -12882,20 +12790,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// Diagnostics toggles are read once: these run per draw / per cached
|
// Diagnostics toggles are read once: these run per draw / per cached
|
||||||
// texture hit, and env lookups plus string parsing are far too
|
// texture hit, and env lookups plus string parsing are far too
|
||||||
// expensive there (and non-trivially so under Rosetta 2).
|
// expensive there (and non-trivially so under Rosetta 2).
|
||||||
private static readonly bool _vulkanValidationEnabled =
|
|
||||||
string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
// Object names and command labels are useful in RenderDoc and validation
|
|
||||||
// captures, but formatting them and calling the debug-utils driver hooks
|
|
||||||
// for every draw is measurable overhead in normal gameplay.
|
|
||||||
private static readonly bool _vulkanDebugUtilsEnabled =
|
|
||||||
_vulkanValidationEnabled ||
|
|
||||||
string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_VK_DEBUG_LABELS"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
private static readonly string? _traceGuestImagesMode =
|
private static readonly string? _traceGuestImagesMode =
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||||
private static readonly bool _traceGuestImagesEnabled =
|
private static readonly bool _traceGuestImagesEnabled =
|
||||||
@@ -12959,80 +12853,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
!string.IsNullOrWhiteSpace(
|
!string.IsNullOrWhiteSpace(
|
||||||
Environment.GetEnvironmentVariable(
|
Environment.GetEnvironmentVariable(
|
||||||
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||||
private static readonly bool _forceFullscreenPipeline =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_FULLSCREEN_PIPELINE") == "1";
|
|
||||||
private static readonly bool _forceFullscreenVertex =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_FULLSCREEN_VERTEX") == "1";
|
|
||||||
private static readonly bool _forceTitleFullscreenVertex =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX") == "1";
|
|
||||||
private static readonly bool _forceDefaultRasterState =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_DEFAULT_RASTER_STATE") == "1";
|
|
||||||
private static readonly bool _forceTitleDefaultRasterState =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE") == "1";
|
|
||||||
private static readonly bool _forceTitleSolidFragment =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT") == "1";
|
|
||||||
private static readonly bool _forceSolidFragment =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_SOLID_FRAGMENT") == "1";
|
|
||||||
private static readonly uint? _forceAttributeFragmentLocation =
|
|
||||||
uint.TryParse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT"),
|
|
||||||
out var forceAttributeFragmentLocation)
|
|
||||||
? forceAttributeFragmentLocation
|
|
||||||
: null;
|
|
||||||
private static readonly string? _fixedFragmentDumpPath =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT");
|
|
||||||
private static readonly bool _forceTitleDefaultBlend =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_DEFAULT_BLEND") == "1";
|
|
||||||
private static readonly bool _forceTitleDefaultViewportScissor =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_DEFAULT_VIEWPORT_SCISSOR") == "1";
|
|
||||||
private static readonly bool _forceTitleDisableCull =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_DISABLE_CULL") == "1";
|
|
||||||
private static readonly bool _forceTitleDisableDepth =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_DISABLE_DEPTH") == "1";
|
|
||||||
private static readonly bool _traceTitleState =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_STATE") == "1";
|
|
||||||
private static readonly bool _forceTitleVertexColorWhite =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_VERTEX_COLOR_WHITE") == "1";
|
|
||||||
private static readonly bool _chunkedDrawsEnabled =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_ENABLE_CHUNKED_DRAWS") == "1";
|
|
||||||
private static readonly string? _traceGuestWritesMode =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_WRITES");
|
|
||||||
private static readonly long _traceGuestWriteOrdinal =
|
|
||||||
long.TryParse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_WRITE_ORDINAL"),
|
|
||||||
out var traceGuestWriteOrdinal)
|
|
||||||
? traceGuestWriteOrdinal
|
|
||||||
: 0;
|
|
||||||
private static readonly long _traceLargeGuestWriteOrdinal =
|
|
||||||
ParseTraceLargeGuestWriteOrdinal(_traceGuestWritesMode);
|
|
||||||
private static readonly int _tracePixelSpirvBytes =
|
|
||||||
int.TryParse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PIXEL_SPIRV_BYTES"),
|
|
||||||
out var tracePixelSpirvBytes)
|
|
||||||
? tracePixelSpirvBytes
|
|
||||||
: 0;
|
|
||||||
private static readonly int _tracePixelSpirvOccurrence =
|
|
||||||
int.TryParse(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PIXEL_SPIRV_OCCURRENCE"),
|
|
||||||
out var tracePixelSpirvOccurrence)
|
|
||||||
? Math.Max(tracePixelSpirvOccurrence, 1)
|
|
||||||
: 1;
|
|
||||||
private static readonly bool _traceTitleDrawEnabled =
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_DRAW") == "1";
|
|
||||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<
|
||||||
string,
|
string,
|
||||||
(bool Wildcard, ulong[] Addresses)> _cachedAddressLists = new();
|
(bool Wildcard, ulong[] Addresses)> _cachedAddressLists = new();
|
||||||
|
|
||||||
private static long ParseTraceLargeGuestWriteOrdinal(string? mode)
|
|
||||||
{
|
|
||||||
return mode is not null &&
|
|
||||||
mode.StartsWith("large@", StringComparison.Ordinal) &&
|
|
||||||
long.TryParse(mode.AsSpan("large@".Length), out var ordinal) &&
|
|
||||||
ordinal > 0
|
|
||||||
? ordinal
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldTraceGuestImageContentsForDiagnostics() =>
|
private static bool ShouldTraceGuestImageContentsForDiagnostics() =>
|
||||||
_traceGuestImagesEnabled;
|
_traceGuestImagesEnabled;
|
||||||
|
|
||||||
@@ -13230,7 +13054,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// MoltenVK this starves the render thread and makes the guest fall
|
// MoltenVK this starves the render thread and makes the guest fall
|
||||||
// behind its own flip queue. Vulkan clips a normal fullscreen draw
|
// behind its own flip queue. Vulkan clips a normal fullscreen draw
|
||||||
// efficiently; keep tiling only as an explicit driver diagnostic.
|
// efficiently; keep tiling only as an explicit driver diagnostic.
|
||||||
var maxPixelsPerDraw = _chunkedDrawsEnabled
|
var maxPixelsPerDraw = Environment.GetEnvironmentVariable(
|
||||||
|
"SHARPEMU_ENABLE_CHUNKED_DRAWS") == "1"
|
||||||
? 512u * 512u
|
? 512u * 512u
|
||||||
: uint.MaxValue;
|
: uint.MaxValue;
|
||||||
var rowsPerDraw = Math.Max(
|
var rowsPerDraw = Math.Max(
|
||||||
|
|||||||
@@ -325,31 +325,9 @@ public sealed record Gen5ShaderProgram(
|
|||||||
ulong Address,
|
ulong Address,
|
||||||
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
||||||
{
|
{
|
||||||
private const uint PixelColorTargetCount = 8;
|
|
||||||
private const int PixelColorMaskBits = 4;
|
|
||||||
private readonly uint _pixelColorExportMasks = ComputePixelColorExportMasks(Instructions);
|
|
||||||
private const int ScalarRegisterCount = 256;
|
private const int ScalarRegisterCount = 256;
|
||||||
private IReadOnlySet<uint>? _runtimeScalarRegisters;
|
private IReadOnlySet<uint>? _runtimeScalarRegisters;
|
||||||
|
|
||||||
public uint PixelColorExportMasks => _pixelColorExportMasks;
|
|
||||||
|
|
||||||
private static uint ComputePixelColorExportMasks(
|
|
||||||
IReadOnlyList<Gen5ShaderInstruction> instructions)
|
|
||||||
{
|
|
||||||
var masks = 0u;
|
|
||||||
foreach (var instruction in instructions)
|
|
||||||
{
|
|
||||||
if (instruction.Control is Gen5ExportControl export &&
|
|
||||||
export.Target < PixelColorTargetCount)
|
|
||||||
{
|
|
||||||
masks |= (export.EnableMask & 0xFu) <<
|
|
||||||
(int)(export.Target * PixelColorMaskBits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return masks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Gen5ImageControl> ImageResources =>
|
public IEnumerable<Gen5ImageControl> ImageResources =>
|
||||||
Instructions
|
Instructions
|
||||||
.Select(instruction => instruction.Control)
|
.Select(instruction => instruction.Control)
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Agc;
|
|
||||||
|
|
||||||
public sealed class Gen5ShaderProgramTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void PixelColorExportMasksPackAllColorTargets()
|
|
||||||
{
|
|
||||||
var program = new Gen5ShaderProgram(
|
|
||||||
0,
|
|
||||||
[
|
|
||||||
Export(0, 0x1),
|
|
||||||
Export(0, 0x4),
|
|
||||||
Export(1, 0x2),
|
|
||||||
Export(2, 0x3),
|
|
||||||
Export(3, 0x4),
|
|
||||||
Export(4, 0x5),
|
|
||||||
Export(5, 0x6),
|
|
||||||
Export(6, 0x7),
|
|
||||||
Export(7, 0x8),
|
|
||||||
Export(8, 0xF),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Assert.Equal(0x8765_4325u, program.PixelColorExportMasks);
|
|
||||||
Assert.Equal(0x8765_4325u, program.PixelColorExportMasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Gen5ShaderInstruction Export(uint target, uint enableMask) => new(
|
|
||||||
0,
|
|
||||||
Gen5ShaderEncoding.Exp,
|
|
||||||
"exp",
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
new Gen5ExportControl(target, enableMask, false, false, false));
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using SharpEmu.Libs.Audio;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Audio;
|
|
||||||
|
|
||||||
public sealed class AudioPcmConversionTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void FloatFullScaleMapsToSignedPcmEndpoints()
|
|
||||||
{
|
|
||||||
Span<byte> source = stackalloc byte[sizeof(float) * 2];
|
|
||||||
WriteFloat(source, 0, -1.0f);
|
|
||||||
WriteFloat(source, 1, 1.0f);
|
|
||||||
Span<byte> destination = stackalloc byte[AudioPcmConversion.OutputFrameSize];
|
|
||||||
|
|
||||||
AudioPcmConversion.ConvertToStereoPcm16(
|
|
||||||
source,
|
|
||||||
destination,
|
|
||||||
frames: 1,
|
|
||||||
channels: 2,
|
|
||||||
bytesPerSample: sizeof(float),
|
|
||||||
isFloat: true,
|
|
||||||
volume: 1.0f);
|
|
||||||
|
|
||||||
Assert.Equal(short.MinValue, BinaryPrimitives.ReadInt16LittleEndian(destination));
|
|
||||||
Assert.Equal(short.MaxValue, BinaryPrimitives.ReadInt16LittleEndian(destination[2..]));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FloatNaNMapsToSilence()
|
|
||||||
{
|
|
||||||
Span<byte> source = stackalloc byte[sizeof(float)];
|
|
||||||
WriteFloat(source, 0, float.NaN);
|
|
||||||
Span<byte> destination = stackalloc byte[AudioPcmConversion.OutputFrameSize];
|
|
||||||
|
|
||||||
AudioPcmConversion.ConvertToStereoPcm16(
|
|
||||||
source,
|
|
||||||
destination,
|
|
||||||
frames: 1,
|
|
||||||
channels: 1,
|
|
||||||
bytesPerSample: sizeof(float),
|
|
||||||
isFloat: true,
|
|
||||||
volume: 1.0f);
|
|
||||||
|
|
||||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(destination));
|
|
||||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(destination[2..]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteFloat(Span<byte> destination, int sample, float value) =>
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(
|
|
||||||
destination[(sample * sizeof(float))..],
|
|
||||||
BitConverter.SingleToInt32Bits(value));
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.Font;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Font;
|
|
||||||
|
|
||||||
public sealed class FontExportsTests
|
|
||||||
{
|
|
||||||
private const ulong Base = 0x1_0000_0000;
|
|
||||||
private const ulong LayoutAddress = Base + 0x100;
|
|
||||||
|
|
||||||
private readonly FakeCpuMemory _memory = new(Base, 0x1000);
|
|
||||||
private readonly CpuContext _ctx;
|
|
||||||
|
|
||||||
public FontExportsTests()
|
|
||||||
{
|
|
||||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SceFontHorizontalLayout is three floats; the sentinel directly after
|
|
||||||
// them must survive the call.
|
|
||||||
[Fact]
|
|
||||||
public void GetHorizontalLayout_WritesExactlyThreeFloats()
|
|
||||||
{
|
|
||||||
const uint Sentinel = 0xDEADBEEF;
|
|
||||||
Span<byte> sentinelBytes = stackalloc byte[sizeof(uint)];
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(sentinelBytes, Sentinel);
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(LayoutAddress + 12, sentinelBytes));
|
|
||||||
|
|
||||||
_ctx[CpuRegister.Rsi] = LayoutAddress;
|
|
||||||
Assert.Equal(0, FontExports.GetHorizontalLayout(_ctx));
|
|
||||||
|
|
||||||
Span<byte> layout = stackalloc byte[16];
|
|
||||||
Assert.True(_ctx.Memory.TryRead(LayoutAddress, layout));
|
|
||||||
Assert.Equal(12.0f, BinaryPrimitives.ReadSingleLittleEndian(layout));
|
|
||||||
Assert.Equal(16.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[4..]));
|
|
||||||
Assert.Equal(0.0f, BinaryPrimitives.ReadSingleLittleEndian(layout[8..]));
|
|
||||||
Assert.Equal(Sentinel, BinaryPrimitives.ReadUInt32LittleEndian(layout[12..]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.Core.Memory;
|
|
||||||
using SharpEmu.Core.Loader;
|
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Memory;
|
|
||||||
|
|
||||||
// PhysicalVirtualMemory is the host-backed (identity-mapped) implementation.
|
|
||||||
// Reserve-only regions (> 4 GiB, non-executable) defer commit until first
|
|
||||||
// access; TryAllocateGuestMemory serves a first-fit free-list with coalescing.
|
|
||||||
// These tests pin that behaviour through fake IHostMemory implementations.
|
|
||||||
public sealed class PhysicalVirtualMemoryTests
|
|
||||||
{
|
|
||||||
// 1. Lazy commit: a reserve-only region has its pages committed on demand
|
|
||||||
// when read; freshly committed pages read as zero.
|
|
||||||
[Fact]
|
|
||||||
public void LazyReadCommitsPageOnDemandAndReadsZero()
|
|
||||||
{
|
|
||||||
using var host = new LazyZeroedHostMemory();
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
// > 4 GiB, non-executable -> reserve-only with lazy commit.
|
|
||||||
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
||||||
Assert.NotEqual(0UL, address);
|
|
||||||
|
|
||||||
// Discard the priming commits AllocateAt issues up front; we want to
|
|
||||||
// observe the on-demand commit triggered by the read itself.
|
|
||||||
host.CommitCalls.Clear();
|
|
||||||
|
|
||||||
var buffer = new byte[1];
|
|
||||||
Assert.True(memory.TryRead(address, buffer));
|
|
||||||
Assert.Equal(0, buffer[0]);
|
|
||||||
|
|
||||||
// The touched page (page-aligned to `address`) was committed on demand.
|
|
||||||
var page = address & ~0xFFFUL;
|
|
||||||
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Reserve-only region: GetPointer commits the page before returning it,
|
|
||||||
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
|
|
||||||
[Fact]
|
|
||||||
public unsafe void GetPointerOnReserveOnlyRegionCommitsAndReturnsValidPointer()
|
|
||||||
{
|
|
||||||
using var host = new LazyZeroedHostMemory();
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
||||||
host.CommitCalls.Clear();
|
|
||||||
|
|
||||||
var pointer = memory.GetPointer(address + 0x123);
|
|
||||||
Assert.NotEqual(0UL, (ulong)pointer);
|
|
||||||
Assert.Equal(address + 0x123, (ulong)pointer);
|
|
||||||
|
|
||||||
var page = (address + 0x123) & ~0xFFFUL;
|
|
||||||
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public unsafe void GetPointerOnUnmappedAddressReturnsNull()
|
|
||||||
{
|
|
||||||
using var host = new LazyZeroedHostMemory();
|
|
||||||
using var memory = new PhysicalVirtualMemory(host);
|
|
||||||
|
|
||||||
Assert.Equal(0UL, (ulong)memory.GetPointer(0x0001_0000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Free-list reuse: a freed range is served back by first-fit allocation,
|
|
||||||
// preferring the lowest fitting free range over the larger trailing span.
|
|
||||||
[Fact]
|
|
||||||
public void FreedRangeIsReusedByFirstFitAllocation()
|
|
||||||
{
|
|
||||||
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
|
||||||
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var second));
|
|
||||||
Assert.NotEqual(first, second);
|
|
||||||
Assert.True(memory.TryFreeGuestMemory(first));
|
|
||||||
|
|
||||||
// A smaller allocation must reuse first's freed slot (lowest fitting range),
|
|
||||||
// not the larger trailing free range.
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x2000, 0x1000, out var reused));
|
|
||||||
Assert.Equal(first, reused);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Coalescing: freeing the middle of three adjacent ranges merges both the
|
|
||||||
// left and right free neighbours in a single TryFreeGuestMemory call,
|
|
||||||
// restoring the full span for subsequent first-fit reuse.
|
|
||||||
[Fact]
|
|
||||||
public void FreeingMiddleRangeCoalescesBothNeighbours()
|
|
||||||
{
|
|
||||||
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
|
||||||
|
|
||||||
// Three adjacent 0x1000 allocations: offsets 0x1000, 0x2000, 0x3000.
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var first));
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var second));
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var third));
|
|
||||||
|
|
||||||
// Free the outer ranges first, leaving two separate free ranges.
|
|
||||||
Assert.True(memory.TryFreeGuestMemory(first));
|
|
||||||
Assert.True(memory.TryFreeGuestMemory(third));
|
|
||||||
|
|
||||||
// Freeing the middle range must coalesce both neighbours at once.
|
|
||||||
Assert.True(memory.TryFreeGuestMemory(second));
|
|
||||||
|
|
||||||
// The whole arena is now one coalesced free range; a full-arena allocation
|
|
||||||
// reuses first's base address.
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x000F_F000, 0x1000, out var coalesced));
|
|
||||||
Assert.Equal(first, coalesced);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Host memory backed by a single real, zero-initialised page. Reserve/Allocate
|
|
||||||
/// report the page-aligned buffer address so lazy-commit read paths can actually
|
|
||||||
/// dereference the returned pointer. Query always reports Reserved, so
|
|
||||||
/// EnsureRangeCommitted issues a Commit on first access.
|
|
||||||
/// </summary>
|
|
||||||
private sealed unsafe class LazyZeroedHostMemory : IHostMemory, IDisposable
|
|
||||||
{
|
|
||||||
private readonly void* _allocation;
|
|
||||||
private readonly ulong _address;
|
|
||||||
private bool _freed;
|
|
||||||
|
|
||||||
public LazyZeroedHostMemory()
|
|
||||||
{
|
|
||||||
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
|
|
||||||
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
|
||||||
|
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
|
||||||
|
|
||||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
|
||||||
|
|
||||||
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
|
||||||
{
|
|
||||||
CommitCalls.Add((address, size, protection));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Free(ulong address)
|
|
||||||
{
|
|
||||||
// The real buffer is released in Dispose; keep Free a no-op so
|
|
||||||
// PhysicalVirtualMemory.Clear does not double-free it.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
||||||
{
|
|
||||||
rawOldProtection = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
||||||
{
|
|
||||||
rawOldProtection = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Query(ulong address, out HostRegionInfo info)
|
|
||||||
{
|
|
||||||
var pageAddress = address & ~0xFFFUL;
|
|
||||||
info = new HostRegionInfo(
|
|
||||||
pageAddress,
|
|
||||||
pageAddress,
|
|
||||||
0x1000,
|
|
||||||
HostRegionState.Reserved,
|
|
||||||
0,
|
|
||||||
HostPageProtection.NoAccess,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FlushInstructionCache(ulong address, ulong size)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!_freed)
|
|
||||||
{
|
|
||||||
System.Runtime.InteropServices.NativeMemory.Free(_allocation);
|
|
||||||
_freed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal host memory for free-list tests: Allocate honours the desired
|
|
||||||
// address (or a fallback), everything else succeeds as a no-op. The guest
|
|
||||||
// allocation arena never dereferences, so no real backing is required.
|
|
||||||
private sealed class FakeHostMemory : IHostMemory
|
|
||||||
{
|
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
|
||||||
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
|
|
||||||
|
|
||||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
|
||||||
Allocate(desiredAddress, size, protection);
|
|
||||||
|
|
||||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
|
||||||
|
|
||||||
public bool Free(ulong address) => true;
|
|
||||||
|
|
||||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
||||||
{
|
|
||||||
rawOldProtection = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
||||||
{
|
|
||||||
rawOldProtection = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Query(ulong address, out HostRegionInfo info)
|
|
||||||
{
|
|
||||||
info = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FlushInstructionCache(ulong address, ulong size)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -84,74 +84,4 @@ public sealed class VirtualMemoryTests
|
|||||||
Assert.False(memory.TryRead(0x3000, unchanged));
|
Assert.False(memory.TryRead(0x3000, unchanged));
|
||||||
Assert.True(memory.TryWrite(0x3000, [9]));
|
Assert.True(memory.TryWrite(0x3000, [9]));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TryReadAndTryWriteOnUnmappedAddressReturnFalse()
|
|
||||||
{
|
|
||||||
var memory = new VirtualMemory();
|
|
||||||
memory.Map(0x1000, 0x100, 0, [], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
|
||||||
|
|
||||||
// Addresses with no covering region fail for both reads and writes.
|
|
||||||
Span<byte> value = stackalloc byte[1];
|
|
||||||
Assert.False(memory.TryRead(0x2000, value));
|
|
||||||
Assert.False(memory.TryWrite(0x2000, value));
|
|
||||||
|
|
||||||
// With no regions mapped at all, every access fails.
|
|
||||||
var empty = new VirtualMemory();
|
|
||||||
Assert.False(empty.TryRead(0x1000, value));
|
|
||||||
Assert.False(empty.TryWrite(0x1000, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zero-length operations succeed on a mapped address and touch nothing, but
|
|
||||||
// still require a mapped address.
|
|
||||||
[Fact]
|
|
||||||
public void ZeroLengthOperationsOnMappedAddressReturnTrue()
|
|
||||||
{
|
|
||||||
var memory = new VirtualMemory();
|
|
||||||
memory.Map(0x1000, 0x100, 0, [0x01], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
|
||||||
|
|
||||||
Assert.True(memory.TryRead(0x1000, []));
|
|
||||||
Assert.True(memory.TryWrite(0x1000, []));
|
|
||||||
|
|
||||||
Span<byte> value = stackalloc byte[1];
|
|
||||||
Assert.True(memory.TryRead(0x1000, value));
|
|
||||||
Assert.Equal(0x01, value[0]);
|
|
||||||
|
|
||||||
Assert.False(memory.TryRead(0x2000, []));
|
|
||||||
Assert.False(memory.TryWrite(0x2000, []));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A page-aligned region must be accessible at both offset 0 and the final byte.
|
|
||||||
[Fact]
|
|
||||||
public void MappingAtPageBoundarySupportsOffsetZeroAndLastByte()
|
|
||||||
{
|
|
||||||
const ulong pageSize = 0x1000;
|
|
||||||
var memory = new VirtualMemory();
|
|
||||||
memory.Map(pageSize, pageSize, 0, [], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
|
||||||
|
|
||||||
Assert.True(memory.TryWrite(pageSize, [0x5A]));
|
|
||||||
Assert.True(memory.TryWrite(pageSize + pageSize - 1, [0xA5]));
|
|
||||||
|
|
||||||
Span<byte> head = stackalloc byte[1];
|
|
||||||
Span<byte> tail = stackalloc byte[1];
|
|
||||||
Assert.True(memory.TryRead(pageSize, head));
|
|
||||||
Assert.True(memory.TryRead(pageSize + pageSize - 1, tail));
|
|
||||||
Assert.Equal(0x5A, head[0]);
|
|
||||||
Assert.Equal(0xA5, tail[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A read/write that starts in one region but extends into the unmapped gap
|
|
||||||
// between two non-adjacent regions must fail across the entire range.
|
|
||||||
[Fact]
|
|
||||||
public void ReadWriteAcrossGapBetweenNonAdjacentRegionsReturnsFalse()
|
|
||||||
{
|
|
||||||
const ulong pageSize = 0x1000;
|
|
||||||
var memory = new VirtualMemory();
|
|
||||||
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
|
|
||||||
memory.Map(pageSize, pageSize, 0, [], protection);
|
|
||||||
memory.Map(pageSize * 3, pageSize, 0, [], protection);
|
|
||||||
|
|
||||||
Assert.False(memory.TryRead(pageSize, new byte[(int)pageSize * 2]));
|
|
||||||
Assert.False(memory.TryWrite(pageSize, new byte[(int)pageSize * 2]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.Pad;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Pad;
|
|
||||||
|
|
||||||
public sealed class PadExportsTests
|
|
||||||
{
|
|
||||||
private const ulong Base = 0x1_0000_0000;
|
|
||||||
private const int InvalidHandle = unchecked((int)0x80920003);
|
|
||||||
|
|
||||||
private readonly FakeCpuMemory _memory = new(Base, 0x1000);
|
|
||||||
private readonly CpuContext _ctx;
|
|
||||||
|
|
||||||
public PadExportsTests()
|
|
||||||
{
|
|
||||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0, 0)]
|
|
||||||
[InlineData(1, 0)]
|
|
||||||
[InlineData(2, InvalidHandle)]
|
|
||||||
[InlineData(-1, InvalidHandle)]
|
|
||||||
public void SetTiltCorrectionState_ValidatesHandle(int handle, int expected)
|
|
||||||
{
|
|
||||||
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
|
|
||||||
Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.SaveData;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.SaveData;
|
|
||||||
|
|
||||||
[CollectionDefinition("SaveDataMemoryState", DisableParallelization = true)]
|
|
||||||
public sealed class SaveDataMemoryStateCollection;
|
|
||||||
|
|
||||||
// The save data memory exports persist to a real backing file whose resolved
|
|
||||||
// path depends on process-wide environment and title configuration. The
|
|
||||||
// fixture pins both and the collection keeps other environment-mutating tests
|
|
||||||
// from running alongside.
|
|
||||||
[Collection("SaveDataMemoryState")]
|
|
||||||
public sealed class SaveDataMemoryExportsTests : IDisposable
|
|
||||||
{
|
|
||||||
private const ulong Base = 0x1_0000_0000;
|
|
||||||
private const ulong SetupParamAddress = Base + 0x100;
|
|
||||||
private const ulong SetupResultAddress = Base + 0x180;
|
|
||||||
private const ulong DataStructAddress = Base + 0x200;
|
|
||||||
private const ulong RequestAddress = Base + 0x280;
|
|
||||||
private const ulong SyncParamAddress = Base + 0x300;
|
|
||||||
private const ulong PayloadAddress = Base + 0x400;
|
|
||||||
private const ulong ReadbackAddress = Base + 0x800;
|
|
||||||
private const ulong LargePayloadAddress = Base + 0x1000;
|
|
||||||
private const ulong LargeReadbackAddress = Base + 0x40000;
|
|
||||||
private const ulong MemorySize = 0x2000;
|
|
||||||
private const ulong UnmappedAddress = Base + 0x100000;
|
|
||||||
private const int MemoryNotReady = unchecked((int)0x809F0012);
|
|
||||||
private const int ParameterError = unchecked((int)0x809F0000);
|
|
||||||
private const int UserId = 0x1001;
|
|
||||||
private const string TitleId = "SDMEMTEST";
|
|
||||||
|
|
||||||
private readonly FakeCpuMemory _memory = new(Base, 0x80000);
|
|
||||||
private readonly CpuContext _ctx;
|
|
||||||
private readonly string _root;
|
|
||||||
private readonly string? _previousRoot;
|
|
||||||
|
|
||||||
public SaveDataMemoryExportsTests()
|
|
||||||
{
|
|
||||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
|
||||||
_root = Path.Combine(Path.GetTempPath(), $"sharpemu-sdmemory-{Guid.NewGuid():N}");
|
|
||||||
_previousRoot = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
|
||||||
Environment.SetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR", _root);
|
|
||||||
SaveDataExports.ConfigureApplicationInfo(TitleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string MemoryPath =>
|
|
||||||
Path.Combine(_root, UserId.ToString(), TitleId, "sce_sdmemory", "memory.dat");
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
SaveDataExports.ConfigureApplicationInfo(null);
|
|
||||||
Environment.SetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR", _previousRoot);
|
|
||||||
if (Directory.Exists(_root))
|
|
||||||
{
|
|
||||||
Directory.Delete(_root, recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Setup_ReportsExistingSizeOnSecondCall()
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
Assert.True(_ctx.TryReadUInt64(SetupResultAddress, out var existedSize));
|
|
||||||
Assert.Equal(0ul, existedSize);
|
|
||||||
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
Assert.True(_ctx.TryReadUInt64(SetupResultAddress, out existedSize));
|
|
||||||
Assert.Equal(MemorySize, existedSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SetThenGet_RoundTripsThroughBackingFile()
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
|
|
||||||
var payload = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(PayloadAddress, payload));
|
|
||||||
WriteRequest(PayloadAddress, (ulong)payload.Length, offset: 0x40);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
|
||||||
|
|
||||||
WriteRequest(ReadbackAddress, (ulong)payload.Length, offset: 0x40);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
var readback = new byte[payload.Length];
|
|
||||||
Assert.True(_ctx.Memory.TryRead(ReadbackAddress, readback));
|
|
||||||
Assert.Equal(payload, readback);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SetThenGet_LargePayload_RoundTrips()
|
|
||||||
{
|
|
||||||
const ulong size = 0x30000;
|
|
||||||
Assert.Equal(0, Setup(0x40000));
|
|
||||||
|
|
||||||
var payload = new byte[size];
|
|
||||||
for (var i = 0; i < payload.Length; i++)
|
|
||||||
{
|
|
||||||
payload[i] = (byte)(i * 31 + 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(LargePayloadAddress, payload));
|
|
||||||
WriteRequest(LargePayloadAddress, size, offset: 0x100);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
|
||||||
|
|
||||||
WriteRequest(LargeReadbackAddress, size, offset: 0x100);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
var readback = new byte[size];
|
|
||||||
Assert.True(_ctx.Memory.TryRead(LargeReadbackAddress, readback));
|
|
||||||
Assert.Equal(payload, readback);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Setup_AbsurdSize_ReturnsParameterError()
|
|
||||||
{
|
|
||||||
Assert.Equal(ParameterError, Setup(ulong.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Setup_InvalidResultPointer_DoesNotCreateBackingFile()
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
|
||||||
Setup(resultAddress: UnmappedAddress));
|
|
||||||
Assert.False(File.Exists(MemoryPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Setup_InvalidResultPointer_DoesNotGrowExistingFile()
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup(0x1000));
|
|
||||||
Assert.Equal(
|
|
||||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
|
||||||
Setup(MemorySize, UnmappedAddress));
|
|
||||||
Assert.Equal(0x1000, new FileInfo(MemoryPath).Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Setup_GrowingExistingMemory_ZeroExtendsAndPreservesContent()
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup(0x1000));
|
|
||||||
var payload = new byte[] { 0x11, 0x22 };
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(PayloadAddress, payload));
|
|
||||||
WriteRequest(PayloadAddress, (ulong)payload.Length, offset: 0xFF0);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
|
||||||
|
|
||||||
Assert.Equal(0, Setup(MemorySize));
|
|
||||||
Assert.True(_ctx.TryReadUInt64(SetupResultAddress, out var existedSize));
|
|
||||||
Assert.Equal(0x1000ul, existedSize);
|
|
||||||
|
|
||||||
WriteRequest(ReadbackAddress, 0x20, offset: 0xFF0);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
var readback = new byte[0x20];
|
|
||||||
Assert.True(_ctx.Memory.TryRead(ReadbackAddress, readback));
|
|
||||||
Assert.Equal(payload, readback[..2]);
|
|
||||||
Assert.All(readback[2..], b => Assert.Equal(0, b));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetSetSync_BeforeSetup_ReturnMemoryNotReady()
|
|
||||||
{
|
|
||||||
WriteRequest(ReadbackAddress, 0x10, offset: 0);
|
|
||||||
Assert.Equal(MemoryNotReady, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
Assert.Equal(MemoryNotReady, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
|
||||||
Assert.Equal(MemoryNotReady, Sync());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetAndSync_AfterSetup_Succeed()
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
WriteRequest(ReadbackAddress, 0x10, offset: 0);
|
|
||||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
Assert.Equal(0, Sync());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Get_BackingFileRemovedAfterSetup_ReturnsMemoryNotReady()
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
Directory.Delete(_root, recursive: true);
|
|
||||||
WriteRequest(ReadbackAddress, 0x10, offset: 0);
|
|
||||||
Assert.Equal(MemoryNotReady, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(MemorySize, 1ul)]
|
|
||||||
[InlineData(0ul, MemorySize + 1)]
|
|
||||||
[InlineData(ulong.MaxValue, 0x10ul)]
|
|
||||||
public void Get_OutOfRange_ReturnsParameterError(ulong offset, ulong size)
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
WriteRequest(ReadbackAddress, size, offset);
|
|
||||||
Assert.Equal(ParameterError, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(MemorySize, 1ul)]
|
|
||||||
[InlineData(0ul, MemorySize + 1)]
|
|
||||||
[InlineData(ulong.MaxValue, 0x10ul)]
|
|
||||||
public void Set_OutOfRange_ReturnsParameterError(ulong offset, ulong size)
|
|
||||||
{
|
|
||||||
Assert.Equal(0, Setup());
|
|
||||||
WriteRequest(PayloadAddress, size, offset);
|
|
||||||
Assert.Equal(ParameterError, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private int Setup(ulong memorySize = MemorySize, ulong resultAddress = SetupResultAddress)
|
|
||||||
{
|
|
||||||
Span<byte> param = stackalloc byte[0x40];
|
|
||||||
param.Clear();
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(param[0x04..], UserId);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(param[0x08..], memorySize);
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(SetupParamAddress, param));
|
|
||||||
|
|
||||||
_ctx[CpuRegister.Rdi] = SetupParamAddress;
|
|
||||||
_ctx[CpuRegister.Rsi] = resultAddress;
|
|
||||||
return SaveDataExports.SaveDataSetupSaveDataMemory2(_ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int Sync()
|
|
||||||
{
|
|
||||||
Span<byte> param = stackalloc byte[0x28];
|
|
||||||
param.Clear();
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(param, UserId);
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(SyncParamAddress, param));
|
|
||||||
|
|
||||||
_ctx[CpuRegister.Rdi] = SyncParamAddress;
|
|
||||||
return SaveDataExports.SaveDataSyncSaveDataMemory(_ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteRequest(ulong bufAddress, ulong bufSize, ulong offset)
|
|
||||||
{
|
|
||||||
Span<byte> data = stackalloc byte[0x18];
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data, bufAddress);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0x08..], bufSize);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0x10..], offset);
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(DataStructAddress, data));
|
|
||||||
|
|
||||||
Span<byte> request = stackalloc byte[0x10];
|
|
||||||
request.Clear();
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(request, UserId);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(request[0x08..], DataStructAddress);
|
|
||||||
Assert.True(_ctx.Memory.TryWrite(RequestAddress, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
private CpuContext Invoke()
|
|
||||||
{
|
|
||||||
_ctx[CpuRegister.Rdi] = RequestAddress;
|
|
||||||
return _ctx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.Libs.VideoOut;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Covers the VideoOut pixel-format mapping functions that bridge the PS5 VideoOut
|
|
||||||
/// format space to the AGC guest texture format space. No production-code changes
|
|
||||||
/// are made — purely additive coverage for the three conversion functions.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class VideoOutPixelFormatTests
|
|
||||||
{
|
|
||||||
// ---- GetBytesPerPixel ----
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetBytesPerPixel_Known8BitRgbaFormats_Returns4()
|
|
||||||
{
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x80000000UL)); // A8R8G8B8Srgb
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x80002200UL)); // A8B8G8R8Srgb
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x8000000022000000UL)); // 2R8G8B8A8Srgb
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x8000000000000000UL)); // 2B8G8R8A8Srgb
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetBytesPerPixel_Known10BitFormats_Returns4()
|
|
||||||
{
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x88060000UL)); // A2R10G10B10
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x8100000622000000UL)); // 2R10G10B10A2
|
|
||||||
Assert.Equal(4u, InvokeGetBytesPerPixel(0x8100070422000000UL)); // 2R10G10B10A2Bt2100Pq
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetBytesPerPixel_UnknownFormat_Returns0()
|
|
||||||
{
|
|
||||||
Assert.Equal(0u, InvokeGetBytesPerPixel(0x00000000UL));
|
|
||||||
Assert.Equal(0u, InvokeGetBytesPerPixel(0xDEADBEEFUL));
|
|
||||||
Assert.Equal(0u, InvokeGetBytesPerPixel(0xFFFFFFFFFFFFFFFFUL));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- NormalizePixelFormat ----
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizePixelFormat_AlreadyNormalized_ReturnsUnchanged()
|
|
||||||
{
|
|
||||||
var alreadyNormalized = 0x8000000000000000UL; // 2B8G8R8A8Srgb
|
|
||||||
Assert.Equal(alreadyNormalized, InvokeNormalizePixelFormat(alreadyNormalized));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizePixelFormat_Low32BitsMatch_ReturnsLow32()
|
|
||||||
{
|
|
||||||
var format = 0xDEAD000088060000UL; // high bits garbage, low = A2R10G10B10
|
|
||||||
Assert.Equal(0x88060000UL, InvokeNormalizePixelFormat(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizePixelFormat_High32BitsMatch_ReturnsHigh32()
|
|
||||||
{
|
|
||||||
var format = 0x8806000000000001UL; // high = A2R10G10B10, low = unknown
|
|
||||||
Assert.Equal(0x88060000UL, InvokeNormalizePixelFormat(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizePixelFormat_CompletelyUnknown_ReturnsOriginal()
|
|
||||||
{
|
|
||||||
var format = 0xCAFEBABECAFEBABEUL;
|
|
||||||
Assert.Equal(format, InvokeNormalizePixelFormat(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- MapPixelFormatToGuestTextureFormat ----
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MapPixelFormat_8BitRgba_Returns56()
|
|
||||||
{
|
|
||||||
Assert.Equal(56u, InvokeMapPixelFormat(0x80000000UL));
|
|
||||||
Assert.Equal(56u, InvokeMapPixelFormat(0x8000000022000000UL));
|
|
||||||
Assert.Equal(56u, InvokeMapPixelFormat(0x8000000000000000UL));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MapPixelFormat_10BitPacked_Returns9()
|
|
||||||
{
|
|
||||||
Assert.Equal(9u, InvokeMapPixelFormat(0x88060000UL));
|
|
||||||
Assert.Equal(9u, InvokeMapPixelFormat(0x8100000622000000UL));
|
|
||||||
Assert.Equal(9u, InvokeMapPixelFormat(0x8100070422000000UL));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MapPixelFormat_Unknown_Returns56()
|
|
||||||
{
|
|
||||||
// Unknown formats now fall back to 56 (8-bit RGBA) so games display
|
|
||||||
// something instead of silently failing the flip.
|
|
||||||
Assert.Equal(56u, InvokeMapPixelFormat(0x00000000UL));
|
|
||||||
Assert.Equal(56u, InvokeMapPixelFormat(0xDEADBEEFUL));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Self-check activation ----
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void StaticConstructor_RunsSelfChecks_WithoutThrowing()
|
|
||||||
{
|
|
||||||
// RunPixelFormatSelfChecks is called from the static constructor.
|
|
||||||
// If the checks fail, Debug.Assert will fail in a debug build.
|
|
||||||
// This test ensures the constructor executed without throwing.
|
|
||||||
Assert.True(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Reflection-based access to internal/private methods ----
|
|
||||||
|
|
||||||
private static uint InvokeGetBytesPerPixel(ulong pixelFormat)
|
|
||||||
{
|
|
||||||
var method = typeof(VideoOutExports)
|
|
||||||
.GetMethod("GetBytesPerPixel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
|
||||||
return (uint)method!.Invoke(null, [pixelFormat])!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong InvokeNormalizePixelFormat(ulong pixelFormat)
|
|
||||||
{
|
|
||||||
var method = typeof(VideoOutExports)
|
|
||||||
.GetMethod("NormalizePixelFormat", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
|
||||||
return (ulong)method!.Invoke(null, [pixelFormat])!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint InvokeMapPixelFormat(ulong pixelFormat)
|
|
||||||
{
|
|
||||||
var method = typeof(VideoOutExports)
|
|
||||||
.GetMethod("MapPixelFormatToGuestTextureFormat", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
|
||||||
return (uint)method!.Invoke(null, [pixelFormat])!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.Libs.Agc;
|
|
||||||
using SharpEmu.Libs.Gpu;
|
|
||||||
using SharpEmu.Libs.VideoOut;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
|
||||||
|
|
||||||
public sealed class VulkanDepthAttachmentTests
|
|
||||||
{
|
|
||||||
private static readonly GuestDepthTarget Target = new(
|
|
||||||
ReadAddress: 0x1000,
|
|
||||||
WriteAddress: 0x1000,
|
|
||||||
Width: 1920,
|
|
||||||
Height: 1080,
|
|
||||||
GuestFormat: 1,
|
|
||||||
SwizzleMode: 0,
|
|
||||||
ClearDepth: 1f,
|
|
||||||
ReadOnly: false);
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GuestDepthTarget_AttachesForDepthWork()
|
|
||||||
{
|
|
||||||
var state = new GuestDepthState(
|
|
||||||
TestEnable: true,
|
|
||||||
WriteEnable: true,
|
|
||||||
CompareOp: 3);
|
|
||||||
|
|
||||||
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(true, false)]
|
|
||||||
[InlineData(false, true)]
|
|
||||||
public void GuestDepthTarget_AttachesForEitherDepthOperation(
|
|
||||||
bool testEnable,
|
|
||||||
bool writeEnable)
|
|
||||||
{
|
|
||||||
var state = new GuestDepthState(testEnable, writeEnable, CompareOp: 3);
|
|
||||||
|
|
||||||
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GuestDepthTarget_RequiresTargetAndDepthWork()
|
|
||||||
{
|
|
||||||
var state = GuestDepthState.Default;
|
|
||||||
|
|
||||||
Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
|
||||||
Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(
|
|
||||||
target: null,
|
|
||||||
new GuestDepthState(true, false, CompareOp: 3)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GuestDepthTarget_AttachesForDepthClear()
|
|
||||||
{
|
|
||||||
var state = new GuestDepthState(
|
|
||||||
TestEnable: false,
|
|
||||||
WriteEnable: false,
|
|
||||||
CompareOp: 7,
|
|
||||||
ClearEnable: true);
|
|
||||||
|
|
||||||
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0x41u, true)]
|
|
||||||
[InlineData(0x40u, false)]
|
|
||||||
public void DepthState_DecodesRenderControlClearBit(
|
|
||||||
uint renderControl,
|
|
||||||
bool clearEnable)
|
|
||||||
{
|
|
||||||
var registers = new Dictionary<uint, uint>
|
|
||||||
{
|
|
||||||
[0x000] = renderControl,
|
|
||||||
[0x200] = 0x776,
|
|
||||||
};
|
|
||||||
|
|
||||||
var state = AgcExports.DecodeDepthState(registers);
|
|
||||||
|
|
||||||
Assert.True(state.TestEnable);
|
|
||||||
Assert.True(state.WriteEnable);
|
|
||||||
Assert.Equal(7u, state.CompareOp);
|
|
||||||
Assert.Equal(clearEnable, state.ClearEnable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user