Compare commits

..

2 Commits

Author SHA1 Message Date
ParantezTech 3fdf76308d [readme] update readme images and change version to 0.0.2 2026-07-17 02:43:20 +03:00
ParantezTech 7b46921546 [readme] update showcase images 2026-07-16 23:26:49 +03:00
34 changed files with 371 additions and 2503 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 69 KiB

+23 -17
View File
@@ -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
+1 -1
View File
@@ -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>
+11 -10
View File
@@ -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 |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
| Void Terrarium | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
## 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 |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
| Void Terrarium | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
> [!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

-408
View File
@@ -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)
+1 -2
View File
@@ -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)
-2
View File
@@ -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!",
-2
View File
@@ -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!"
+4 -28
View File
@@ -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" />
-109
View File
@@ -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();
-2
View File
@@ -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
View File
@@ -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;
}
+2 -13
View File
@@ -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);
}
-79
View File
@@ -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",
+2 -3
View File
@@ -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
-13
View File
@@ -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);
}
}
}
+2 -17
View File
@@ -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,
+171 -346
View File
@@ -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);
}
}