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:
|
||||
branches:
|
||||
- "**"
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
@@ -55,11 +53,6 @@ jobs:
|
||||
release_tag="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 "safe-ref=${safe_ref}"
|
||||
@@ -210,9 +203,7 @@ jobs:
|
||||
- init
|
||||
- build
|
||||
- build-posix
|
||||
# Versioned releases are immutable and tag-driven. Branch and manual runs
|
||||
# still produce Actions artifacts without modifying a published release.
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -222,7 +213,7 @@ jobs:
|
||||
with:
|
||||
path: release
|
||||
|
||||
- name: Create release
|
||||
- name: Create or update release
|
||||
shell: bash
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
@@ -240,11 +231,26 @@ jobs:
|
||||
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
|
||||
|
||||
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
||||
exit 1
|
||||
gh release upload "${RELEASE_TAG}" "${assets[@]}" --clobber
|
||||
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
|
||||
|
||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||
--verify-tag \
|
||||
--title "${RELEASE_NAME}" \
|
||||
--notes "${notes}"
|
||||
keep=()
|
||||
for asset_path in "${assets[@]}"; do
|
||||
keep+=("$(basename "${asset_path}")")
|
||||
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
|
||||
|
||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.2-beta.2</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
@@ -41,16 +41,6 @@ This project is developed purely for research and educational purposes. There ar
|
||||
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**.
|
||||
|
||||
## Games Tested
|
||||
|
||||
| Demons Souls Remake | Dreaming Sarah |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Void Terrarium | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
## 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.
|
||||
@@ -98,6 +88,17 @@ chmod +x ./SharpEmu
|
||||
A Vulkan-capable GPU and current graphics driver are required. The macOS
|
||||
release includes the MoltenVK Vulkan implementation.
|
||||
|
||||
## Games Tested
|
||||
|
||||
| Demons Souls Remake | Dreaming Sarah |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Void Terrarium | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This project does **not** support or condone piracy.
|
||||
> 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);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)hostRspStorage;
|
||||
var emitter = new NativeCodeEmitter(ptr2);
|
||||
int offset = 0;
|
||||
|
||||
emitter.Emit(0x53); // push rbx
|
||||
emitter.Emit(0x55); // push rbp
|
||||
emitter.Emit(0x57); // push rdi
|
||||
emitter.Emit(0x56); // push rsi
|
||||
emitter.Emit(0x41); emitter.Emit(0x54); // push r12
|
||||
emitter.Emit(0x41); emitter.Emit(0x55); // push r13
|
||||
emitter.Emit(0x41); emitter.Emit(0x56); // push r14
|
||||
emitter.Emit(0x41); emitter.Emit(0x57); // push r15
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref emitter.Offset);
|
||||
void Emit(byte value) => ptr2[offset++] = value;
|
||||
void EmitU64(ulong value)
|
||||
{
|
||||
*(ulong*)(ptr2 + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitMovR64Imm(byte rex, byte opcode, ulong value)
|
||||
{
|
||||
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
|
||||
// abandoning the host stack. This path is used when a blocked guest
|
||||
// continuation migrates to another managed worker.
|
||||
emitter.Emit(0x48); emitter.Emit(0x83); emitter.Emit(0xEC); emitter.Emit(0x08); // sub rsp,8
|
||||
emitter.Emit(0xC7); emitter.Emit(0x04); emitter.Emit(0x24); // mov dword [rsp],imm32
|
||||
emitter.Emit(context.Mxcsr);
|
||||
emitter.Emit(0x0F); emitter.Emit(0xAE); emitter.Emit(0x14); emitter.Emit(0x24); // ldmxcsr [rsp]
|
||||
emitter.Emit(0x66); emitter.Emit(0xC7); emitter.Emit(0x04); emitter.Emit(0x24); // mov word [rsp],imm16
|
||||
emitter.Emit(context.FpuControlWord);
|
||||
emitter.Emit(0xD9); emitter.Emit(0x2C); emitter.Emit(0x24); // fldcw [rsp]
|
||||
emitter.Emit(0x48); emitter.Emit(0x83); emitter.Emit(0xC4); emitter.Emit(0x08); // add rsp,8
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBA, hostRspSlot); // mov r10, hostRspSlot
|
||||
emitter.Emit(0x49); emitter.Emit(0x89); emitter.Emit(0x22); // mov [r10], rsp
|
||||
emitter.EmitMovR64Immediate(0x48, 0xB8, context[CpuRegister.Rsp]); // mov rax, guest rsp
|
||||
emitter.Emit(0x48); emitter.Emit(0x89); emitter.Emit(0xC4); // mov rsp, rax
|
||||
emitter.Emit(0x48); emitter.Emit(0x83); emitter.Emit(0xEC); emitter.Emit(0x08); // reserve transfer slot
|
||||
emitter.EmitMovR64Immediate(0x48, 0xB8, entryPoint); // mov rax, entryPoint
|
||||
emitter.Emit(0x48); emitter.Emit(0x89); emitter.Emit(0x04); emitter.Emit(0x24); // mov [rsp],rax
|
||||
emitter.EmitMovR64Immediate(0x48, 0xBB, context[CpuRegister.Rbx]); // mov rbx, imm64
|
||||
emitter.EmitMovR64Immediate(0x48, 0xBD, context[CpuRegister.Rbp]); // mov rbp, imm64
|
||||
emitter.EmitMovR64Immediate(0x48, 0xBF, context[CpuRegister.Rdi]); // mov rdi, imm64
|
||||
emitter.EmitMovR64Immediate(0x48, 0xBE, context[CpuRegister.Rsi]); // mov rsi, imm64
|
||||
emitter.EmitMovR64Immediate(0x48, 0xBA, context[CpuRegister.Rdx]); // mov rdx, imm64
|
||||
emitter.EmitMovR64Immediate(0x48, 0xB9, context[CpuRegister.Rcx]); // mov rcx, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xB8, context[CpuRegister.R8]); // mov r8, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xB9, context[CpuRegister.R9]); // mov r9, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBA, context[CpuRegister.R10]); // mov r10, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBC, context[CpuRegister.R12]); // mov r12, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBD, context[CpuRegister.R13]); // mov r13, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBE, context[CpuRegister.R14]); // mov r14, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBF, context[CpuRegister.R15]); // mov r15, imm64
|
||||
emitter.EmitMovR64Immediate(0x49, 0xBB, context[CpuRegister.R11]); // mov r11, imm64
|
||||
emitter.EmitMovR64Immediate(0x48, 0xB8, context[CpuRegister.Rax]); // mov rax, imm64
|
||||
emitter.Emit(0xC3); // ret through the synthetic transfer slot
|
||||
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x08); // sub rsp,8
|
||||
Emit(0xC7); Emit(0x04); Emit(0x24); // mov dword [rsp],imm32
|
||||
*(uint*)(ptr2 + offset) = context.Mxcsr; offset += sizeof(uint);
|
||||
Emit(0x0F); Emit(0xAE); Emit(0x14); Emit(0x24); // ldmxcsr [rsp]
|
||||
Emit(0x66); Emit(0xC7); Emit(0x04); Emit(0x24); // mov word [rsp],imm16
|
||||
*(ushort*)(ptr2 + offset) = context.FpuControlWord; offset += sizeof(ushort);
|
||||
Emit(0xD9); Emit(0x2C); Emit(0x24); // fldcw [rsp]
|
||||
Emit(0x48); Emit(0x83); Emit(0xC4); Emit(0x08); // add rsp,8
|
||||
EmitMovR64Imm(0x49, 0xBA, hostRspSlot); // mov r10, hostRspSlot
|
||||
Emit(0x49); Emit(0x89); Emit(0x22); // mov [r10], rsp
|
||||
EmitMovR64Imm(0x48, 0xB8, context[CpuRegister.Rsp]); // mov rax, guest rsp
|
||||
Emit(0x48); Emit(0x89); Emit(0xC4); // mov rsp, rax
|
||||
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x08); // reserve transfer slot
|
||||
EmitMovR64Imm(0x48, 0xB8, entryPoint); // mov rax, entryPoint
|
||||
Emit(0x48); Emit(0x89); Emit(0x04); Emit(0x24); // mov [rsp],rax
|
||||
EmitMovR64Imm(0x48, 0xBB, context[CpuRegister.Rbx]); // mov rbx, imm64
|
||||
EmitMovR64Imm(0x48, 0xBD, context[CpuRegister.Rbp]); // mov rbp, imm64
|
||||
EmitMovR64Imm(0x48, 0xBF, context[CpuRegister.Rdi]); // mov rdi, imm64
|
||||
EmitMovR64Imm(0x48, 0xBE, context[CpuRegister.Rsi]); // mov rsi, imm64
|
||||
EmitMovR64Imm(0x48, 0xBA, context[CpuRegister.Rdx]); // mov rdx, imm64
|
||||
EmitMovR64Imm(0x48, 0xB9, context[CpuRegister.Rcx]); // mov rcx, imm64
|
||||
EmitMovR64Imm(0x49, 0xB8, context[CpuRegister.R8]); // mov r8, imm64
|
||||
EmitMovR64Imm(0x49, 0xB9, context[CpuRegister.R9]); // mov r9, imm64
|
||||
EmitMovR64Imm(0x49, 0xBA, context[CpuRegister.R10]); // mov r10, imm64
|
||||
EmitMovR64Imm(0x49, 0xBC, context[CpuRegister.R12]); // mov r12, imm64
|
||||
EmitMovR64Imm(0x49, 0xBD, context[CpuRegister.R13]); // mov r13, imm64
|
||||
EmitMovR64Imm(0x49, 0xBE, context[CpuRegister.R14]); // mov r14, imm64
|
||||
EmitMovR64Imm(0x49, 0xBF, context[CpuRegister.R15]); // mov r15, imm64
|
||||
EmitMovR64Imm(0x49, 0xBB, context[CpuRegister.R11]); // mov r11, imm64
|
||||
EmitMovR64Imm(0x48, 0xB8, context[CpuRegister.Rax]); // mov rax, imm64
|
||||
Emit(0xC3); // ret through the synthetic transfer slot
|
||||
ActiveEntryReturnSentinelRip = (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)
|
||||
{
|
||||
if (alignment == 0)
|
||||
|
||||
@@ -197,9 +197,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
|
||||
{
|
||||
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
|
||||
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;
|
||||
|
||||
@@ -198,24 +198,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
{
|
||||
if (size == 0)
|
||||
|
||||
@@ -141,8 +141,6 @@
|
||||
"Options.About" : "About",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Source code, issues and project development.",
|
||||
"About.Github.LatestCommitLabel": "Latest Commit",
|
||||
"About.Github.LatestCommitDescription": "Latest commit on the main branch",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||
"About.GithubButton": "Contribute in GitHub!",
|
||||
|
||||
@@ -131,8 +131,6 @@
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Github.LatestCommitLabel": "Último Commit",
|
||||
"About.Github.LatestCommitDescription": "Último commit en la rama main",
|
||||
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||
"About.GithubButton": "Contribuye en GitHub!",
|
||||
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||
|
||||
@@ -333,35 +333,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Classes="sectionTitle"
|
||||
Text="ABOUT" />
|
||||
|
||||
<!--Latest commit info-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||
Width="24" Height="24" VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
|
||||
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1" x:Name="LatestCommitHashText" Classes="ghost"
|
||||
Content="Loading…" FontSize="12" FontFamily="Consolas,monospace"
|
||||
VerticalAlignment="Center" IsEnabled="False" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<!--Update-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/update-icon.png"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
||||
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="0" 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>
|
||||
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
||||
Content="Check for updates" VerticalAlignment="Center" />
|
||||
|
||||
@@ -20,7 +20,6 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
@@ -79,10 +78,6 @@ public partial class MainWindow : Window
|
||||
private long _navUpNextAt;
|
||||
private long _navDownNextAt;
|
||||
|
||||
//Github http client for latest commit
|
||||
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
|
||||
private string? _latestCommitSha;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -200,21 +195,6 @@ public partial class MainWindow : Window
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
|
||||
LatestCommitHashText.Click += (_, _) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_latestCommitSha))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName =
|
||||
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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 ----
|
||||
|
||||
private void PollGamepad()
|
||||
@@ -486,8 +381,6 @@ public partial class MainWindow : Window
|
||||
ApplySettingsToControls();
|
||||
LocateEmulator();
|
||||
UpdateDiscordPresence();
|
||||
_ = LoadLatestCommitAsync();
|
||||
|
||||
if (_settings.CheckForUpdatesOnStartup)
|
||||
{
|
||||
_ = CheckForUpdatesAsync();
|
||||
@@ -623,8 +516,6 @@ public partial class MainWindow : Window
|
||||
GithubButton.Content = loc.Get("About.GithubButton");
|
||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||
UpdateLabel.Text = loc.Get("Updater.Label");
|
||||
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
|
||||
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
|
||||
RefreshUpdateText();
|
||||
|
||||
UpdateEmptyStateTexts();
|
||||
|
||||
@@ -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\github.png" Link="Assets/github.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+100
-150
@@ -121,7 +121,6 @@ public static partial class AgcExports
|
||||
private const uint CbBlend0Control = 0x1E0;
|
||||
private const uint PaScModeCntl0 = 0x292;
|
||||
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
||||
private const uint DbRenderControl = 0x000;
|
||||
private const uint DbDepthView = 0x002;
|
||||
private const uint DbDepthSizeXy = 0x007;
|
||||
private const uint DbDepthClear = 0x00B;
|
||||
@@ -3729,16 +3728,13 @@ public static partial class AgcExports
|
||||
_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} " +
|
||||
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
||||
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
||||
$"packet=0x{packetAddress:X16} action='{debugName}'");
|
||||
}
|
||||
TraceAgc(
|
||||
$"agc.wait_producer_scheduled label=0x{waiting.Address:X16} " +
|
||||
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
||||
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
||||
$"packet=0x{packetAddress:X16} action='{debugName}'");
|
||||
}
|
||||
|
||||
return producer;
|
||||
@@ -3756,19 +3752,16 @@ public static partial class AgcExports
|
||||
producer.Completed = true;
|
||||
}
|
||||
|
||||
if (_traceAgc)
|
||||
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(
|
||||
producer.Memory,
|
||||
producer.Address,
|
||||
producer.Length))
|
||||
{
|
||||
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(
|
||||
producer.Memory,
|
||||
producer.Address,
|
||||
producer.Length))
|
||||
{
|
||||
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}'");
|
||||
}
|
||||
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 current = currentValue.HasValue
|
||||
? $"0x{currentValue.Value:X16}"
|
||||
@@ -5306,7 +5291,7 @@ public static partial class AgcExports
|
||||
var hasDepthOnlyCandidate = hasExportShader &&
|
||||
!hasPixelShader &&
|
||||
depthTarget is not null &&
|
||||
(depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
|
||||
(depthState.TestEnable || depthState.WriteEnable);
|
||||
if (hasDepthOnlyCandidate &&
|
||||
TryCreateTranslatedDepthOnlyGuestDraw(
|
||||
ctx,
|
||||
@@ -5951,14 +5936,13 @@ public static partial class AgcExports
|
||||
// Every bound color target the shader exports to. Deferred renderers
|
||||
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
||||
// Fall back to slot 0 if we cannot match any export to a bound target.
|
||||
var pixelColorExportMasks = pixelState.Program.PixelColorExportMasks;
|
||||
var allBoundTargets = GetRenderTargets(state.CxRegisters);
|
||||
// At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
|
||||
// closure allocations. Slots are distinct, so sorting by slot is stable.
|
||||
var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
|
||||
foreach (var target in allBoundTargets)
|
||||
{
|
||||
if (GetPixelColorExportMask(pixelColorExportMasks, target.Slot) != 0)
|
||||
if (HasPixelColorExport(pixelState, target.Slot))
|
||||
{
|
||||
selectedTargets.Add(target);
|
||||
}
|
||||
@@ -5981,7 +5965,7 @@ public static partial class AgcExports
|
||||
{
|
||||
TraceAgcShader(
|
||||
$"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}");
|
||||
}
|
||||
|
||||
@@ -6102,39 +6086,54 @@ public static partial class AgcExports
|
||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||
}
|
||||
|
||||
var imageBindings = pixelEvaluation.ImageBindings
|
||||
.Concat(exportEvaluation.ImageBindings);
|
||||
var textures = new List<TranslatedImageBinding>(
|
||||
pixelEvaluation.ImageBindings.Count +
|
||||
exportEvaluation.ImageBindings.Count);
|
||||
if (!TryAppendTranslatedImageBindings(
|
||||
pixelEvaluation.ImageBindings,
|
||||
textures,
|
||||
pixelShaderAddress,
|
||||
exportShaderAddress,
|
||||
out error) ||
|
||||
!TryAppendTranslatedImageBindings(
|
||||
exportEvaluation.ImageBindings,
|
||||
textures,
|
||||
pixelShaderAddress,
|
||||
exportShaderAddress,
|
||||
out error))
|
||||
foreach (var binding in imageBindings)
|
||||
{
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
return false;
|
||||
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 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[
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count];
|
||||
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];
|
||||
}
|
||||
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||
.ToArray();
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
exportEvaluation.VertexInputs ?? [];
|
||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||
@@ -6149,13 +6148,6 @@ public static partial class AgcExports
|
||||
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(
|
||||
exportShaderAddress,
|
||||
pixelShaderAddress,
|
||||
@@ -6173,11 +6165,11 @@ public static partial class AgcExports
|
||||
DecodeDepthTarget(state.CxRegisters),
|
||||
guestTargets,
|
||||
ApplyTransparentPremultipliedFillClear(
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||
textures,
|
||||
vertexInputs,
|
||||
pixelEvaluation.InitialScalarRegisters),
|
||||
pixelUserData,
|
||||
pixelEvaluation.InitialScalarRegisters.Take(8).ToArray(),
|
||||
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
||||
state.CxRegisters.TryGetValue(
|
||||
CbColor0Info + renderTargets.FirstOrDefault().Slot * CbColorRegisterStride,
|
||||
@@ -6189,55 +6181,6 @@ public static partial class AgcExports
|
||||
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 _tracedAstroTitlePixelGlobalProbe;
|
||||
|
||||
@@ -6466,10 +6409,26 @@ public static partial class AgcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint GetPixelColorExportMask(uint packedMasks, uint target) =>
|
||||
target < ColorTargetCount
|
||||
? (packedMasks >> (int)(target * 4)) & 0xFu
|
||||
: 0;
|
||||
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
|
||||
GetPixelColorExportMask(state, target) != 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)
|
||||
{
|
||||
@@ -6667,7 +6626,7 @@ public static partial class AgcExports
|
||||
private static GuestRenderState CreateRenderState(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||
uint pixelColorExportMasks)
|
||||
Gen5ShaderState pixelState)
|
||||
{
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
@@ -6676,21 +6635,15 @@ public static partial class AgcExports
|
||||
|
||||
var target = targets[0];
|
||||
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(
|
||||
blends,
|
||||
targets.Select(target =>
|
||||
{
|
||||
var blend = DecodeBlendState(registers, target.Slot);
|
||||
return blend with
|
||||
{
|
||||
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
|
||||
};
|
||||
}).ToArray(),
|
||||
scissor,
|
||||
DecodeViewport(registers, target.Width, target.Height, scissor),
|
||||
DecodeRasterState(registers),
|
||||
@@ -6699,30 +6652,27 @@ public static partial class AgcExports
|
||||
|
||||
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
|
||||
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering).
|
||||
// DB_RENDER_CONTROL (context register 0x000): DEPTH_CLEAR_ENABLE bit0.
|
||||
private const uint DbDepthControl = 0x200;
|
||||
|
||||
internal static GuestDepthState DecodeDepthState(
|
||||
private static GuestDepthState DecodeDepthState(
|
||||
IReadOnlyDictionary<uint, uint> registers)
|
||||
{
|
||||
var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
|
||||
registers.TryGetValue(DbRenderControl, out var renderControl);
|
||||
if (!registers.TryGetValue(DbDepthControl, out var control))
|
||||
{
|
||||
return GuestDepthState.Default;
|
||||
}
|
||||
|
||||
var testEnable = (control & 0x2u) != 0;
|
||||
var writeEnable = (control & 0x4u) != 0;
|
||||
var compareOp = hasDepthControl
|
||||
? (control >> 4) & 0x7u
|
||||
: GuestDepthState.Default.CompareOp;
|
||||
var clearEnable = (renderControl & 0x1u) != 0;
|
||||
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
|
||||
var compareOp = (control >> 4) & 0x7u;
|
||||
return new GuestDepthState(testEnable, writeEnable, compareOp);
|
||||
}
|
||||
|
||||
private static GuestDepthTarget? DecodeDepthTarget(
|
||||
IReadOnlyDictionary<uint, uint> registers)
|
||||
{
|
||||
var depthState = DecodeDepthState(registers);
|
||||
if (!depthState.TestEnable &&
|
||||
!depthState.WriteEnable &&
|
||||
!depthState.ClearEnable)
|
||||
if (!depthState.TestEnable && !depthState.WriteEnable)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -52,19 +52,8 @@ internal static class AudioPcmConversion
|
||||
}
|
||||
|
||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||
return ConvertFloatSample(BitConverter.Int32BitsToSingle(bits));
|
||||
}
|
||||
|
||||
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));
|
||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||
return checked((short)MathF.Round(value * short.MaxValue));
|
||||
}
|
||||
|
||||
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) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3OdRkSjOcog",
|
||||
ExportName = "sceFontBindRenderer",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int BindRenderer(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "N1EBMeGhf7E",
|
||||
ExportName = "sceFontSetScalePixel",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetScalePixel(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TMtqoFQjjbA",
|
||||
ExportName = "sceFontSetEffectSlant",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetEffectSlant(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v0phZwa4R5o",
|
||||
ExportName = "sceFontSetEffectWeight",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetEffectWeight(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6vGCkkQJOcI",
|
||||
ExportName = "sceFontSetupRenderScalePixel",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetupRenderScalePixel(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lz9y9UFO2UU",
|
||||
ExportName = "sceFontSetupRenderEffectSlant",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetupRenderEffectSlant(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XIGorvLusDQ",
|
||||
ExportName = "sceFontSetupRenderEffectWeight",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetupRenderEffectWeight(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "imxVx8lm+KM",
|
||||
ExportName = "sceFontGetHorizontalLayout",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int GetHorizontalLayout(CpuContext ctx)
|
||||
{
|
||||
var layoutAddress = ctx[CpuRegister.Rsi];
|
||||
if (layoutAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Baseline, line advance, decoration extent: the same invented geometry
|
||||
// as GetRenderCharGlyphMetrics.
|
||||
var values = new[] { 12.0f, 16.0f, 0.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
ctx,
|
||||
layoutAddress + (ulong)(index * sizeof(float)),
|
||||
BitConverter.SingleToUInt32Bits(values[index])))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "cKYtVmeSTcw",
|
||||
ExportName = "sceFontOpenFontSet",
|
||||
|
||||
@@ -91,10 +91,9 @@ internal readonly record struct GuestRasterState(
|
||||
internal readonly record struct GuestDepthState(
|
||||
bool TestEnable,
|
||||
bool WriteEnable,
|
||||
uint CompareOp,
|
||||
bool ClearEnable = false)
|
||||
uint CompareOp)
|
||||
{
|
||||
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
|
||||
|
||||
@@ -127,19 +127,6 @@ public static class PadExports
|
||||
: 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(
|
||||
Nid = "gjP9-KQzoUk",
|
||||
ExportName = "scePadGetControllerInformation",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -15,7 +14,6 @@ public static class SaveDataExports
|
||||
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
||||
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||
private const int OrbisSaveDataErrorMemoryNotReady = unchecked((int)0x809F0012);
|
||||
private const int SaveDataTitleIdSize = 10;
|
||||
private const int SaveDataDirNameSize = 32;
|
||||
private const int SaveDataParamSize = 0x530;
|
||||
@@ -31,10 +29,7 @@ public static class SaveDataExports
|
||||
private const uint MountModeCreate = 1u << 2;
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
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 _memoryGate = new();
|
||||
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||
private static string? _titleId;
|
||||
|
||||
@@ -477,19 +472,6 @@ public static class SaveDataExports
|
||||
private static string ResolveTitleSaveRoot(int userId, string 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()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
@@ -688,197 +670,4 @@ public static class SaveDataExports
|
||||
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
// Save data memory: a small per-user blob titles read and write without
|
||||
// mounting anything, backed by one zero-filled file per user and title.
|
||||
[SysAbiExport(
|
||||
Nid = "oQySEUfgXRA",
|
||||
ExportName = "sceSaveDataSetupSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSetupSaveDataMemory2(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, paramAddress + 0x04, out var userId) ||
|
||||
!ctx.TryReadUInt64(paramAddress + 0x08, out var memorySize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || memorySize == 0 || memorySize > SaveDataMemoryMaxSize)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = ResolveSaveDataMemoryPath(userId);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
var backing = new FileInfo(path);
|
||||
var existedSize = backing.Exists ? (ulong)backing.Length : 0;
|
||||
|
||||
// The result write comes first so a faulted result pointer
|
||||
// cannot leave created or grown setup state behind.
|
||||
if (resultAddress != 0 && !ctx.TryWriteUInt64(resultAddress, existedSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (existedSize < memorySize)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
using var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
stream.SetLength((long)memorySize);
|
||||
}
|
||||
|
||||
TraceSaveData($"memory-setup2 user={userId} size=0x{memorySize:X} existed=0x{existedSize:X}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QwOO7vegnV8",
|
||||
ExportName = "sceSaveDataGetSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataGetSaveDataMemory2(CpuContext ctx) =>
|
||||
TransferSaveDataMemory(ctx, write: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "cduy9v4YmT4",
|
||||
ExportName = "sceSaveDataSetSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSetSaveDataMemory2(CpuContext ctx) =>
|
||||
TransferSaveDataMemory(ctx, write: true);
|
||||
|
||||
// Writes go straight through to the backing file, so a ready state is
|
||||
// all sync has to confirm.
|
||||
[SysAbiExport(
|
||||
Nid = "wiT9jeC7xPw",
|
||||
ExportName = "sceSaveDataSyncSaveDataMemory",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSyncSaveDataMemory(CpuContext ctx)
|
||||
{
|
||||
var syncAddress = ctx[CpuRegister.Rdi];
|
||||
if (syncAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, syncAddress, out var userId))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(
|
||||
File.Exists(ResolveSaveDataMemoryPath(userId)) ? 0 : OrbisSaveDataErrorMemoryNotReady);
|
||||
}
|
||||
|
||||
private static int TransferSaveDataMemory(CpuContext ctx, bool write)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, requestAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(requestAddress + 0x08, out var dataAddress))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = ResolveSaveDataMemoryPath(userId);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorMemoryNotReady);
|
||||
}
|
||||
|
||||
if (dataAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (!TryReadMemoryData(ctx, dataAddress, out var bufAddress, out var bufSize, out var offset))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
using var stream = new FileStream(
|
||||
path, FileMode.Open, write ? FileAccess.ReadWrite : FileAccess.Read);
|
||||
var length = (ulong)stream.Length;
|
||||
if (bufAddress == 0 || bufSize > length || offset > length - bufSize)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
// The guarded file length bounds bufSize, so one rented buffer
|
||||
// covers the transfer and a guest fault never partially writes.
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Max(bufSize, 1));
|
||||
try
|
||||
{
|
||||
var span = buffer.AsSpan(0, (int)bufSize);
|
||||
stream.Seek((long)offset, SeekOrigin.Begin);
|
||||
if (write)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(bufAddress, span))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
stream.Write(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.ReadExactly(span);
|
||||
if (!ctx.Memory.TryWrite(bufAddress, span))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"memory-{(write ? "set2" : "get2")} user={userId} offset=0x{offset:X} size=0x{bufSize:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1646,12 +1646,8 @@ public static class VideoOutExports
|
||||
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||
// 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).
|
||||
// Unknown formats default to 56 (8-bit RGBA) with a logged warning so games
|
||||
// display something rather than silently failing the flip pipeline.
|
||||
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat)
|
||||
{
|
||||
var normalized = NormalizePixelFormat(pixelFormat);
|
||||
var result = normalized switch
|
||||
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
|
||||
NormalizePixelFormat(pixelFormat) switch
|
||||
{
|
||||
SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||
@@ -1669,17 +1665,6 @@ public static class VideoOutExports
|
||||
_ => 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(
|
||||
ulong pixelFormat,
|
||||
byte red,
|
||||
|
||||
@@ -2213,12 +2213,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return keys;
|
||||
}
|
||||
|
||||
internal static bool ShouldAttachGuestDepth(
|
||||
GuestDepthTarget? target,
|
||||
GuestDepthState state) =>
|
||||
target is not null &&
|
||||
(state.TestEnable || state.WriteEnable || state.ClearEnable);
|
||||
|
||||
private readonly record struct Presentation(
|
||||
byte[]? Pixels,
|
||||
uint Width,
|
||||
@@ -2775,11 +2769,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
private void LoadDebugUtilsCommands()
|
||||
{
|
||||
if (!_vulkanDebugUtilsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var setObjectName = _vk.GetDeviceProcAddr(_device, "vkSetDebugUtilsObjectNameEXT");
|
||||
var beginLabel = _vk.GetDeviceProcAddr(_device, "vkCmdBeginDebugUtilsLabelEXT");
|
||||
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} " +
|
||||
$"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()
|
||||
{
|
||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
||||
byte* validationLayerName = null;
|
||||
|
||||
try
|
||||
@@ -2949,8 +2900,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
enabledExtensions[index] = extensions[index];
|
||||
}
|
||||
|
||||
if (_vulkanDebugUtilsEnabled &&
|
||||
IsInstanceExtensionAvailable(DebugUtilsExtensionName))
|
||||
if (IsInstanceExtensionAvailable(DebugUtilsExtensionName))
|
||||
{
|
||||
debugUtilsExtension = (byte*)SilkMarshal.StringToPtr(DebugUtilsExtensionName);
|
||||
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
||||
@@ -2965,12 +2915,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
instanceCreateFlags |= InstanceCreateFlags.EnumeratePortabilityBitKhr;
|
||||
}
|
||||
|
||||
if (_vulkanValidationEnabled &&
|
||||
IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
||||
if (enableValidation && IsInstanceLayerAvailable("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?).");
|
||||
}
|
||||
@@ -4450,14 +4399,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
|
||||
1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
|
||||
$"waited_ms={waitedMs:F3}");
|
||||
}
|
||||
TraceVulkanShader(
|
||||
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
|
||||
$"waited_ms={waitedMs:F3}");
|
||||
}
|
||||
|
||||
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
|
||||
@@ -4465,13 +4411,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||
work.Action();
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
||||
}
|
||||
TraceVulkanShader(
|
||||
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
|
||||
$"submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
|
||||
}
|
||||
|
||||
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
||||
@@ -4943,13 +4886,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
IReadOnlyList<GuestImageResource>? renderTargets = null,
|
||||
ulong shaderAddress = 0)
|
||||
{
|
||||
if (!_traceGuestImagesEnabled &&
|
||||
!_traceGuestImageAddressFilterEnabled &&
|
||||
GuestImageTraceInterval() is null)
|
||||
{
|
||||
return Array.Empty<GuestImageResource>();
|
||||
}
|
||||
|
||||
var candidates = new HashSet<GuestImageResource>();
|
||||
foreach (var renderTarget in renderTargets ?? [])
|
||||
{
|
||||
@@ -5192,35 +5128,47 @@ internal static unsafe class VulkanVideoPresenter
|
||||
bool hasDepthAttachment = false,
|
||||
GuestDepthResource? feedbackDepth = null)
|
||||
{
|
||||
var isTitleDraw = IsTitleDraw(draw.VertexBuffers);
|
||||
var forceFullscreenVertex = _forceFullscreenPipeline ||
|
||||
_forceFullscreenVertex ||
|
||||
isTitleDraw && _forceTitleFullscreenVertex ||
|
||||
AnyTargetAddressMatches(
|
||||
feedbackTargets,
|
||||
"SHARPEMU_FORCE_FULLSCREEN_VERTEX_TARGETS");
|
||||
var forceRasterState = _forceFullscreenPipeline ||
|
||||
_forceDefaultRasterState ||
|
||||
isTitleDraw && _forceTitleDefaultRasterState ||
|
||||
AnyTargetAddressMatches(
|
||||
feedbackTargets,
|
||||
"SHARPEMU_FORCE_DEFAULT_RASTER_STATE_TARGETS");
|
||||
var isTitleDraw = 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);
|
||||
var forceFullscreenPipeline = Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_FULLSCREEN_PIPELINE") == "1";
|
||||
var forceFullscreenVertex = forceFullscreenPipeline ||
|
||||
Environment.GetEnvironmentVariable(
|
||||
"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 =
|
||||
_forceTitleSolidFragment &&
|
||||
Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT") == "1" &&
|
||||
isTitleDraw;
|
||||
var forceSolidFragment = forceTitleSolidFragment ||
|
||||
_forceFullscreenPipeline ||
|
||||
_forceSolidFragment ||
|
||||
AnyTargetAddressMatches(
|
||||
feedbackTargets,
|
||||
"SHARPEMU_FORCE_SOLID_FRAGMENT_TARGETS");
|
||||
var attributeFragmentLocation =
|
||||
_forceAttributeFragmentLocation.GetValueOrDefault();
|
||||
var forceAttributeFragment =
|
||||
_forceAttributeFragmentLocation.HasValue &&
|
||||
AnyTargetAddressMatches(
|
||||
feedbackTargets,
|
||||
"SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT_TARGETS");
|
||||
var forceSolidFragment = forceTitleSolidFragment || forceFullscreenPipeline ||
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_SOLID_FRAGMENT") == "1" ||
|
||||
feedbackTargets?.Any(target => AddressListContains(
|
||||
"SHARPEMU_FORCE_SOLID_FRAGMENT_TARGETS",
|
||||
target.Address)) == true;
|
||||
var forceAttributeFragment = uint.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT"),
|
||||
out var attributeFragmentLocation) &&
|
||||
feedbackTargets?.Any(target => AddressListContains(
|
||||
"SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT_TARGETS",
|
||||
target.Address)) == true;
|
||||
var vertexSpirv = forceFullscreenVertex
|
||||
? SpirvFixedShaders.CreateFullscreenVertex(0)
|
||||
: draw.VertexSpirv;
|
||||
@@ -5229,9 +5177,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
: forceAttributeFragment
|
||||
? SpirvFixedShaders.CreateAttributeFragment(attributeFragmentLocation)
|
||||
: 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)
|
||||
{
|
||||
@@ -5281,18 +5231,21 @@ internal static unsafe class VulkanVideoPresenter
|
||||
resources.Raster = GuestRasterState.Default;
|
||||
resources.Depth = GuestDepthState.Default;
|
||||
}
|
||||
if (isTitleDraw && _forceTitleDefaultBlend)
|
||||
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_TITLE_DEFAULT_BLEND") == "1")
|
||||
{
|
||||
resources.Blends = Enumerable.Repeat(
|
||||
GuestBlendState.Default,
|
||||
renderTargetFormats.Count).ToArray();
|
||||
}
|
||||
if (isTitleDraw && _forceTitleDefaultViewportScissor)
|
||||
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_TITLE_DEFAULT_VIEWPORT_SCISSOR") == "1")
|
||||
{
|
||||
resources.Scissor = null;
|
||||
resources.Viewport = null;
|
||||
}
|
||||
if (isTitleDraw && _forceTitleDisableCull)
|
||||
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_TITLE_DISABLE_CULL") == "1")
|
||||
{
|
||||
resources.Raster = resources.Raster with
|
||||
{
|
||||
@@ -5300,11 +5253,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CullBack = false,
|
||||
};
|
||||
}
|
||||
if (isTitleDraw && _forceTitleDisableDepth)
|
||||
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_TITLE_DISABLE_DEPTH") == "1")
|
||||
{
|
||||
resources.Depth = GuestDepthState.Default;
|
||||
}
|
||||
if (isTitleDraw && _traceTitleState)
|
||||
if (isTitleDraw && Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_TRACE_TITLE_STATE") == "1")
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.title_state " +
|
||||
@@ -7568,43 +7523,21 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
||||
var endAddress = checked(guestBuffer.BaseAddress + size);
|
||||
GuestBufferAllocation? allocation = null;
|
||||
var allocationPriority = -1;
|
||||
// This runs for every bound global buffer. Preserve the previous
|
||||
// stable queue preference without allocating LINQ sort state.
|
||||
foreach (var candidate in _guestBufferAllocations)
|
||||
{
|
||||
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
|
||||
candidate.BaseAddress + candidate.Size < endAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidatePriority = string.Equals(
|
||||
var allocation = _guestBufferAllocations
|
||||
.Where(candidate =>
|
||||
candidate.BaseAddress <= guestBuffer.BaseAddress &&
|
||||
candidate.BaseAddress + candidate.Size >= endAddress)
|
||||
.OrderByDescending(candidate =>
|
||||
string.Equals(
|
||||
candidate.QueueName,
|
||||
_activeGuestQueue.Name,
|
||||
StringComparison.Ordinal)
|
||||
? 2
|
||||
: string.Equals(
|
||||
StringComparison.Ordinal))
|
||||
.ThenByDescending(candidate =>
|
||||
string.Equals(
|
||||
candidate.QueueName,
|
||||
SharedReadOnlyGuestBufferQueue,
|
||||
StringComparison.Ordinal)
|
||||
? 1
|
||||
: 0;
|
||||
if (candidatePriority > allocationPriority)
|
||||
{
|
||||
allocation = candidate;
|
||||
allocationPriority = candidatePriority;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocation is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"no Vulkan guest buffer allocation covers " +
|
||||
$"0x{guestBuffer.BaseAddress:X16}-0x{endAddress:X16}");
|
||||
}
|
||||
|
||||
StringComparison.Ordinal))
|
||||
.First();
|
||||
var guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress;
|
||||
var descriptorOffset = guestOffset &
|
||||
~(GuestStorageBufferOffsetAlignment - 1);
|
||||
@@ -7653,15 +7586,16 @@ internal static unsafe class VulkanVideoPresenter
|
||||
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
|
||||
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
|
||||
}
|
||||
var mapped = new Span<byte>(
|
||||
(void*)(allocation.Mapped + checked((nint)guestOffset)),
|
||||
guestBuffer.Length);
|
||||
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, mapped) != true)
|
||||
var live = new byte[guestBuffer.Length];
|
||||
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, live) == 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() &&
|
||||
@@ -7671,6 +7605,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
||||
$"bytes={guestBuffer.Length}");
|
||||
}
|
||||
if (_setDebugUtilsObjectName is not null)
|
||||
{
|
||||
SetDebugName(
|
||||
ObjectType.Buffer,
|
||||
allocation.Buffer.Handle,
|
||||
$"SharpEmu global 0x{guestBuffer.BaseAddress:X16} {guestBuffer.Length}b");
|
||||
}
|
||||
|
||||
if (guestBuffer.Pooled)
|
||||
{
|
||||
GuestDataPool.Return(guestBuffer.Data);
|
||||
@@ -7907,7 +7849,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
ReadOnlySpan<byte> source = guestBuffer.Data.AsSpan(0, guestBuffer.Length);
|
||||
byte[]? forcedVertexColors = null;
|
||||
if (_forceTitleVertexColorWhite &&
|
||||
if (Environment.GetEnvironmentVariable("SHARPEMU_FORCE_TITLE_VERTEX_COLOR_WHITE") == "1" &&
|
||||
guestBuffer.Location == 0 &&
|
||||
guestBuffer.ComponentCount == 4 &&
|
||||
guestBuffer.DataFormat == 10 &&
|
||||
@@ -9433,67 +9375,37 @@ internal static unsafe class VulkanVideoPresenter
|
||||
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 &&
|
||||
work.Draw.RenderState.Blends[index].Enable)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment.");
|
||||
ReturnPooledGuestData(work.Draw);
|
||||
return;
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
"[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++)
|
||||
{
|
||||
if (work.Draw.RenderState.Blends[index] !=
|
||||
work.Draw.RenderState.Blends[0])
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
|
||||
ReturnPooledGuestData(work.Draw);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
|
||||
ReturnPooledGuestData(work.Draw);
|
||||
return;
|
||||
}
|
||||
|
||||
var formats = new Format[targetFormats.Length];
|
||||
for (var index = 0; index < targetFormats.Length; index++)
|
||||
{
|
||||
formats[index] = targetFormats[index].Format;
|
||||
}
|
||||
var formats = targetFormats.Select(target => target.Format).ToArray();
|
||||
|
||||
var hasStorageFeedback = false;
|
||||
foreach (var texture in work.Draw.Textures)
|
||||
{
|
||||
if (!texture.IsStorage || texture.Address == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var target in work.Targets)
|
||||
{
|
||||
if (target.Address != 0 && target.Address == texture.Address)
|
||||
{
|
||||
hasStorageFeedback = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStorageFeedback)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStorageFeedback)
|
||||
var targetAddresses = work.Targets
|
||||
.Where(target => target.Address != 0)
|
||||
.Select(target => target.Address)
|
||||
.ToHashSet();
|
||||
if (work.Draw.Textures.Any(texture =>
|
||||
texture.IsStorage && targetAddresses.Contains(texture.Address)))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[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");
|
||||
ReturnPooledGuestData(work.Draw);
|
||||
return;
|
||||
@@ -9528,7 +9440,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
|
||||
var draw = work.Draw;
|
||||
var clearDepthForDraw = draw.RenderState.Depth.ClearEnable;
|
||||
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
|
||||
{
|
||||
draw = draw with
|
||||
@@ -9541,10 +9452,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
GuestDepthResource? depth = null;
|
||||
DepthFramebufferResource? depthFramebuffer = null;
|
||||
if (ShouldAttachGuestDepth(
|
||||
work.DepthTarget,
|
||||
draw.RenderState.Depth) &&
|
||||
work.DepthTarget is { } depthTarget)
|
||||
if (work.DepthTarget is { } depthTarget &&
|
||||
(draw.RenderState.Depth.TestEnable ||
|
||||
draw.RenderState.Depth.WriteEnable))
|
||||
{
|
||||
var effectiveDepthTarget = depthTarget;
|
||||
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
|
||||
@@ -9591,45 +9501,21 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
depth = GetOrCreateGuestDepth(effectiveDepthTarget);
|
||||
PrepareFirstUseDepth(depth, draw.RenderState.Depth);
|
||||
if (clearDepthForDraw)
|
||||
{
|
||||
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
|
||||
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
|
||||
}
|
||||
if (targets.Length == 1)
|
||||
{
|
||||
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
|
||||
}
|
||||
}
|
||||
|
||||
if (clearDepthForDraw)
|
||||
{
|
||||
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
|
||||
// clear operation. The draw still produces color, but its
|
||||
// interpolated vertex Z is not the guest clear value.
|
||||
draw = draw with
|
||||
{
|
||||
RenderState = draw.RenderState with
|
||||
{
|
||||
Depth = draw.RenderState.Depth with
|
||||
{
|
||||
TestEnable = false,
|
||||
WriteEnable = false,
|
||||
ClearEnable = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
var renderPass = depthFramebuffer is null
|
||||
? firstTarget.Initialized
|
||||
? firstTarget.RenderPass
|
||||
: firstTarget.InitialRenderPass
|
||||
: firstTarget.Initialized
|
||||
? depth!.Initialized && !clearDepthForDraw
|
||||
? depth!.Initialized
|
||||
? depthFramebuffer.LoadRenderPass
|
||||
: depthFramebuffer.DepthClearRenderPass
|
||||
: depth!.Initialized && !clearDepthForDraw
|
||||
: depth!.Initialized
|
||||
? depthFramebuffer.ColorClearRenderPass
|
||||
: depthFramebuffer.BothClearRenderPass;
|
||||
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
|
||||
@@ -9645,7 +9531,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
targets.Select(target =>
|
||||
target.Initialized || target.InitialUploadPending).ToArray(),
|
||||
depth,
|
||||
depth?.Initialized == true && !clearDepthForDraw);
|
||||
depth?.Initialized == true);
|
||||
transientRenderPass = renderPass;
|
||||
transientFramebuffer = framebuffer;
|
||||
}
|
||||
@@ -9833,12 +9719,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (depth is not null)
|
||||
{
|
||||
depth.Initialized = true;
|
||||
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||
if (clearDepthForDraw)
|
||||
{
|
||||
depth.InitializationSource = "guest-depth-clear";
|
||||
}
|
||||
else if (draw.RenderState.Depth.WriteEnable)
|
||||
if (draw.RenderState.Depth.WriteEnable)
|
||||
{
|
||||
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;
|
||||
if (_tracePixelSpirvBytes > 0 &&
|
||||
_tracePixelSpirvBytes == work.Draw.PixelSpirv.Length)
|
||||
if (int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PIXEL_SPIRV_BYTES"),
|
||||
out var tracePixelSpirvBytes) &&
|
||||
tracePixelSpirvBytes == work.Draw.PixelSpirv.Length)
|
||||
{
|
||||
var pixelWriteCount = _pixelSpirvWriteCounts.TryGetValue(
|
||||
_tracePixelSpirvBytes,
|
||||
tracePixelSpirvBytes,
|
||||
out var previousPixelWriteCount)
|
||||
? previousPixelWriteCount + 1
|
||||
: 1;
|
||||
_pixelSpirvWriteCounts[_tracePixelSpirvBytes] = pixelWriteCount;
|
||||
_pixelSpirvWriteCounts[tracePixelSpirvBytes] = pixelWriteCount;
|
||||
var tracePixelSpirvOccurrence = 1;
|
||||
_ = int.TryParse(
|
||||
Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_TRACE_PIXEL_SPIRV_OCCURRENCE"),
|
||||
out tracePixelSpirvOccurrence);
|
||||
tracePixelSpirv =
|
||||
pixelWriteCount == _tracePixelSpirvOccurrence;
|
||||
pixelWriteCount == Math.Max(tracePixelSpirvOccurrence, 1);
|
||||
}
|
||||
var traceTitleDraw =
|
||||
!_tracedTitleDraw &&
|
||||
_traceTitleDrawEnabled &&
|
||||
IsTitleDraw(work.Draw.VertexBuffers);
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_DRAW") == "1" &&
|
||||
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;
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var traceAddressWrite =
|
||||
ShouldTraceGuestImageWriteForDiagnostics(target.Address);
|
||||
var traceSmallWrites = _traceGuestWritesMode == "small" &&
|
||||
var traceSmallWrites = guestWritesMode == "small" &&
|
||||
target.Width <= 512 && target.Height <= 256;
|
||||
var traceLargeWrites =
|
||||
(_traceGuestWritesMode == "large" ||
|
||||
_traceLargeGuestWriteOrdinal != 0) &&
|
||||
(guestWritesMode == "large" || traceLargeWriteOrdinal != 0) &&
|
||||
target.Width >= 2560 && target.Height >= 1440;
|
||||
if (traceAddressWrite || traceSmallWrites ||
|
||||
traceLargeWrites || tracePixelSpirv || traceTitleDraw)
|
||||
@@ -9905,10 +9816,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_tracedGuestWriteCounts[target.Address] = writeCount;
|
||||
var shouldTraceWrite = tracePixelSpirv || traceTitleDraw
|
||||
? true
|
||||
: traceAddressWrite && _traceGuestWriteOrdinal > 0
|
||||
? writeCount == _traceGuestWriteOrdinal
|
||||
: _traceLargeGuestWriteOrdinal != 0
|
||||
? writeCount == _traceLargeGuestWriteOrdinal
|
||||
: traceAddressWrite && traceAddressWriteOrdinal > 0
|
||||
? writeCount == traceAddressWriteOrdinal
|
||||
: traceLargeWriteOrdinal != 0
|
||||
? writeCount == traceLargeWriteOrdinal
|
||||
: writeCount <=
|
||||
(traceLargeWrites ? 2 : traceSmallWrites ? 48 : 3);
|
||||
if (traceAddressWrite || shouldTraceWrite)
|
||||
@@ -9942,13 +9853,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.offscreen_draw mrt={targets.Length} " +
|
||||
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
||||
$"textures={work.Draw.Textures.Count}");
|
||||
}
|
||||
TraceVulkanShader(
|
||||
$"vk.offscreen_draw mrt={targets.Length} " +
|
||||
$"size={firstTarget.Width}x{firstTarget.Height} " +
|
||||
$"textures={work.Draw.Textures.Count}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -12882,20 +12790,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// Diagnostics toggles are read once: these run per draw / per cached
|
||||
// texture hit, and env lookups plus string parsing are far too
|
||||
// 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 =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||
private static readonly bool _traceGuestImagesEnabled =
|
||||
@@ -12959,80 +12853,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
!string.IsNullOrWhiteSpace(
|
||||
Environment.GetEnvironmentVariable(
|
||||
"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<
|
||||
string,
|
||||
(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() =>
|
||||
_traceGuestImagesEnabled;
|
||||
|
||||
@@ -13230,7 +13054,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// MoltenVK this starves the render thread and makes the guest fall
|
||||
// behind its own flip queue. Vulkan clips a normal fullscreen draw
|
||||
// efficiently; keep tiling only as an explicit driver diagnostic.
|
||||
var maxPixelsPerDraw = _chunkedDrawsEnabled
|
||||
var maxPixelsPerDraw = Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_ENABLE_CHUNKED_DRAWS") == "1"
|
||||
? 512u * 512u
|
||||
: uint.MaxValue;
|
||||
var rowsPerDraw = Math.Max(
|
||||
|
||||
@@ -325,31 +325,9 @@ public sealed record Gen5ShaderProgram(
|
||||
ulong Address,
|
||||
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 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 =>
|
||||
Instructions
|
||||
.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.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