mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eb021c7d1 | |||
| 4ac29f340a |
@@ -1,120 +0,0 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Posts (and keeps updated) a single PR comment linking the per-platform build
|
||||
# artifacts once "Build and Release" finishes.
|
||||
#
|
||||
# This is a workflow_run workflow on purpose: PRs from forks run "Build and
|
||||
# Release" with a read-only GITHUB_TOKEN and cannot comment. workflow_run runs
|
||||
# afterwards in the base-repo context with a read-write token and does not check
|
||||
# out untrusted fork code, so it can comment safely. Because of that, GitHub only
|
||||
# triggers it from the copy on the default branch — it does nothing until merged
|
||||
# to main.
|
||||
name: PR Build Links
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Release"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Post artifact links
|
||||
runs-on: ubuntu-latest
|
||||
# Only for successful PR builds — artifacts exist only when the build passed.
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Post or update the artifact-links comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = context.payload.workflow_run;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Resolve the PR. Same-repo PRs are in workflow_run.pull_requests, but
|
||||
// fork PRs leave it empty AND their head commit is not on a base-repo
|
||||
// branch, so listPullRequestsAssociatedWithCommit misses them too. Match
|
||||
// the open PR by its head "owner:branch" instead, which works for forks.
|
||||
let prNumber;
|
||||
if (run.pull_requests && run.pull_requests.length > 0) {
|
||||
prNumber = run.pull_requests[0].number;
|
||||
} else {
|
||||
let candidates = [];
|
||||
const headOwner = run.head_repository && run.head_repository.owner
|
||||
? run.head_repository.owner.login
|
||||
: null;
|
||||
if (headOwner && run.head_branch) {
|
||||
const byHead = await github.rest.pulls.list({
|
||||
owner, repo, state: 'open',
|
||||
head: `${headOwner}:${run.head_branch}`, per_page: 10,
|
||||
});
|
||||
candidates = byHead.data;
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
const byCommit = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner, repo, commit_sha: run.head_sha,
|
||||
});
|
||||
candidates = byCommit.data.filter(pr => pr.state === 'open');
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
core.info('No open PR for this build; nothing to comment.');
|
||||
return;
|
||||
}
|
||||
prNumber = candidates[0].number;
|
||||
}
|
||||
|
||||
// Collect the per-platform artifacts the build produced.
|
||||
const artifacts = await github.paginate(
|
||||
github.rest.actions.listWorkflowRunArtifacts,
|
||||
{ owner, repo, run_id: run.id, per_page: 100 },
|
||||
);
|
||||
const platforms = [
|
||||
{ key: 'win-x64', label: 'Windows (win-x64)' },
|
||||
{ key: 'linux-x64', label: 'Linux (linux-x64)' },
|
||||
{ key: 'osx-x64', label: 'macOS (osx-x64)' },
|
||||
];
|
||||
const rows = [];
|
||||
for (const platform of platforms) {
|
||||
const artifact = artifacts.find(a => a.name.includes(platform.key));
|
||||
if (!artifact) {
|
||||
continue;
|
||||
}
|
||||
const url = `https://github.com/${owner}/${repo}/actions/runs/${run.id}/artifacts/${artifact.id}`;
|
||||
rows.push(`| ${platform.label} | [\`${artifact.name}\`](${url}) |`);
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
core.info('No platform artifacts on this run; nothing to comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = '<!-- pr-build-links -->';
|
||||
const body = [
|
||||
marker,
|
||||
`### 📦 Build artifacts — \`${run.head_sha.substring(0, 7)}\``,
|
||||
'',
|
||||
'| Platform | Download |',
|
||||
'| --- | --- |',
|
||||
...rows,
|
||||
'',
|
||||
`From [build run #${run.run_number}](${run.html_url}). ` +
|
||||
'Downloads require a GitHub login and expire after 90 days.',
|
||||
].join('\n');
|
||||
|
||||
// Upsert one comment so repeated builds refresh it in place.
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: prNumber, per_page: 100,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
|
||||
}
|
||||
@@ -94,20 +94,14 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
Directory.Packages.props
|
||||
Directory.Build.props
|
||||
src/**/packages.lock.json
|
||||
|
||||
- name: Restore solution
|
||||
run: dotnet restore SharpEmu.slnx
|
||||
run: dotnet restore SharpEmu.slnx --locked-mode
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||
|
||||
# Runs every test project in the solution; a test failure fails the build, as
|
||||
# does an aerolib.bin generation failure in the build step above (the MSBuild
|
||||
# task logs an error and returns false).
|
||||
- name: Run tests
|
||||
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
|
||||
|
||||
- name: Validate synthetic shaders
|
||||
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
|
||||
|
||||
@@ -118,7 +112,7 @@ jobs:
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||
|
||||
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
|
||||
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64-${{ needs.init.outputs.short-sha }}.zip"
|
||||
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
|
||||
if (Test-Path $archivePath) {
|
||||
Remove-Item $archivePath -Force
|
||||
@@ -130,7 +124,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
|
||||
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64-${{ needs.init.outputs.short-sha }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
build-posix:
|
||||
@@ -163,19 +157,14 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
Directory.Packages.props
|
||||
Directory.Build.props
|
||||
src/**/packages.lock.json
|
||||
|
||||
- name: Restore solution
|
||||
run: dotnet restore SharpEmu.slnx
|
||||
run: dotnet restore SharpEmu.slnx --locked-mode
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build SharpEmu.slnx -c Release --no-restore
|
||||
|
||||
# Also runs on Linux/macOS: the aerolib.bin MSBuild task does platform-sensitive
|
||||
# path handling, so a cross-platform generation regression surfaces here.
|
||||
- name: Run tests
|
||||
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
|
||||
|
||||
- name: Publish ${{ matrix.rid }} CLI
|
||||
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r ${{ matrix.rid }} --self-contained true --no-restore -p:PublishDir="$PUBLISH_DIR"
|
||||
|
||||
@@ -187,14 +176,14 @@ jobs:
|
||||
run: |
|
||||
mkdir -p "$RELEASE_DIR"
|
||||
# tar keeps the executable bit, which zip would drop.
|
||||
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
|
||||
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz" \
|
||||
-C "$PUBLISH_DIR" .
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
|
||||
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
|
||||
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
@@ -239,18 +228,3 @@ jobs:
|
||||
--notes "${notes}" \
|
||||
--target "${GITHUB_SHA}"
|
||||
fi
|
||||
|
||||
keep=()
|
||||
for asset_path in "${assets[@]}"; do
|
||||
keep+=("$(basename "${asset_path}")")
|
||||
done
|
||||
mapfile -t release_assets < <(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort)
|
||||
for asset in "${release_assets[@]}"; do
|
||||
case "${asset}" in
|
||||
sharpemu-${VERSION}-*.zip|sharpemu-${VERSION}-*.tar.gz)
|
||||
if ! printf '%s\n' "${keep[@]}" | grep -Fxq "${asset}"; then
|
||||
gh release delete-asset "${RELEASE_TAG}" "${asset}" --yes
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -9,6 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<SharpEmuVersion>0.0.1</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
|
||||
@@ -12,9 +12,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Silk.NET.Input" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
|
||||
@@ -91,18 +91,18 @@ release includes the MoltenVK Vulkan implementation.
|
||||
## Games Tested
|
||||
|
||||
* **Demon's Souls Remake**
|
||||
* [Demon's Souls [PPSA01341]](https://github.com/sharpemu/sharpemu/issues/2)
|
||||
* [Demon's Souls [PPSA01341]](https://github.com/par274/sharpemu/issues/2)
|
||||
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this.
|
||||

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

|
||||
|
||||
@@ -115,7 +115,7 @@ release includes the MoltenVK Vulkan implementation.
|
||||
## Build
|
||||
|
||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||
2. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
|
||||
2. Clone the repository: `git clone https://github.com/par274/sharpemu.git`
|
||||
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
||||
4. Build the project: `dotnet build` or `dotnet publish`
|
||||
5. Build artifacts will be located in the `artifacts` directory.
|
||||
@@ -141,7 +141,7 @@ Provided valuable references for filesystem handling and low-level C# implementa
|
||||
|
||||
# License
|
||||
|
||||
- [**GPL-2.0 license**](https://github.com/sharpemu/sharpemu/blob/main/LICENSE)
|
||||
- [**GPL-2.0 license**](https://github.com/par274/sharpemu/blob/main/LICENSE)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ path = [
|
||||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"_logs/**",
|
||||
".github/images/**",
|
||||
|
||||
@@ -11,12 +11,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
|
||||
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Offline check: SysAbiExport ExportName must hash to its Nid (name2nid).
|
||||
|
||||
NIDs absent from aerolib.bin are skipped (unknown/unresolved symbols).
|
||||
Known historic mislabels may be allowlisted with a one-line reason.
|
||||
|
||||
Run from the repository root:
|
||||
python scripts/check_sysabi_aerolib.py
|
||||
python scripts/check_sysabi_aerolib.py --strict
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
|
||||
SRC_ROOT = Path("src")
|
||||
AEROLIB_BIN = Path("src/SharpEmu.HLE/Aerolib/aerolib.bin")
|
||||
SYSABI_EXPORT_RE = re.compile(r"\[SysAbiExport\((.*?)\)\]", re.DOTALL)
|
||||
NID_RE = re.compile(r'Nid\s*=\s*"([^"]+)"')
|
||||
EXPORT_NAME_RE = re.compile(r'ExportName\s*=\s*"([^"]+)"')
|
||||
|
||||
# NID -> reason. Keep minimal; fix ExportName when safe instead of growing this list.
|
||||
ALLOWLISTED_NIDS: dict[str, str] = {
|
||||
"KMcEa+rHsIo": "Historic kernel MapMemory stub bound to sceAvPlayerAddSource NID; API rewrite deferred.",
|
||||
"WV1GwM32NgY": "Historic WebApi2 init alias for PushEventCreateHandle NID; ABI rewrite deferred.",
|
||||
}
|
||||
|
||||
|
||||
def name2nid(name: str) -> str:
|
||||
symbol = hashlib.sha1(name.encode() + uhx("518D64A635DED8C1E6B039B1C3E55230")).digest()
|
||||
id_val = struct.unpack("<Q", symbol[:8])[0]
|
||||
nid = base64enc(uhx("%016x" % id_val), b"+-").rstrip(b"=")
|
||||
return nid.decode("utf-8")
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
cwd = Path.cwd()
|
||||
if (cwd / SRC_ROOT).is_dir() and (cwd / "scripts").is_dir():
|
||||
return cwd
|
||||
script_root = Path(__file__).resolve().parent.parent
|
||||
if (script_root / SRC_ROOT).is_dir():
|
||||
return script_root
|
||||
raise SystemExit("Run from the repository root (src/ and scripts/ expected).")
|
||||
|
||||
|
||||
def load_aerolib_nids(aerolib_path: Path) -> set[str]:
|
||||
data = aerolib_path.read_bytes()
|
||||
if len(data) < 4:
|
||||
raise SystemExit(f"Aerolib binary too small: {aerolib_path}")
|
||||
|
||||
count = struct.unpack_from("<I", data, 0)[0]
|
||||
offset = 4
|
||||
nids: set[str] = set()
|
||||
for _ in range(count):
|
||||
if offset >= len(data):
|
||||
raise SystemExit(f"Truncated aerolib.bin while reading NIDs: {aerolib_path}")
|
||||
nid_len = data[offset]
|
||||
offset += 1
|
||||
nid = data[offset : offset + nid_len].decode("utf-8")
|
||||
offset += nid_len
|
||||
if offset + 2 > len(data):
|
||||
raise SystemExit(f"Truncated aerolib.bin name length: {aerolib_path}")
|
||||
name_len = struct.unpack_from("<H", data, offset)[0]
|
||||
offset += 2 + name_len
|
||||
nids.add(nid)
|
||||
return nids
|
||||
|
||||
|
||||
def iter_sysabi_exports(cs_path: Path, text: str):
|
||||
for match in SYSABI_EXPORT_RE.finditer(text):
|
||||
block = match.group(1)
|
||||
nid_match = NID_RE.search(block)
|
||||
export_match = EXPORT_NAME_RE.search(block)
|
||||
if nid_match is None or export_match is None:
|
||||
continue
|
||||
|
||||
nid = nid_match.group(1)
|
||||
export_name = export_match.group(1)
|
||||
nid_attr = f'Nid = "{nid}"'
|
||||
abs_pos = text.find(nid_attr, match.start(), match.end())
|
||||
if abs_pos < 0:
|
||||
abs_pos = match.start()
|
||||
line = text.count("\n", 0, abs_pos) + 1
|
||||
yield cs_path, line, nid, export_name
|
||||
|
||||
|
||||
def scan(src_root: Path, catalog_nids: set[str]):
|
||||
checked = 0
|
||||
mismatches = []
|
||||
skipped_no_catalog = 0
|
||||
allowlisted = 0
|
||||
|
||||
for cs_path in sorted(src_root.rglob("*.cs")):
|
||||
text = cs_path.read_text(encoding="utf-8")
|
||||
for path, line, nid, export_name in iter_sysabi_exports(cs_path, text):
|
||||
checked += 1
|
||||
computed = name2nid(export_name)
|
||||
if computed == nid:
|
||||
continue
|
||||
|
||||
if nid not in catalog_nids:
|
||||
skipped_no_catalog += 1
|
||||
continue
|
||||
|
||||
if nid in ALLOWLISTED_NIDS:
|
||||
allowlisted += 1
|
||||
continue
|
||||
|
||||
mismatches.append((path, line, nid, export_name, computed))
|
||||
|
||||
return checked, mismatches, skipped_no_catalog, allowlisted
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check that SysAbiExport ExportName values hash to their Nid via name2nid."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Exit 1 when any non-skipped/non-allowlisted ExportName does not hash to its Nid.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Print only the summary line.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
aerolib_path = repo_root / AEROLIB_BIN
|
||||
if not aerolib_path.is_file():
|
||||
raise SystemExit(f"Missing Aerolib catalog: {aerolib_path.as_posix()}")
|
||||
|
||||
catalog_nids = load_aerolib_nids(aerolib_path)
|
||||
checked, mismatches, skipped_no_catalog, allowlisted = scan(
|
||||
repo_root / SRC_ROOT, catalog_nids
|
||||
)
|
||||
ok = checked - len(mismatches) - skipped_no_catalog - allowlisted
|
||||
|
||||
if not args.quiet:
|
||||
for path, line, nid, export_name, computed in mismatches:
|
||||
rel = path.relative_to(repo_root).as_posix()
|
||||
print(
|
||||
f"{rel}:{line}: NID={nid} ExportName={export_name!r} "
|
||||
f"computed={computed}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"checked={checked} ok={ok} fail={len(mismatches)} "
|
||||
f"skipped_no_catalog={skipped_no_catalog} allowlisted={allowlisted} "
|
||||
f"allowlist_size={len(ALLOWLISTED_NIDS)}"
|
||||
)
|
||||
|
||||
if args.strict and mismatches:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
|
||||
NAMES = 'scripts/ps5_names.txt'
|
||||
OUTPUT = 'src/SharpEmu.HLE/Aerolib/aerolib.bin'
|
||||
|
||||
def name2nid(name):
|
||||
symbol = hashlib.sha1(name.encode() + uhx('518D64A635DED8C1E6B039B1C3E55230')).digest()
|
||||
id_val = struct.unpack('<Q', symbol[:8])[0]
|
||||
nid = base64enc(uhx('%016x' % id_val), b'+-').rstrip(b'=')
|
||||
return nid.decode('utf-8')
|
||||
|
||||
def generate():
|
||||
names_path = Path(NAMES)
|
||||
output_path = Path(OUTPUT)
|
||||
|
||||
entries = []
|
||||
with open(names_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
name = line.strip()
|
||||
if name:
|
||||
nid = name2nid(name)
|
||||
entries.append((nid, name))
|
||||
|
||||
print(f"Found {len(entries)} entries")
|
||||
|
||||
data = bytearray()
|
||||
data.extend(struct.pack('<I', len(entries)))
|
||||
|
||||
for nid, name in entries:
|
||||
nid_bytes = nid.encode('utf-8')
|
||||
name_bytes = name.encode('utf-8')
|
||||
data.append(len(nid_bytes))
|
||||
data.extend(nid_bytes)
|
||||
data.extend(struct.pack('<H', len(name_bytes)))
|
||||
data.extend(name_bytes)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||
print(f"Total entries: {len(entries)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate()
|
||||
@@ -0,0 +1,572 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.3, )",
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Iced": "[1.21.0, )",
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"SharpEmu.Libs": "[1.0.0, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.gui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.3.18, )",
|
||||
"Avalonia.Desktop": "[11.3.18, )",
|
||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )",
|
||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Input": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Input": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Input.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,9 +84,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||
};
|
||||
var moduleManager = new ModuleManager();
|
||||
// The compile-time generated registry (SharpEmu.SourceGenerators) is the sole
|
||||
// registration source; content tests in SharpEmu.Libs.Tests pin its invariants.
|
||||
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||
moduleManager.Freeze();
|
||||
|
||||
// Resolve the host platform once at the composition root; on unsupported
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Iced": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Input": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Silk.NET.Input": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Input.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,9 @@
|
||||
"Options.General": "Opções Gerais",
|
||||
"Options.Env.Tab": "Ambiente",
|
||||
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||
"Options.Env.Desc": "Variáveis de ambiente passadas ao emulador durante a inicialização.",
|
||||
"Options.Env.Bthid.Desc": "Informa o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica verificando indefinidamente.\nNormalmente, deixe desativado. Alguns títulos travam quando a inicialização falha.",
|
||||
"Options.Env.LoopGuard.Desc": "Não força o encerramento de títulos que repetem a mesma chamada por tempo excessivo.\nExperimente esta opção se um jogo fechar sozinho durante o carregamento.",
|
||||
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
|
||||
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
|
||||
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
|
||||
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
||||
@@ -83,7 +83,7 @@
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Pesquisar...",
|
||||
"Console.AutoScroll": "Rolagem automática",
|
||||
"Console.Split": "Dividir",
|
||||
"Console.Split": "Recortar",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpar",
|
||||
"Console.WindowTitle": "Console do SharpEmu",
|
||||
@@ -134,13 +134,5 @@
|
||||
"Dialog.PsExecutables": "Executáveis de PS",
|
||||
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||
"Dialog.LogFiles": "Arquivos de log",
|
||||
|
||||
"Options.About" : "Sobre",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
|
||||
"About.GithubButton": "Contribua no GitHub!",
|
||||
"About.DiscordButton": "Entre no nosso Discord!"
|
||||
"Dialog.LogFiles": "Arquivos de log"
|
||||
}
|
||||
|
||||
@@ -26,15 +26,6 @@
|
||||
"Library.Loading": "Chargement de la bibliothèque…",
|
||||
|
||||
"Options.General": "Général",
|
||||
"Options.Env.Tab": "Environnement",
|
||||
"Options.Section.Environment": "VARIABLES D’ENVIRONNEMENT",
|
||||
"Options.Env.Desc": "Paramètres passés à l’émulateur comme variables d’environnement au lancement.",
|
||||
"Options.Env.Bthid.Desc": "Signaler le HID Bluetooth comme indisponible pour les titres dont le middleware volant/FFB interroge indéfiniment.\nLaissez désactivé normalement. Certains titres se bloquent si l’initialisation échoue.",
|
||||
"Options.Env.LoopGuard.Desc": "Ne pas forcer la fermeture des titres qui répètent le même appel trop longtemps.\nEssayez ceci quand un jeu se ferme tout seul pendant le chargement.",
|
||||
"Options.Env.VkValidation.Desc": "Activer les couches de validation Vulkan pour le débogage GPU.\nRalentit. Nécessite l’installation du Vulkan SDK.",
|
||||
"Options.Env.DumpSpirv.Desc": "Exporter les shaders AGC et leurs traductions SPIR-V dans le dossier shader-dumps.\nÀ utiliser pour signaler des bugs de shader ou de rendu.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Journaliser les allocations de mémoire directe et les échecs dans la console.\nÀ utiliser quand un jeu plante ou se ferme pendant le démarrage.",
|
||||
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
|
||||
"Options.Section.Emulation": "ÉMULATION",
|
||||
"Options.Section.Logging": "JOURNALISATION",
|
||||
"Options.Section.Launcher": "LANCEUR",
|
||||
@@ -134,13 +125,5 @@
|
||||
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||
"Dialog.LogFiles": "Fichiers journaux",
|
||||
|
||||
"Options.About": "À propos",
|
||||
"About.Github.Label": "GitHub",
|
||||
"About.Github.Desc": "Code source, issues et développement du projet.",
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
|
||||
"About.GithubButton": "Contribuer sur GitHub !",
|
||||
"About.DiscordButton": "Rejoindre notre Discord !"
|
||||
"Dialog.LogFiles": "Fichiers journaux"
|
||||
}
|
||||
|
||||
@@ -34,12 +34,6 @@ public partial class MainWindow : Window
|
||||
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
|
||||
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
|
||||
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
|
||||
private static readonly StringComparer FilePathComparer = OperatingSystem.IsWindows()
|
||||
? StringComparer.OrdinalIgnoreCase
|
||||
: StringComparer.Ordinal;
|
||||
private static readonly StringComparison FilePathComparison = OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
private readonly List<GameEntry> _allGames = new();
|
||||
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
||||
@@ -172,7 +166,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "https://github.com/sharpemu/sharpemu",
|
||||
FileName = "https://github.com/par274/sharpemu",
|
||||
UseShellExecute = true
|
||||
});
|
||||
};
|
||||
@@ -733,7 +727,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
if (!_settings.GameFolders.Contains(path, FilePathComparer))
|
||||
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_settings.GameFolders.Add(path);
|
||||
changed = true;
|
||||
@@ -743,7 +737,7 @@ public partial class MainWindow : Window
|
||||
// games beneath it that were removed from the library earlier.
|
||||
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
|
||||
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
|
||||
excluded.StartsWith(prefix, FilePathComparison)) > 0;
|
||||
excluded.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) > 0;
|
||||
|
||||
if (changed)
|
||||
{
|
||||
@@ -756,7 +750,7 @@ public partial class MainWindow : Window
|
||||
private async Task RescanLibraryAsync()
|
||||
{
|
||||
var folders = _settings.GameFolders.ToArray();
|
||||
var excluded = new HashSet<string>(_settings.ExcludedGames, FilePathComparer);
|
||||
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
||||
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
|
||||
EmptyState.IsVisible = false;
|
||||
LoadingState.IsVisible = true;
|
||||
@@ -874,7 +868,7 @@ public partial class MainWindow : Window
|
||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
|
||||
{
|
||||
var games = new List<GameEntry>();
|
||||
var seen = new HashSet<string>(FilePathComparer);
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var enumeration = new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = true,
|
||||
@@ -1138,13 +1132,13 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_settings.ExcludedGames.Contains(game.Path, FilePathComparer))
|
||||
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_settings.ExcludedGames.Add(game.Path);
|
||||
_settings.Save();
|
||||
}
|
||||
|
||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, FilePathComparison));
|
||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
|
||||
GameList.SelectedItem = null;
|
||||
RefreshVisibleGames();
|
||||
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
|
||||
@@ -1168,7 +1162,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
if (selectedPath is not null &&
|
||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, FilePathComparison))
|
||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
|
||||
is { } reselected)
|
||||
{
|
||||
GameList.SelectedItem = reselected;
|
||||
@@ -1493,7 +1487,7 @@ public partial class MainWindow : Window
|
||||
_isRunning = true;
|
||||
_runningGameName = displayName;
|
||||
_runningGameTitleId = _allGames
|
||||
.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?
|
||||
.FirstOrDefault(game => game.Path.Equals(ebootPath, StringComparison.OrdinalIgnoreCase))?
|
||||
.TitleId;
|
||||
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
StatusDot.Fill = SuccessLineBrush;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Avalonia": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a string parameter of a [SysAbiExport] handler as a guest null-terminated
|
||||
/// UTF-8 pointer. The generated thunk reads the string from the argument register's
|
||||
/// address (bounded by <see cref="MaxLength"/>) before invoking the handler, and
|
||||
/// returns ORBIS_GEN2_ERROR_MEMORY_FAULT to the guest when the read fails — including
|
||||
/// a null pointer, matching TryReadNullTerminatedUtf8's contract.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GuestCStringAttribute : Attribute
|
||||
{
|
||||
public GuestCStringAttribute(int maxLength) => MaxLength = maxLength;
|
||||
|
||||
/// <summary>Upper bound in bytes for the guest read, terminator included.</summary>
|
||||
public int MaxLength { get; }
|
||||
}
|
||||
@@ -271,26 +271,6 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
||||
|
||||
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
|
||||
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
|
||||
if (result != MAP_FAILED && (ulong)result != (ulong)address)
|
||||
{
|
||||
munmap(result, (nuint)alignedSize);
|
||||
result = MAP_FAILED;
|
||||
}
|
||||
|
||||
if (result == MAP_FAILED && OperatingSystem.IsMacOS())
|
||||
{
|
||||
// The hint-only attempt above didn't land at the requested
|
||||
// address. This is routinely the case for the PS5's fixed
|
||||
// 32 GiB image base under Rosetta 2 on Apple Silicon, where
|
||||
// the kernel never honors that hint. Retry with true
|
||||
// MAP_FIXED: this can clobber an untracked host mapping
|
||||
// (dyld, the runtime's JIT heap, Rosetta) if one already
|
||||
// sits exactly there, but without it guest images that
|
||||
// require this base never load at all on macOS.
|
||||
Trace($"exact mmap hint failed, retrying with MAP_FIXED: addr=0x{(ulong)address:X16}");
|
||||
result = mmap((nint)address, (nuint)alignedSize, posixProtect, flags | MAP_FIXED, -1, 0);
|
||||
}
|
||||
|
||||
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||
{
|
||||
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public interface IModuleManager
|
||||
{
|
||||
/// <summary>Registers pre-built exports (the compile-time generated registry).</summary>
|
||||
int RegisterExports(IReadOnlyList<ExportedFunction> exports);
|
||||
int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null);
|
||||
|
||||
void Freeze();
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ public sealed class ModuleManager : IModuleManager
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<Assembly> _warmupAssemblies = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
public int RegisterExports(IReadOnlyList<ExportedFunction> exports)
|
||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exports);
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
|
||||
lock (_registrationGate)
|
||||
{
|
||||
@@ -27,21 +27,48 @@ public sealed class ModuleManager : IModuleManager
|
||||
throw new InvalidOperationException("Module registration is frozen.");
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
foreach (var export in exports)
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
if (!_dispatchTable.TryAdd(export.Nid, export.Function))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{export.Nid}' ({export.Name}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
_exportTable[export.Nid] = export;
|
||||
_exportNameTable.TryAdd(export.Name, export);
|
||||
// The warm sweep in Freeze() covers every assembly that contributed a
|
||||
// handler (generated thunks resolve to their home assembly too).
|
||||
_warmupAssemblies.Add(export.Function.Method.Module.Assembly);
|
||||
registeredCount++;
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
|
||||
{
|
||||
var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);
|
||||
if (exportAttribute is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var exportInfo = ResolveExportInfo(exportAttribute, method, generation, symbolCatalog);
|
||||
if (exportInfo is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_exportTable[exportInfo.Value.Nid] = new ExportedFunction(
|
||||
exportInfo.Value.LibraryName,
|
||||
exportInfo.Value.Nid,
|
||||
exportInfo.Value.ExportName,
|
||||
exportInfo.Value.Target,
|
||||
(SysAbiFunction)handler);
|
||||
_exportNameTable.TryAdd(exportInfo.Value.ExportName, _exportTable[exportInfo.Value.Nid]);
|
||||
|
||||
registeredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return registeredCount;
|
||||
@@ -65,8 +92,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
Assembly[] assemblies;
|
||||
lock (_registrationGate)
|
||||
{
|
||||
assemblies = new Assembly[_warmupAssemblies.Count];
|
||||
_warmupAssemblies.CopyTo(assemblies);
|
||||
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||
}
|
||||
|
||||
assemblies = WithGuestReachableDependencies(assemblies);
|
||||
@@ -302,4 +328,107 @@ public sealed class ModuleManager : IModuleManager
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Delegate CreateHandler(Type ownerType, MethodInfo method, IDictionary<Type, object> instances)
|
||||
{
|
||||
ValidateSignature(method);
|
||||
|
||||
object? target = null;
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
if (!instances.TryGetValue(ownerType, out target))
|
||||
{
|
||||
target = Activator.CreateInstance(ownerType)
|
||||
?? throw new InvalidOperationException($"Cannot instantiate module type: {ownerType.FullName}");
|
||||
instances.Add(ownerType, target);
|
||||
}
|
||||
}
|
||||
|
||||
var parameterCount = method.GetParameters().Length;
|
||||
if (parameterCount == 0)
|
||||
{
|
||||
var noArg = method.IsStatic
|
||||
? (Func<int>)method.CreateDelegate(typeof(Func<int>))
|
||||
: (Func<int>)method.CreateDelegate(typeof(Func<int>), target!);
|
||||
|
||||
SysAbiFunction adapter = _ => noArg();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
return method.IsStatic
|
||||
? method.CreateDelegate(typeof(SysAbiFunction))
|
||||
: method.CreateDelegate(typeof(SysAbiFunction), target!);
|
||||
}
|
||||
|
||||
private static void ValidateSignature(MethodInfo method)
|
||||
{
|
||||
if (method.ReturnType != typeof(int))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.DeclaringType?.FullName}.{method.Name} must return int.");
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(CpuContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.DeclaringType?.FullName}.{method.Name} must accept no arguments or one {nameof(CpuContext)} argument.");
|
||||
}
|
||||
|
||||
private static ExportInfo? ResolveExportInfo(
|
||||
SysAbiExportAttribute exportAttribute,
|
||||
MethodInfo method,
|
||||
Generation generation,
|
||||
ISymbolCatalog? symbolCatalog)
|
||||
{
|
||||
var target = exportAttribute.Target == Generation.None
|
||||
? generation
|
||||
: exportAttribute.Target;
|
||||
if ((target & generation) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nid = exportAttribute.Nid;
|
||||
var exportName = exportAttribute.ExportName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName) && symbolCatalog?.TryGetByExportName(exportName, out var byName) == true)
|
||||
{
|
||||
nid = byName.Nid;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(nid) && symbolCatalog?.TryGetByNid(nid, out var byNid) == true)
|
||||
{
|
||||
exportName = string.IsNullOrWhiteSpace(exportName) ? byNid.ExportName : exportName;
|
||||
target = exportAttribute.Target == Generation.None ? byNid.Target : target;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nid))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.DeclaringType?.FullName}.{method.Name} must define a NID or match one in symbols catalog.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(exportName))
|
||||
{
|
||||
exportName = method.Name;
|
||||
}
|
||||
|
||||
if ((target & generation) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var libraryName = string.IsNullOrWhiteSpace(exportAttribute.LibraryName) ? "libKernel" : exportAttribute.LibraryName;
|
||||
return new ExportInfo(nid, exportName, libraryName, target);
|
||||
}
|
||||
|
||||
private readonly record struct ExportInfo(string Nid, string ExportName, string LibraryName, Generation Target);
|
||||
}
|
||||
|
||||
@@ -21,51 +21,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Forces build ordering for the aerolib task below; loaded as a build component,
|
||||
never a runtime dependency. -->
|
||||
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
|
||||
ReferenceOutputAssembly="false" />
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- aerolib.bin (the runtime NID -> name catalog) is derived data: generated here
|
||||
from scripts/ps5_names.txt via the same Ps5Nid implementation the analyzers use,
|
||||
embedded from the intermediate directory, and never committed. Replaces
|
||||
scripts/generate_aerolib_binary.py. -->
|
||||
<PropertyGroup>
|
||||
<SharpEmuAerolibNamesFile>$(MSBuildThisFileDirectory)..\..\scripts\ps5_names.txt</SharpEmuAerolibNamesFile>
|
||||
<SharpEmuAerolibTaskAssembly>$(MSBuildThisFileDirectory)..\..\artifacts\bin\$(Configuration)\netstandard2.0\SharpEmu.SourceGenerators.dll</SharpEmuAerolibTaskAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<UsingTask TaskName="SharpEmu.SourceGenerators.GenerateAerolibBinaryTask"
|
||||
TaskFactory="TaskHostFactory"
|
||||
AssemblyFile="$(SharpEmuAerolibTaskAssembly)" />
|
||||
|
||||
<!-- Runs after ResolveProjectReferences (which builds the task assembly) and before
|
||||
resource processing embeds the output. Skipped for design-time builds: the IDE
|
||||
resolves project references without compiling them, so on a fresh clone the task
|
||||
assembly does not exist yet and loading it would break the design-time build. -->
|
||||
<Target Name="GenerateAerolibBinary"
|
||||
DependsOnTargets="ResolveProjectReferences"
|
||||
Inputs="$(SharpEmuAerolibNamesFile);$(SharpEmuAerolibTaskAssembly)"
|
||||
Outputs="$(IntermediateOutputPath)aerolib.bin">
|
||||
<GenerateAerolibBinaryTask NamesFile="$(SharpEmuAerolibNamesFile)"
|
||||
OutputFile="$(IntermediateOutputPath)aerolib.bin" />
|
||||
</Target>
|
||||
|
||||
<!-- The resource item is created inside a target (not statically) so design-time
|
||||
builds never reference a file that was not generated, and in a separate target
|
||||
from the generation so an up-to-date skip of GenerateAerolibBinary cannot drop
|
||||
the item (an up-to-date target skips its whole body, ItemGroups included).
|
||||
Hooked before AssignTargetPaths: dynamic EmbeddedResource items added later
|
||||
than that miss the resource pipeline entirely. -->
|
||||
<Target Name="EmbedAerolibBinary"
|
||||
BeforeTargets="AssignTargetPaths"
|
||||
DependsOnTargets="GenerateAerolibBinary"
|
||||
Condition="'$(DesignTimeBuild)' != 'true'">
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(IntermediateOutputPath)aerolib.bin"
|
||||
LogicalName="SharpEmu.HLE.Aerolib.aerolib.bin"
|
||||
Visible="false" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
-165
@@ -1,13 +1,11 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
@@ -159,14 +157,13 @@ public static class AgcExports
|
||||
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
||||
private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = [];
|
||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||
// Concurrent so the per-draw/per-dispatch hit path is lock-free (and no longer
|
||||
// shares _submitTraceGate with tracing).
|
||||
private static readonly ConcurrentDictionary<
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, ulong OutputLayout, uint OutputCount, uint Attributes),
|
||||
(IGuestCompiledShader Vertex, IGuestCompiledShader Pixel)> _graphicsShaderCache = new();
|
||||
private static readonly ConcurrentDictionary<
|
||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
|
||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||
IGuestCompiledShader> _computeShaderCache = new();
|
||||
byte[]> _computeSpirvCache = new();
|
||||
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly bool _traceAgc = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
||||
@@ -337,20 +334,17 @@ public static class AgcExports
|
||||
ulong ExportShaderAddress,
|
||||
ulong PixelShaderAddress,
|
||||
uint PrimitiveType,
|
||||
IGuestCompiledShader VertexShader,
|
||||
IGuestCompiledShader PixelShader,
|
||||
byte[] VertexSpirv,
|
||||
byte[] PixelSpirv,
|
||||
uint AttributeCount,
|
||||
uint VertexCount,
|
||||
uint InstanceCount,
|
||||
GuestIndexBuffer? IndexBuffer,
|
||||
VulkanGuestIndexBuffer? IndexBuffer,
|
||||
IReadOnlyList<TranslatedImageBinding> Textures,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs,
|
||||
IReadOnlyList<RenderTargetDescriptor> RenderTargets,
|
||||
// The seam-shaped view of RenderTargets, built once here so the per-frame
|
||||
// submit path does not rebuild it for every draw of a cached translation.
|
||||
IReadOnlyList<GuestRenderTarget> GuestTargets,
|
||||
GuestRenderState RenderState);
|
||||
VulkanGuestRenderState RenderState);
|
||||
|
||||
private sealed record TranslatedImageBinding(
|
||||
TextureDescriptor Descriptor,
|
||||
@@ -429,8 +423,6 @@ public static class AgcExports
|
||||
|
||||
private sealed record RegisterDefaultsAllocation(ulong Primary, ulong Internal);
|
||||
|
||||
// NID captured from shipped titles; 'sceAgcInit' is a working label that collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "23LRUSvYu1M",
|
||||
ExportName = "sceAgcInit",
|
||||
@@ -448,7 +440,6 @@ public static class AgcExports
|
||||
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "2JtWUUiYBXs",
|
||||
@@ -628,8 +619,6 @@ public static class AgcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "HV4j+E0MBHE",
|
||||
ExportName = "sceAgcCreateInterpolantMapping",
|
||||
@@ -687,10 +676,7 @@ public static class AgcExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||
#pragma warning disable SHEM004
|
||||
[SysAbiExport(
|
||||
Nid = "V++UgBtQhn0",
|
||||
ExportName = "sceAgcGetDataPacketPayloadAddress",
|
||||
@@ -734,7 +720,6 @@ public static class AgcExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LtTouSCZjHM",
|
||||
@@ -2555,8 +2540,6 @@ public static class AgcExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "-KRzWekV120",
|
||||
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
||||
@@ -2571,7 +2554,6 @@ public static class AgcExports
|
||||
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "h9z6+0hEydk",
|
||||
@@ -2585,8 +2567,6 @@ public static class AgcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "qj7QZpgr9Uw",
|
||||
ExportName = "sceAgcUnknownQj7QZpgr9Uw",
|
||||
@@ -2607,7 +2587,6 @@ public static class AgcExports
|
||||
$"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}");
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
// WAIT_REG_MEM packets whose condition is not met suspend their DCB into
|
||||
// GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest
|
||||
@@ -2741,12 +2720,8 @@ public static class AgcExports
|
||||
ctx.TryReadUInt32(currentAddress + sizeof(uint), out var eventTypeRaw))
|
||||
{
|
||||
var eventType = eventTypeRaw & 0x3Fu;
|
||||
|
||||
// The guest registers AGC events with a full eventId, but the command buffer
|
||||
// only carries a 6-bit EVENT_TYPE. Those two values are not the same numbering
|
||||
// scheme, so exact ident matching never wakes anything. Trigger every graphics
|
||||
// event registration instead (workaround for issue #173).
|
||||
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
||||
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents(
|
||||
eventType,
|
||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||
eventType);
|
||||
if (tracePackets)
|
||||
@@ -2967,7 +2942,7 @@ public static class AgcExports
|
||||
handle,
|
||||
displayBufferIndex,
|
||||
out var cachedDisplayBuffer) &&
|
||||
GuestGpu.Current.TrySubmitGuestImage(
|
||||
VulkanVideoPresenter.TrySubmitGuestImage(
|
||||
cachedDisplayBuffer.Address,
|
||||
cachedDisplayBuffer.Width,
|
||||
cachedDisplayBuffer.Height,
|
||||
@@ -2991,11 +2966,11 @@ public static class AgcExports
|
||||
displayBufferIndex,
|
||||
translatedDisplayBuffer,
|
||||
"draw-fallback");
|
||||
var textures = CreateGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
|
||||
var textures = CreateVulkanGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
|
||||
var globalMemoryBuffers =
|
||||
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
GuestGpu.Current.SubmitTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
VulkanVideoPresenter.SubmitTranslatedDraw(
|
||||
translatedDraw.PixelSpirv,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDisplayBuffer.Width,
|
||||
@@ -3003,7 +2978,7 @@ public static class AgcExports
|
||||
translatedDraw.AttributeCount);
|
||||
TraceAgcShader(
|
||||
$"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " +
|
||||
$"spirv={translatedDraw.PixelShader.Payload.Length} textures={textures.Count} " +
|
||||
$"spirv={translatedDraw.PixelSpirv.Length} textures={textures.Count} " +
|
||||
$"global_buffers={globalMemoryBuffers.Count} " +
|
||||
$"fallback={fallbackTextureCount} {translatedDisplayBuffer.Width}x{translatedDisplayBuffer.Height}");
|
||||
|
||||
@@ -3038,7 +3013,7 @@ public static class AgcExports
|
||||
displayBufferIndex,
|
||||
out var displayBuffer))
|
||||
{
|
||||
GuestGpu.Current.SubmitGuestDraw(
|
||||
VulkanVideoPresenter.SubmitGuestDraw(
|
||||
state.GuestDrawKind,
|
||||
displayBuffer.Width,
|
||||
displayBuffer.Height);
|
||||
@@ -3434,23 +3409,29 @@ public static class AgcExports
|
||||
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
|
||||
if (firstTarget.Address != 0)
|
||||
{
|
||||
var textures = CreateGuestDrawTextures(
|
||||
var textures = CreateVulkanGuestDrawTextures(
|
||||
ctx,
|
||||
translatedDraw.Textures,
|
||||
out _);
|
||||
var globalMemoryBuffers =
|
||||
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
var vertexBuffers =
|
||||
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||
CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||
TraceRectListVertices(translatedDraw, vertexBuffers);
|
||||
TraceGrassDrawVertices(translatedDraw, textures, vertexBuffers);
|
||||
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||
translatedDraw.PixelSpirv,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
translatedDraw.GuestTargets,
|
||||
translatedDraw.VertexShader,
|
||||
translatedDraw.RenderTargets.Select(target =>
|
||||
new VulkanGuestRenderTarget(
|
||||
target.Address,
|
||||
target.Width,
|
||||
target.Height,
|
||||
target.Format,
|
||||
target.NumberType)).ToArray(),
|
||||
translatedDraw.VertexSpirv,
|
||||
translatedDraw.VertexCount,
|
||||
translatedDraw.InstanceCount,
|
||||
translatedDraw.PrimitiveType,
|
||||
@@ -3464,14 +3445,14 @@ public static class AgcExports
|
||||
.FirstOrDefault(binding => binding.IsStorage);
|
||||
if (storageTarget is not null)
|
||||
{
|
||||
var textures = CreateGuestDrawTextures(
|
||||
var textures = CreateVulkanGuestDrawTextures(
|
||||
ctx,
|
||||
translatedDraw.Textures,
|
||||
out _);
|
||||
var globalMemoryBuffers =
|
||||
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
GuestGpu.Current.SubmitStorageTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
||||
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
||||
translatedDraw.PixelSpirv,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
@@ -3580,14 +3561,14 @@ public static class AgcExports
|
||||
.Where(target => HasPixelColorExport(pixelState, target.Slot))
|
||||
.OrderBy(target => target.Slot)
|
||||
.ToArray();
|
||||
var renderTargetOutputKinds = new Gen5PixelOutputKind[renderTargets.Length];
|
||||
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
var target = renderTargets[index];
|
||||
if (!GuestGpu.Current.TryGetRenderTargetOutputKind(
|
||||
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
|
||||
target.Format,
|
||||
target.NumberType,
|
||||
out renderTargetOutputKinds[index]))
|
||||
out renderTargetFormats[index]))
|
||||
{
|
||||
error =
|
||||
$"unsupported color target format={target.Format} number_type={target.NumberType}";
|
||||
@@ -3595,17 +3576,16 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
// Exact packed encoding of the output layout — guest slot (6 bits, CB targets are
|
||||
// 0-7) plus output kind (2 bits) per target, host locations being the sequential
|
||||
// byte positions. Replaces a per-draw LINQ + string build that allocated on every
|
||||
// draw, cache hit or not; the target count disambiguates trailing zero bytes.
|
||||
var outputLayout = 0UL;
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
outputLayout |= (ulong)(((renderTargets[index].Slot & 0x3Fu) << 2) |
|
||||
(uint)renderTargetOutputKinds[index]) << (index * 8);
|
||||
}
|
||||
|
||||
var pixelOutputs = renderTargets
|
||||
.Select((target, location) => new Gen5PixelOutputBinding(
|
||||
target.Slot,
|
||||
(uint)location,
|
||||
renderTargetFormats[location].OutputKind))
|
||||
.ToArray();
|
||||
var outputLayout = string.Join(
|
||||
';',
|
||||
pixelOutputs.Select(output =>
|
||||
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
|
||||
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||
@@ -3615,25 +3595,19 @@ public static class AgcExports
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
outputLayout,
|
||||
(uint)renderTargets.Length,
|
||||
attributeCount);
|
||||
var totalGlobalBuffers =
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count;
|
||||
_graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
|
||||
(byte[] Vertex, byte[] Pixel) compiled;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_graphicsSpirvCache.TryGetValue(shaderKey, out compiled);
|
||||
}
|
||||
|
||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||
{
|
||||
var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
|
||||
for (var location = 0; location < renderTargets.Length; location++)
|
||||
{
|
||||
pixelOutputs[location] = new Gen5PixelOutputBinding(
|
||||
renderTargets[location].Slot,
|
||||
(uint)location,
|
||||
renderTargetOutputKinds[location]);
|
||||
}
|
||||
|
||||
if (!GuestGpu.Current.TryCompilePixelShader(
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
pixelState,
|
||||
pixelEvaluation,
|
||||
pixelOutputs,
|
||||
@@ -3643,7 +3617,7 @@ public static class AgcExports
|
||||
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||
imageBindingBase: 0,
|
||||
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||
!GuestGpu.Current.TryCompileVertexShader(
|
||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
out var vertexShader,
|
||||
@@ -3656,20 +3630,23 @@ public static class AgcExports
|
||||
return false;
|
||||
}
|
||||
|
||||
compiled = (vertexShader!, pixelShader!);
|
||||
DumpCompiledShader(
|
||||
compiled = (vertexShader.Spirv, pixelShader.Spirv);
|
||||
DumpSpirv(
|
||||
"vs",
|
||||
exportShaderAddress,
|
||||
exportStateFingerprint,
|
||||
compiled.Vertex,
|
||||
exportState.Program);
|
||||
DumpCompiledShader(
|
||||
DumpSpirv(
|
||||
"ps",
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
compiled.Pixel,
|
||||
pixelState.Program);
|
||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_graphicsSpirvCache.TryAdd(shaderKey, compiled);
|
||||
}
|
||||
}
|
||||
|
||||
var imageBindings = pixelEvaluation.ImageBindings
|
||||
@@ -3706,17 +3683,6 @@ public static class AgcExports
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
exportEvaluation.VertexInputs ?? [];
|
||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||
var guestTargets = new GuestRenderTarget[renderTargets.Length];
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
{
|
||||
guestTargets[index] = new GuestRenderTarget(
|
||||
renderTargets[index].Address,
|
||||
renderTargets[index].Width,
|
||||
renderTargets[index].Height,
|
||||
renderTargets[index].Format,
|
||||
renderTargets[index].NumberType);
|
||||
}
|
||||
|
||||
draw = new TranslatedGuestDraw(
|
||||
exportShaderAddress,
|
||||
pixelShaderAddress,
|
||||
@@ -3726,12 +3692,11 @@ public static class AgcExports
|
||||
attributeCount,
|
||||
vertexCount,
|
||||
state.InstanceCount,
|
||||
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
|
||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||
textures,
|
||||
globalMemoryBindings,
|
||||
vertexInputs,
|
||||
renderTargets,
|
||||
guestTargets,
|
||||
ApplyTransparentPremultipliedFillClear(
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
|
||||
textures,
|
||||
@@ -3752,8 +3717,8 @@ public static class AgcExports
|
||||
/// Treat precisely that draw shape as an overwrite only when every MRT
|
||||
/// attachment uses the same premultiplied blend pattern.
|
||||
/// </summary>
|
||||
private static GuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||
GuestRenderState renderState,
|
||||
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||
VulkanGuestRenderState renderState,
|
||||
IReadOnlyList<TranslatedImageBinding> textures,
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
|
||||
IReadOnlyList<uint> pixelUserData)
|
||||
@@ -3783,7 +3748,7 @@ public static class AgcExports
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
|
||||
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
|
||||
blend is
|
||||
{
|
||||
Enable: true,
|
||||
@@ -3792,7 +3757,7 @@ public static class AgcExports
|
||||
ColorFunc: 0,
|
||||
};
|
||||
|
||||
private static GuestIndexBuffer? CreateGuestIndexBuffer(
|
||||
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||
CpuContext ctx,
|
||||
SubmittedDcbState state,
|
||||
uint indexCount)
|
||||
@@ -3810,7 +3775,7 @@ public static class AgcExports
|
||||
var address = state.IndexBufferAddress + byteOffset;
|
||||
return (ctx.Memory.TryRead(address, data) ||
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, data))
|
||||
? new GuestIndexBuffer(data, is32Bit)
|
||||
? new VulkanGuestIndexBuffer(data, is32Bit)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -3978,19 +3943,19 @@ public static class AgcExports
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static GuestRenderState CreateRenderState(
|
||||
private static VulkanGuestRenderState CreateRenderState(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
IReadOnlyList<RenderTargetDescriptor> targets,
|
||||
Gen5ShaderState pixelState)
|
||||
{
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return GuestRenderState.Default;
|
||||
return VulkanGuestRenderState.Default;
|
||||
}
|
||||
|
||||
var target = targets[0];
|
||||
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
||||
return new GuestRenderState(
|
||||
return new VulkanGuestRenderState(
|
||||
targets.Select(target =>
|
||||
{
|
||||
var blend = DecodeBlendState(registers, target.Slot);
|
||||
@@ -4003,7 +3968,7 @@ public static class AgcExports
|
||||
DecodeViewport(registers, target.Width, target.Height, scissor));
|
||||
}
|
||||
|
||||
private static GuestBlendState DecodeBlendState(
|
||||
private static VulkanGuestBlendState DecodeBlendState(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
uint slot)
|
||||
{
|
||||
@@ -4014,7 +3979,7 @@ public static class AgcExports
|
||||
}
|
||||
|
||||
registers.TryGetValue(CbBlend0Control + slot, out var control);
|
||||
return new GuestBlendState(
|
||||
return new VulkanGuestBlendState(
|
||||
((control >> 30) & 1u) != 0,
|
||||
control & 0x1Fu,
|
||||
(control >> 8) & 0x1Fu,
|
||||
@@ -4026,14 +3991,14 @@ public static class AgcExports
|
||||
writeMask);
|
||||
}
|
||||
|
||||
private static GuestRect? DecodeScissor(
|
||||
private static VulkanGuestRect? DecodeScissor(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
uint targetWidth,
|
||||
uint targetHeight)
|
||||
{
|
||||
if (targetWidth == 0 || targetHeight == 0)
|
||||
{
|
||||
return new GuestRect(0, 0, 0, 0);
|
||||
return new VulkanGuestRect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
var left = 0;
|
||||
@@ -4098,22 +4063,22 @@ public static class AgcExports
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GuestRect(
|
||||
return new VulkanGuestRect(
|
||||
left,
|
||||
top,
|
||||
checked((uint)(right - left)),
|
||||
checked((uint)(bottom - top)));
|
||||
}
|
||||
|
||||
private static GuestViewport? DecodeViewport(
|
||||
private static VulkanGuestViewport? DecodeViewport(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
uint targetWidth,
|
||||
uint targetHeight,
|
||||
GuestRect? scissor)
|
||||
VulkanGuestRect? scissor)
|
||||
{
|
||||
if (targetWidth == 0 || targetHeight == 0)
|
||||
{
|
||||
return new GuestViewport(0, 0, 0, 0, 0, 1);
|
||||
return new VulkanGuestViewport(0, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
var minDepth = 0f;
|
||||
@@ -4139,7 +4104,7 @@ public static class AgcExports
|
||||
xScale > 0f &&
|
||||
yScale != 0f)
|
||||
{
|
||||
return new GuestViewport(
|
||||
return new VulkanGuestViewport(
|
||||
xOffset - xScale,
|
||||
yOffset - yScale,
|
||||
xScale * 2f,
|
||||
@@ -4152,10 +4117,10 @@ public static class AgcExports
|
||||
{
|
||||
return minDepth == 0f && maxDepth == 1f
|
||||
? null
|
||||
: new GuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
|
||||
: new VulkanGuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
|
||||
}
|
||||
|
||||
return new GuestViewport(
|
||||
return new VulkanGuestViewport(
|
||||
rect.X,
|
||||
rect.Y,
|
||||
rect.Width,
|
||||
@@ -4332,7 +4297,7 @@ public static class AgcExports
|
||||
var blend = draw.RenderState.Blend;
|
||||
TraceAgcShader(
|
||||
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
|
||||
$"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelShader.Payload.Length} " +
|
||||
$"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelSpirv.Length} " +
|
||||
$"primitive=0x{draw.PrimitiveType:X} " +
|
||||
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
|
||||
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
|
||||
@@ -4342,16 +4307,16 @@ public static class AgcExports
|
||||
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
|
||||
}
|
||||
|
||||
private static IReadOnlyList<GuestDrawTexture> CreateGuestDrawTextures(
|
||||
private static IReadOnlyList<VulkanGuestDrawTexture> CreateVulkanGuestDrawTextures(
|
||||
CpuContext ctx,
|
||||
IReadOnlyList<TranslatedImageBinding> bindings,
|
||||
out int fallbackTextureCount)
|
||||
{
|
||||
var textures = new List<GuestDrawTexture>(bindings.Count);
|
||||
var textures = new List<VulkanGuestDrawTexture>(bindings.Count);
|
||||
fallbackTextureCount = 0;
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (TryCreateGuestDrawTexture(
|
||||
if (TryCreateVulkanGuestDrawTexture(
|
||||
ctx,
|
||||
binding.Descriptor,
|
||||
binding.IsStorage,
|
||||
@@ -4370,13 +4335,13 @@ public static class AgcExports
|
||||
return textures;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<GuestMemoryBuffer> CreateGuestMemoryBuffers(
|
||||
private static IReadOnlyList<VulkanGuestMemoryBuffer> CreateVulkanGuestMemoryBuffers(
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
|
||||
{
|
||||
var buffers = new GuestMemoryBuffer[bindings.Count];
|
||||
var buffers = new VulkanGuestMemoryBuffer[bindings.Count];
|
||||
for (var index = 0; index < bindings.Count; index++)
|
||||
{
|
||||
buffers[index] = new GuestMemoryBuffer(
|
||||
buffers[index] = new VulkanGuestMemoryBuffer(
|
||||
bindings[index].BaseAddress,
|
||||
bindings[index].Data);
|
||||
}
|
||||
@@ -4384,14 +4349,14 @@ public static class AgcExports
|
||||
return buffers;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<GuestVertexBuffer> CreateGuestVertexBuffers(
|
||||
private static IReadOnlyList<VulkanGuestVertexBuffer> CreateVulkanGuestVertexBuffers(
|
||||
IReadOnlyList<Gen5VertexInputBinding> bindings)
|
||||
{
|
||||
var buffers = new GuestVertexBuffer[bindings.Count];
|
||||
var buffers = new VulkanGuestVertexBuffer[bindings.Count];
|
||||
for (var index = 0; index < bindings.Count; index++)
|
||||
{
|
||||
var binding = bindings[index];
|
||||
buffers[index] = new GuestVertexBuffer(
|
||||
buffers[index] = new VulkanGuestVertexBuffer(
|
||||
binding.Location,
|
||||
binding.ComponentCount,
|
||||
binding.DataFormat,
|
||||
@@ -4405,13 +4370,13 @@ public static class AgcExports
|
||||
return buffers;
|
||||
}
|
||||
|
||||
private static bool TryCreateGuestDrawTexture(
|
||||
private static bool TryCreateVulkanGuestDrawTexture(
|
||||
CpuContext ctx,
|
||||
TextureDescriptor descriptor,
|
||||
bool isStorage,
|
||||
uint mipLevel,
|
||||
IReadOnlyList<uint> samplerDescriptor,
|
||||
out GuestDrawTexture texture)
|
||||
out VulkanGuestDrawTexture texture)
|
||||
{
|
||||
texture = default!;
|
||||
if (descriptor.Type != Gen5TextureType2D ||
|
||||
@@ -4447,12 +4412,12 @@ public static class AgcExports
|
||||
|
||||
if (!isStorage &&
|
||||
descriptor.Address != 0 &&
|
||||
GuestGpu.Current.IsGpuGuestImageAvailable(
|
||||
VulkanVideoPresenter.IsGpuGuestImageAvailable(
|
||||
descriptor.Address,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType))
|
||||
{
|
||||
texture = new GuestDrawTexture(
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
@@ -4466,7 +4431,7 @@ public static class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4486,7 +4451,7 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
texture = new GuestDrawTexture(
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
@@ -4500,7 +4465,7 @@ public static class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4541,7 +4506,7 @@ public static class AgcExports
|
||||
DumpTextureSourceIfRequested(descriptor, sourceWidth, source);
|
||||
|
||||
var rgba = source;
|
||||
texture = new GuestDrawTexture(
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
@@ -4555,7 +4520,7 @@ public static class AgcExports
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToGuestSampler(samplerDescriptor));
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4588,8 +4553,8 @@ public static class AgcExports
|
||||
|
||||
private static void TraceGrassDrawVertices(
|
||||
TranslatedGuestDraw draw,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
||||
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||
IReadOnlyList<VulkanGuestVertexBuffer> vertexBuffers)
|
||||
{
|
||||
if (_grassTraceCount >= 6 ||
|
||||
!textures.Any(texture => texture.Width == 288 && texture.Height == 160) ||
|
||||
@@ -4628,7 +4593,7 @@ public static class AgcExports
|
||||
|
||||
private static void TraceRectListVertices(
|
||||
TranslatedGuestDraw draw,
|
||||
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
||||
IReadOnlyList<VulkanGuestVertexBuffer> vertexBuffers)
|
||||
{
|
||||
if (draw.PrimitiveType != 0x11 ||
|
||||
draw.IndexBuffer is not null ||
|
||||
@@ -4711,7 +4676,7 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||
bool isStorage,
|
||||
uint format,
|
||||
uint numberType)
|
||||
@@ -4759,9 +4724,9 @@ public static class AgcExports
|
||||
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
|
||||
}
|
||||
|
||||
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
|
||||
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
||||
descriptor.Count >= 4
|
||||
? new GuestSampler(
|
||||
? new VulkanGuestSampler(
|
||||
descriptor[0],
|
||||
descriptor[1],
|
||||
descriptor[2],
|
||||
@@ -4959,39 +4924,47 @@ public static class AgcExports
|
||||
localSizeX,
|
||||
localSizeY,
|
||||
localSizeZ);
|
||||
_computeShaderCache.TryGetValue(shaderKey, out var computeShader);
|
||||
byte[] computeSpirv;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_computeSpirvCache.TryGetValue(shaderKey, out computeSpirv!);
|
||||
}
|
||||
|
||||
if (computeShader is null &&
|
||||
GuestGpu.Current.TryCompileComputeShader(
|
||||
if (computeSpirv is null &&
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
shaderState,
|
||||
evaluation,
|
||||
localSizeX,
|
||||
localSizeY,
|
||||
localSizeZ,
|
||||
out computeShader,
|
||||
out var compiledCompute,
|
||||
out computeError))
|
||||
{
|
||||
DumpCompiledShader(
|
||||
computeSpirv = compiledCompute.Spirv;
|
||||
DumpSpirv(
|
||||
"cs",
|
||||
shaderAddress,
|
||||
shaderKey.Item2,
|
||||
computeShader!,
|
||||
computeSpirv,
|
||||
shaderState.Program);
|
||||
}
|
||||
|
||||
if (computeShader is not null)
|
||||
if (computeSpirv is not null)
|
||||
{
|
||||
_computeShaderCache.TryAdd(shaderKey, computeShader);
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_computeSpirvCache.TryAdd(shaderKey, computeSpirv);
|
||||
}
|
||||
|
||||
var textures = CreateGuestDrawTextures(
|
||||
var textures = CreateVulkanGuestDrawTextures(
|
||||
ctx,
|
||||
translatedBindings,
|
||||
out _);
|
||||
var globalMemoryBuffers =
|
||||
CreateGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
|
||||
GuestGpu.Current.SubmitComputeDispatch(
|
||||
CreateVulkanGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
|
||||
VulkanVideoPresenter.SubmitComputeDispatch(
|
||||
shaderAddress,
|
||||
computeShader,
|
||||
computeSpirv,
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
dispatch.GroupCountX,
|
||||
@@ -5162,7 +5135,7 @@ public static class AgcExports
|
||||
}
|
||||
}
|
||||
else if (source is { } cachedSourceTexture &&
|
||||
GuestGpu.Current.TrySubmitGuestImageBlit(
|
||||
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
||||
cachedSourceTexture.Address,
|
||||
cachedSourceTexture.Width,
|
||||
cachedSourceTexture.Height,
|
||||
@@ -5516,7 +5489,7 @@ public static class AgcExports
|
||||
$"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}");
|
||||
}
|
||||
|
||||
if (GuestGpu.Current.TryCompilePixelShader(
|
||||
if (Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
pixelState,
|
||||
evaluation,
|
||||
[new(0, 0, Gen5PixelOutputKind.Float)],
|
||||
@@ -5525,7 +5498,7 @@ public static class AgcExports
|
||||
{
|
||||
TraceAgcShader(
|
||||
$"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " +
|
||||
$"bytes={compiledPixel!.Payload.Length} bindings={evaluation.ImageBindings.Count} " +
|
||||
$"bytes={compiledPixel.Spirv.Length} bindings={evaluation.ImageBindings.Count} " +
|
||||
$"global_buffers={evaluation.GlobalMemoryBindings.Count}");
|
||||
}
|
||||
else
|
||||
@@ -6415,14 +6388,14 @@ public static class AgcExports
|
||||
$"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " +
|
||||
$"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}";
|
||||
|
||||
private static void DumpCompiledShader(
|
||||
private static void DumpSpirv(
|
||||
string stage,
|
||||
ulong shaderAddress,
|
||||
ulong stateFingerprint,
|
||||
IGuestCompiledShader shader,
|
||||
byte[] spirv,
|
||||
Gen5ShaderProgram program)
|
||||
{
|
||||
if (shader.Payload.Length == 0 ||
|
||||
if (spirv.Length == 0 ||
|
||||
!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"),
|
||||
"1",
|
||||
@@ -6434,9 +6407,7 @@ public static class AgcExports
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "shader-dumps");
|
||||
Directory.CreateDirectory(directory);
|
||||
var name = $"{shaderAddress:X16}-{stateFingerprint:X16}.{stage}";
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(directory, $"{name}.{shader.PayloadFileExtension}"),
|
||||
shader.Payload);
|
||||
File.WriteAllBytes(Path.Combine(directory, $"{name}.spv"), spirv);
|
||||
|
||||
var lines = new List<string>(program.Instructions.Count + 2)
|
||||
{
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Wires the backend-neutral shader compiler to this assembly's HLE services. The
|
||||
/// module initializer runs before any Libs code can invoke the evaluator, so the hook
|
||||
/// is always installed first.
|
||||
/// </summary>
|
||||
internal static class AgcShaderCompilerHooks
|
||||
{
|
||||
[ModuleInitializer]
|
||||
[SuppressMessage(
|
||||
"Usage",
|
||||
"CA2255:The 'ModuleInitializer' attribute should not be used in libraries",
|
||||
Justification = "This is the rule's intended advanced scenario: the hook must be " +
|
||||
"installed before any code path can reach the evaluator, and every such path " +
|
||||
"enters through this assembly.")]
|
||||
internal static void Install()
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
||||
}
|
||||
}
|
||||
+42
-28
@@ -1,9 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public enum Gen5ShaderEncoding
|
||||
internal enum Gen5ShaderEncoding
|
||||
{
|
||||
Sop1,
|
||||
Sop2,
|
||||
@@ -26,7 +26,7 @@ public enum Gen5ShaderEncoding
|
||||
Exp,
|
||||
}
|
||||
|
||||
public enum Gen5OperandKind
|
||||
internal enum Gen5OperandKind
|
||||
{
|
||||
ScalarRegister,
|
||||
VectorRegister,
|
||||
@@ -34,7 +34,7 @@ public enum Gen5OperandKind
|
||||
LiteralConstant,
|
||||
}
|
||||
|
||||
public enum Gen5ShaderResourceKind
|
||||
internal enum Gen5ShaderResourceKind
|
||||
{
|
||||
ReadOnlyTexture,
|
||||
ReadWriteTexture,
|
||||
@@ -42,31 +42,45 @@ public enum Gen5ShaderResourceKind
|
||||
ConstantBuffer,
|
||||
}
|
||||
|
||||
public enum Gen5PixelOutputKind
|
||||
internal enum Gen5PixelOutputKind
|
||||
{
|
||||
Float,
|
||||
Uint,
|
||||
Sint,
|
||||
}
|
||||
|
||||
public readonly record struct Gen5PixelOutputBinding(
|
||||
internal readonly record struct Gen5PixelOutputBinding(
|
||||
uint GuestSlot,
|
||||
uint HostLocation,
|
||||
Gen5PixelOutputKind Kind);
|
||||
|
||||
public readonly record struct Gen5ShaderResourceMapping(
|
||||
internal enum Gen5SpirvStage
|
||||
{
|
||||
Vertex,
|
||||
Pixel,
|
||||
Compute,
|
||||
}
|
||||
|
||||
internal sealed record Gen5SpirvShader(
|
||||
byte[] Spirv,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
uint AttributeCount,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
|
||||
|
||||
internal readonly record struct Gen5ShaderResourceMapping(
|
||||
Gen5ShaderResourceKind Kind,
|
||||
uint Slot,
|
||||
uint OffsetDwords,
|
||||
bool SizeFlag);
|
||||
|
||||
public sealed record Gen5ShaderMetadata(
|
||||
internal sealed record Gen5ShaderMetadata(
|
||||
uint ExtendedUserDataSizeDwords,
|
||||
uint ShaderResourceTableSizeDwords,
|
||||
IReadOnlyDictionary<uint, uint> DirectResources,
|
||||
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
|
||||
|
||||
public readonly record struct Gen5ComputeSystemRegisters(
|
||||
internal readonly record struct Gen5ComputeSystemRegisters(
|
||||
uint? WorkGroupXRegister,
|
||||
uint? WorkGroupYRegister,
|
||||
uint? WorkGroupZRegister,
|
||||
@@ -119,14 +133,14 @@ public readonly record struct Gen5ComputeSystemRegisters(
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Gen5ShaderState(
|
||||
internal sealed record Gen5ShaderState(
|
||||
Gen5ShaderProgram Program,
|
||||
IReadOnlyList<uint> UserData,
|
||||
Gen5ShaderMetadata? Metadata,
|
||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||
uint UserDataScalarRegisterBase = 0);
|
||||
|
||||
public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
||||
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
||||
{
|
||||
public static Gen5Operand Scalar(uint index) =>
|
||||
new(Gen5OperandKind.ScalarRegister, index);
|
||||
@@ -163,9 +177,9 @@ public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
||||
};
|
||||
}
|
||||
|
||||
public abstract record Gen5InstructionControl;
|
||||
internal abstract record Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5ImageControl(
|
||||
internal sealed record Gen5ImageControl(
|
||||
uint Dmask,
|
||||
uint VectorAddress,
|
||||
IReadOnlyList<uint> AddressRegisters,
|
||||
@@ -183,7 +197,7 @@ public sealed record Gen5ImageControl(
|
||||
: VectorAddress + (uint)component;
|
||||
}
|
||||
|
||||
public sealed record Gen5GlobalMemoryControl(
|
||||
internal sealed record Gen5GlobalMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
uint VectorData,
|
||||
@@ -192,7 +206,7 @@ public sealed record Gen5GlobalMemoryControl(
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5BufferMemoryControl(
|
||||
internal sealed record Gen5BufferMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
uint VectorData,
|
||||
@@ -203,25 +217,25 @@ public sealed record Gen5BufferMemoryControl(
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5ExportControl(
|
||||
internal sealed record Gen5ExportControl(
|
||||
uint Target,
|
||||
uint EnableMask,
|
||||
bool Compressed,
|
||||
bool Done,
|
||||
bool ValidMask) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5InterpolationControl(
|
||||
internal sealed record Gen5InterpolationControl(
|
||||
uint Attribute,
|
||||
uint Channel) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5Vop3Control(
|
||||
internal sealed record Gen5Vop3Control(
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint OutputModifier,
|
||||
bool Clamp,
|
||||
uint? ScalarDestination) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5SdwaControl(
|
||||
internal sealed record Gen5SdwaControl(
|
||||
uint DestinationSelect,
|
||||
uint Source0Select,
|
||||
uint Source1Select,
|
||||
@@ -230,7 +244,7 @@ public sealed record Gen5SdwaControl(
|
||||
uint OutputModifier,
|
||||
bool Clamp) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5DppControl(
|
||||
internal sealed record Gen5DppControl(
|
||||
uint Control,
|
||||
bool FetchInactive,
|
||||
bool BoundControl,
|
||||
@@ -239,17 +253,17 @@ public sealed record Gen5DppControl(
|
||||
uint BankMask,
|
||||
uint RowMask) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5ScalarMemoryControl(
|
||||
internal sealed record Gen5ScalarMemoryControl(
|
||||
uint DestinationCount,
|
||||
int ImmediateOffsetBytes,
|
||||
uint? DynamicOffsetRegister) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5DataShareControl(
|
||||
internal sealed record Gen5DataShareControl(
|
||||
uint Offset0,
|
||||
uint Offset1,
|
||||
bool Gds) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5ImageBinding(
|
||||
internal sealed record Gen5ImageBinding(
|
||||
uint Pc,
|
||||
string Opcode,
|
||||
Gen5ImageControl Control,
|
||||
@@ -257,13 +271,13 @@ public sealed record Gen5ImageBinding(
|
||||
IReadOnlyList<uint> SamplerDescriptor,
|
||||
uint? MipLevel);
|
||||
|
||||
public sealed record Gen5GlobalMemoryBinding(
|
||||
internal sealed record Gen5GlobalMemoryBinding(
|
||||
uint ScalarAddress,
|
||||
ulong BaseAddress,
|
||||
IReadOnlyList<uint> InstructionPcs,
|
||||
byte[] Data);
|
||||
|
||||
public sealed record Gen5VertexInputBinding(
|
||||
internal sealed record Gen5VertexInputBinding(
|
||||
uint Pc,
|
||||
uint Location,
|
||||
uint ComponentCount,
|
||||
@@ -274,7 +288,7 @@ public sealed record Gen5VertexInputBinding(
|
||||
uint OffsetBytes,
|
||||
byte[] Data);
|
||||
|
||||
public sealed record Gen5ShaderEvaluation(
|
||||
internal sealed record Gen5ShaderEvaluation(
|
||||
IReadOnlyList<uint> InitialScalarRegisters,
|
||||
IReadOnlyList<uint> ScalarRegisters,
|
||||
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
|
||||
@@ -284,7 +298,7 @@ public sealed record Gen5ShaderEvaluation(
|
||||
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
|
||||
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
|
||||
|
||||
public sealed record Gen5ShaderInstruction(
|
||||
internal sealed record Gen5ShaderInstruction(
|
||||
uint Pc,
|
||||
Gen5ShaderEncoding Encoding,
|
||||
string Opcode,
|
||||
@@ -293,7 +307,7 @@ public sealed record Gen5ShaderInstruction(
|
||||
IReadOnlyList<Gen5Operand> Destinations,
|
||||
Gen5InstructionControl? Control);
|
||||
|
||||
public sealed record Gen5ShaderProgram(
|
||||
internal sealed record Gen5ShaderProgram(
|
||||
ulong Address,
|
||||
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
||||
{
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public static class Gen5ShaderMetadataReader
|
||||
internal static class Gen5ShaderMetadataReader
|
||||
{
|
||||
private const ulong ShaderUserDataOffset = 0x08;
|
||||
private const int ResourceClassCount = 4;
|
||||
+12
-20
@@ -2,21 +2,13 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public static class Gen5ShaderScalarEvaluator
|
||||
internal static class Gen5ShaderScalarEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional fallback for global-memory reads that ctx.Memory cannot satisfy (the
|
||||
/// emulator installs the HLE-tracked libc heap reader here at module load). Kept as
|
||||
/// a hook so this project never depends on the HLE module implementations.
|
||||
/// </summary>
|
||||
public static Gen5FallbackMemoryReader? FallbackMemoryReader { get; set; }
|
||||
|
||||
public delegate bool Gen5FallbackMemoryReader(ulong baseAddress, Span<byte> destination);
|
||||
|
||||
private const int ScalarRegisterCount = 256;
|
||||
private const int ImageDescriptorDwords = 8;
|
||||
private const int SamplerDescriptorDwords = 4;
|
||||
@@ -28,12 +20,12 @@ public static class Gen5ShaderScalarEvaluator
|
||||
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
|
||||
: 1 * 1024 * 1024;
|
||||
|
||||
public static long GlobalMemoryReadCount;
|
||||
public static long GlobalMemoryReadBytes;
|
||||
public static long GlobalMemoryReadCacheHits;
|
||||
public static long GlobalMemoryReadPvmBytes;
|
||||
public static long GlobalMemoryReadLibcBytes;
|
||||
public static long GlobalMemoryReadReuses;
|
||||
internal static long GlobalMemoryReadCount;
|
||||
internal static long GlobalMemoryReadBytes;
|
||||
internal static long GlobalMemoryReadCacheHits;
|
||||
internal static long GlobalMemoryReadPvmBytes;
|
||||
internal static long GlobalMemoryReadLibcBytes;
|
||||
internal static long GlobalMemoryReadReuses;
|
||||
|
||||
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
|
||||
private static readonly object _crossFrameReadGate = new();
|
||||
@@ -581,12 +573,12 @@ public static class Gen5ShaderScalarEvaluator
|
||||
[ThreadStatic]
|
||||
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
|
||||
|
||||
public static void BeginGlobalMemoryReadScope()
|
||||
internal static void BeginGlobalMemoryReadScope()
|
||||
{
|
||||
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
|
||||
}
|
||||
|
||||
public static void EndGlobalMemoryReadScope()
|
||||
internal static void EndGlobalMemoryReadScope()
|
||||
{
|
||||
_globalMemoryReadCache = null;
|
||||
}
|
||||
@@ -655,7 +647,7 @@ public static class Gen5ShaderScalarEvaluator
|
||||
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
||||
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
||||
if (readFromPvm ||
|
||||
FallbackMemoryReader?.Invoke(baseAddress, data) == true)
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadCount);
|
||||
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
|
||||
+7
-15
@@ -2,13 +2,14 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public static class Gen5ShaderTranslator
|
||||
internal static class Gen5ShaderTranslator
|
||||
{
|
||||
private const int MaxInstructions = 4096;
|
||||
private const int MinimumUserDataDwords = 16;
|
||||
@@ -275,9 +276,7 @@ public static class Gen5ShaderTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public contract entry: emitter test suites and tools drive the decoder directly
|
||||
// from raw instruction words.
|
||||
public static bool TryDecodeProgram(
|
||||
private static bool TryDecodeProgram(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
out Gen5ShaderProgram program,
|
||||
@@ -911,10 +910,9 @@ public static class Gen5ShaderTranslator
|
||||
0x16A => "VMulHiU32",
|
||||
0x16B => "VMulLoI32",
|
||||
0x16C => "VMulHiI32",
|
||||
0x360 => "VReadlaneB32",
|
||||
0x361 => "VWritelaneB32",
|
||||
0x360 => "VMadU32U16",
|
||||
0x361 => "VMulLoU32",
|
||||
0x362 => "VLdexpF32",
|
||||
0x373 => "VMadU32U16",
|
||||
0x346 => "VLshlAddU32",
|
||||
0x347 => "VAddLshlU32",
|
||||
0x36D => "VAdd3U32",
|
||||
@@ -1222,7 +1220,7 @@ public static class Gen5ShaderTranslator
|
||||
private static bool IsMimgInstruction(string name) =>
|
||||
name.StartsWith("Image", StringComparison.Ordinal);
|
||||
|
||||
public static bool IsStorageImageOperation(string name) =>
|
||||
internal static bool IsStorageImageOperation(string name) =>
|
||||
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
|
||||
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
|
||||
|
||||
@@ -1467,12 +1465,6 @@ public static class Gen5ShaderTranslator
|
||||
Gen5Operand.Source((extra >> 18) & 0x1FF, literal),
|
||||
];
|
||||
destinations = [Gen5Operand.Vector(word & 0xFF)];
|
||||
if (opcode == "VReadlaneB32")
|
||||
{
|
||||
// VReadlaneB32 writes to scalar destination (bits 8-14), not vector.
|
||||
// Bits 0-7 are unused for this opcode.
|
||||
destinations = [Gen5Operand.Scalar((word >> 8) & 0x7F)];
|
||||
}
|
||||
var isVop3B = IsVop3BOpcode((word >> 16) & 0x3FF);
|
||||
control = new Gen5Vop3Control(
|
||||
isVop3B ? 0 : (word >> 8) & 0x7,
|
||||
+45
-74
@@ -1,11 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
public static partial class Gen5SpirvTranslator
|
||||
internal static partial class Gen5SpirvTranslator
|
||||
{
|
||||
private sealed partial class CompilationContext
|
||||
{
|
||||
@@ -24,11 +22,6 @@ public static partial class Gen5SpirvTranslator
|
||||
return TryEmitVectorCompare(instruction, out error);
|
||||
}
|
||||
|
||||
if (instruction.Opcode == "VReadlaneB32")
|
||||
{
|
||||
return TryEmitReadlane(instruction, out error);
|
||||
}
|
||||
|
||||
if (!TryGetVectorDestination(instruction, out var destination))
|
||||
{
|
||||
error = "missing vector destination";
|
||||
@@ -39,36 +32,10 @@ public static partial class Gen5SpirvTranslator
|
||||
switch (instruction.Opcode)
|
||||
{
|
||||
case "VMovB32":
|
||||
case "VReadlaneB32":
|
||||
case "VReadfirstlaneB32":
|
||||
result = GetRawSource(instruction, 0);
|
||||
break;
|
||||
case "VWritelaneB32":
|
||||
{
|
||||
// vdst[lane(src1)] = src0
|
||||
// Per-lane: if current lane == src1, write src0, else keep old value.
|
||||
var oldValue = LoadV(destination);
|
||||
var src0 = GetRawSource(instruction, 0);
|
||||
var laneSelect = GetRawSource(instruction, 1);
|
||||
var currentLane = _subgroupInvocationIdInput != 0
|
||||
? BitwiseAnd(
|
||||
Load(_uintType, _subgroupInvocationIdInput),
|
||||
UInt(RdnaWaveLaneCount - 1))
|
||||
: UInt(0);
|
||||
var isTargetLane = _module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
currentLane,
|
||||
laneSelect);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
isTargetLane,
|
||||
src0,
|
||||
oldValue);
|
||||
// Writelane writes to a specific lane regardless of exec mask.
|
||||
StoreV(destination, result, guardWithExec: false);
|
||||
return true;
|
||||
}
|
||||
case "VCndmaskB32":
|
||||
{
|
||||
var condition = instruction.Sources.Count > 2
|
||||
@@ -2369,42 +2336,6 @@ public static partial class Gen5SpirvTranslator
|
||||
StoreWaveMask(106, carry);
|
||||
}
|
||||
|
||||
private bool TryEmitReadlane(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (instruction.Destinations.Count == 0 ||
|
||||
instruction.Destinations[0].Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
error = "VReadlaneB32 expects scalar destination";
|
||||
return false;
|
||||
}
|
||||
|
||||
var destination = instruction.Destinations[0].Value;
|
||||
var src0 = GetRawSource(instruction, 0);
|
||||
|
||||
if (_subgroupInvocationIdInput != 0)
|
||||
{
|
||||
// sdst = vsrc0[lane(src1)] — broadcast from the specified lane.
|
||||
var laneSelect = GetRawSource(instruction, 1);
|
||||
var broadcast = _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformBroadcast,
|
||||
_uintType,
|
||||
UInt(3), // Subgroup scope
|
||||
src0,
|
||||
laneSelect);
|
||||
StoreS(destination, broadcast);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: no subgroup ops, read current lane's value.
|
||||
StoreS(destination, src0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private uint EmitPermlane16(
|
||||
Gen5ShaderInstruction instruction,
|
||||
bool exchangeRows)
|
||||
@@ -2560,7 +2491,47 @@ public static partial class Gen5SpirvTranslator
|
||||
resultSignChanged);
|
||||
}
|
||||
|
||||
private static bool TryDecodeInlineConstant(uint encoded, out uint value) =>
|
||||
Gen5InlineConstants.TryDecode(encoded, out value);
|
||||
private static bool TryDecodeInlineConstant(uint encoded, out uint value)
|
||||
{
|
||||
if (encoded == 125)
|
||||
{
|
||||
value = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (encoded is >= 128 and <= 192)
|
||||
{
|
||||
value = encoded - 128;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (encoded is >= 193 and <= 208)
|
||||
{
|
||||
value = unchecked((uint)-(int)(encoded - 192));
|
||||
return true;
|
||||
}
|
||||
|
||||
var floatingPoint = encoded switch
|
||||
{
|
||||
240 => 0.5f,
|
||||
241 => -0.5f,
|
||||
242 => 1.0f,
|
||||
243 => -1.0f,
|
||||
244 => 2.0f,
|
||||
245 => -2.0f,
|
||||
246 => 4.0f,
|
||||
247 => -4.0f,
|
||||
248 => 1.0f / (2.0f * MathF.PI),
|
||||
_ => float.NaN,
|
||||
};
|
||||
if (float.IsNaN(floatingPoint))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BitConverter.SingleToUInt32Bits(floatingPoint);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -1,11 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
public static partial class Gen5SpirvTranslator
|
||||
internal static partial class Gen5SpirvTranslator
|
||||
{
|
||||
private const uint ScalarRegisterCount = 256;
|
||||
private const uint VectorRegisterCount = 512;
|
||||
@@ -2507,7 +2505,7 @@ public static partial class Gen5SpirvTranslator
|
||||
|
||||
private bool UsesSubgroupShuffle() =>
|
||||
_state.Program.Instructions.Any(instruction =>
|
||||
instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32" or "VReadlaneB32");
|
||||
instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32");
|
||||
|
||||
private bool UsesWaveControl() =>
|
||||
_state.Program.Instructions.Any(instruction =>
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public static class SpirvFixedShaders
|
||||
internal static class SpirvFixedShaders
|
||||
{
|
||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||
{
|
||||
+11
-11
@@ -4,9 +4,9 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public enum SpirvOp : ushort
|
||||
internal enum SpirvOp : ushort
|
||||
{
|
||||
Nop = 0,
|
||||
Name = 5,
|
||||
@@ -167,7 +167,7 @@ public enum SpirvOp : ushort
|
||||
GroupNonUniformShuffleDown = 348,
|
||||
}
|
||||
|
||||
public enum SpirvCapability : uint
|
||||
internal enum SpirvCapability : uint
|
||||
{
|
||||
Shader = 1,
|
||||
Float16 = 9,
|
||||
@@ -186,7 +186,7 @@ public enum SpirvCapability : uint
|
||||
RuntimeDescriptorArray = 5302,
|
||||
}
|
||||
|
||||
public enum SpirvStorageClass : uint
|
||||
internal enum SpirvStorageClass : uint
|
||||
{
|
||||
UniformConstant = 0,
|
||||
Input = 1,
|
||||
@@ -200,21 +200,21 @@ public enum SpirvStorageClass : uint
|
||||
StorageBuffer = 12,
|
||||
}
|
||||
|
||||
public enum SpirvExecutionModel : uint
|
||||
internal enum SpirvExecutionModel : uint
|
||||
{
|
||||
Vertex = 0,
|
||||
Fragment = 4,
|
||||
GLCompute = 5,
|
||||
}
|
||||
|
||||
public enum SpirvExecutionMode : uint
|
||||
internal enum SpirvExecutionMode : uint
|
||||
{
|
||||
OriginUpperLeft = 7,
|
||||
DepthReplacing = 12,
|
||||
LocalSize = 17,
|
||||
}
|
||||
|
||||
public enum SpirvDecoration : uint
|
||||
internal enum SpirvDecoration : uint
|
||||
{
|
||||
Block = 2,
|
||||
ArrayStride = 6,
|
||||
@@ -227,7 +227,7 @@ public enum SpirvDecoration : uint
|
||||
Offset = 35,
|
||||
}
|
||||
|
||||
public enum SpirvBuiltIn : uint
|
||||
internal enum SpirvBuiltIn : uint
|
||||
{
|
||||
Position = 0,
|
||||
VertexIndex = 42,
|
||||
@@ -241,7 +241,7 @@ public enum SpirvBuiltIn : uint
|
||||
SubgroupLocalInvocationId = 41,
|
||||
}
|
||||
|
||||
public enum SpirvImageDim : uint
|
||||
internal enum SpirvImageDim : uint
|
||||
{
|
||||
Dim1D = 0,
|
||||
Dim2D = 1,
|
||||
@@ -250,7 +250,7 @@ public enum SpirvImageDim : uint
|
||||
Buffer = 5,
|
||||
}
|
||||
|
||||
public enum SpirvImageFormat : uint
|
||||
internal enum SpirvImageFormat : uint
|
||||
{
|
||||
Unknown = 0,
|
||||
Rgba32f = 1,
|
||||
@@ -294,7 +294,7 @@ public enum SpirvImageFormat : uint
|
||||
R8ui = 39,
|
||||
}
|
||||
|
||||
public sealed class SpirvModuleBuilder
|
||||
internal sealed class SpirvModuleBuilder
|
||||
{
|
||||
private const uint Magic = 0x07230203;
|
||||
private const uint Version15 = 0x00010500;
|
||||
@@ -48,25 +48,19 @@ public static class DiscMapExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "fJgP+wqifno",
|
||||
ExportName = "sceDiscMapUnknownFJgP",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "ioKMruft1ek",
|
||||
ExportName = "sceDiscMapUnknownIoKM",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
private static int WriteMappingTriple(CpuContext ctx, string exportName)
|
||||
{
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
|
||||
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
|
||||
/// Vulkan is the only backend today; Metal/DX12 slot in here.
|
||||
/// </summary>
|
||||
internal static class GuestGpu
|
||||
{
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||
|
||||
public static IGuestGpuBackend Current => Instance.Value;
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
// The types that cross the guest-GPU backend seam. Every field is either a neutral
|
||||
// primitive (dimensions, counts, host pixel bytes) or a raw guest/AGC value (guest
|
||||
// addresses, guest format and number-type codes, guest register bitfields). Host
|
||||
// graphics-API values must never appear here: each backend owns the guest -> native
|
||||
// translation for its API.
|
||||
|
||||
/// <summary>A guest texture referenced by a draw or dispatch. Format/NumberType/
|
||||
/// TileMode/DstSelect are raw guest descriptor codes.</summary>
|
||||
internal sealed record GuestDrawTexture(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Format,
|
||||
uint NumberType,
|
||||
byte[] RgbaPixels,
|
||||
bool IsFallback,
|
||||
bool IsStorage,
|
||||
uint MipLevels = 1,
|
||||
uint MipLevel = 0,
|
||||
uint Pitch = 0,
|
||||
uint TileMode = 0,
|
||||
uint DstSelect = 0xFAC,
|
||||
GuestSampler Sampler = default);
|
||||
|
||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||
internal readonly record struct GuestSampler(
|
||||
uint Word0,
|
||||
uint Word1,
|
||||
uint Word2,
|
||||
uint Word3);
|
||||
|
||||
internal sealed record GuestMemoryBuffer(
|
||||
ulong BaseAddress,
|
||||
byte[] Data);
|
||||
|
||||
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
|
||||
internal sealed record GuestVertexBuffer(
|
||||
uint Location,
|
||||
uint ComponentCount,
|
||||
uint DataFormat,
|
||||
uint NumberFormat,
|
||||
ulong BaseAddress,
|
||||
uint Stride,
|
||||
uint OffsetBytes,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record GuestIndexBuffer(
|
||||
byte[] Data,
|
||||
bool Is32Bit);
|
||||
|
||||
internal readonly record struct GuestRect(
|
||||
int X,
|
||||
int Y,
|
||||
uint Width,
|
||||
uint Height);
|
||||
|
||||
internal readonly record struct GuestViewport(
|
||||
float X,
|
||||
float Y,
|
||||
float Width,
|
||||
float Height,
|
||||
float MinDepth,
|
||||
float MaxDepth);
|
||||
|
||||
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
||||
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
|
||||
internal readonly record struct GuestBlendState(
|
||||
bool Enable,
|
||||
uint ColorSrcFactor,
|
||||
uint ColorDstFactor,
|
||||
uint ColorFunc,
|
||||
uint AlphaSrcFactor,
|
||||
uint AlphaDstFactor,
|
||||
uint AlphaFunc,
|
||||
bool SeparateAlphaBlend,
|
||||
uint WriteMask)
|
||||
{
|
||||
public static GuestBlendState Default { get; } = new(
|
||||
Enable: false,
|
||||
ColorSrcFactor: 1,
|
||||
ColorDstFactor: 0,
|
||||
ColorFunc: 0,
|
||||
AlphaSrcFactor: 1,
|
||||
AlphaDstFactor: 0,
|
||||
AlphaFunc: 0,
|
||||
SeparateAlphaBlend: false,
|
||||
WriteMask: 0xFu);
|
||||
}
|
||||
|
||||
internal sealed record GuestRenderState(
|
||||
IReadOnlyList<GuestBlendState> Blends,
|
||||
GuestRect? Scissor,
|
||||
GuestViewport? Viewport)
|
||||
{
|
||||
public static GuestRenderState Default { get; } = new(
|
||||
[GuestBlendState.Default],
|
||||
Scissor: null,
|
||||
Viewport: null);
|
||||
|
||||
public GuestBlendState Blend =>
|
||||
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
|
||||
}
|
||||
|
||||
/// <summary>Format/NumberType are raw guest render-target register codes.</summary>
|
||||
internal sealed record GuestRenderTarget(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Format,
|
||||
uint NumberType,
|
||||
uint MipLevels = 1);
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// A guest shader compiled by a backend, opaque to the export layers: only the backend
|
||||
/// that produced it can submit it. <see cref="Payload"/> is the backend-defined compiled
|
||||
/// bytes (SPIR-V words for Vulkan; MSL/DXIL for future backends), exposed solely for
|
||||
/// diagnostics dumps and size traces — callers must never interpret it.
|
||||
/// </summary>
|
||||
internal interface IGuestCompiledShader
|
||||
{
|
||||
byte[] Payload { get; }
|
||||
|
||||
/// <summary>File extension for diagnostics dumps of <see cref="Payload"/> ("spv",
|
||||
/// "msl", ...), so dumps stay honestly labeled whatever the backend.</summary>
|
||||
string PayloadFileExtension { get; }
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// The guest-GPU backend seam: everything the AGC/VideoOut export layers need from a
|
||||
/// host renderer, expressed in guest-domain terms so Vulkan, Metal, and DX12 backends
|
||||
/// can each translate to their native API. Two rules keep it that way: no host-API
|
||||
/// value (formats, blend enums, barrier or pass concepts) may cross this interface,
|
||||
/// and submission is coarse-grained — synchronization is a backend-internal concern.
|
||||
///
|
||||
/// Shader compilation also lives behind the seam: the backend owns its codegen and
|
||||
/// returns opaque <see cref="IGuestCompiledShader"/> handles that only it can submit.
|
||||
/// </summary>
|
||||
internal interface IGuestGpuBackend
|
||||
{
|
||||
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
|
||||
void EnsureStarted(uint width, uint height);
|
||||
|
||||
// Shader compilation. The optional base/index parameters describe how a multi-stage
|
||||
// draw lays both stages' resources into one flat per-role slot space (buffers,
|
||||
// images, scalar-spill slots); each backend maps those slots to its own API binding
|
||||
// model. -1 keeps the emitter's single-stage defaults.
|
||||
|
||||
bool TryCompileVertexShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1);
|
||||
|
||||
bool TryCompilePixelShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1);
|
||||
|
||||
bool TryCompileComputeShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX,
|
||||
uint localSizeY,
|
||||
uint localSizeZ,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error);
|
||||
|
||||
void HideSplashScreen();
|
||||
|
||||
/// <summary>Presents one CPU-produced BGRA frame.</summary>
|
||||
void Submit(byte[] bgraFrame, uint width, uint height);
|
||||
|
||||
/// <summary>Presents a recognized fixed-function guest draw (see GuestDrawKind).</summary>
|
||||
void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height);
|
||||
|
||||
void SubmitTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null);
|
||||
|
||||
void SubmitOffscreenTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null);
|
||||
|
||||
void SubmitStorageTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
uint width,
|
||||
uint height);
|
||||
|
||||
void SubmitComputeDispatch(
|
||||
ulong shaderAddress,
|
||||
IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX,
|
||||
uint groupCountY,
|
||||
uint groupCountZ);
|
||||
|
||||
bool TrySubmitGuestImage(
|
||||
ulong address,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitchInPixel);
|
||||
|
||||
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
||||
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
||||
|
||||
/// <summary>Format/numberType are raw guest texture descriptor codes.</summary>
|
||||
bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType);
|
||||
|
||||
bool TrySubmitGuestImageBlit(
|
||||
ulong sourceAddress,
|
||||
uint sourceWidth,
|
||||
uint sourceHeight,
|
||||
uint sourceFormat,
|
||||
ulong destinationAddress,
|
||||
uint destinationWidth,
|
||||
uint destinationHeight,
|
||||
uint destinationFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the backend supports the guest render-target format, and how its pixel
|
||||
/// outputs are typed. Deliberately does not expose the backend's native format —
|
||||
/// the guest codes cross the seam and each backend maps them internally.
|
||||
/// </summary>
|
||||
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
/// <summary>The Vulkan backend's compiled shader: raw SPIR-V words.</summary>
|
||||
internal sealed record VulkanCompiledGuestShader(byte[] Spirv) : IGuestCompiledShader
|
||||
{
|
||||
public byte[] Payload => Spirv;
|
||||
|
||||
public string PayloadFileExtension => "spv";
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
/// <summary>
|
||||
/// Vulkan backend for the guest-GPU seam: SPIR-V codegen via
|
||||
/// SharpEmu.ShaderCompiler.Vulkan, rendering via a thin adapter over the existing
|
||||
/// VulkanVideoPresenter statics (folding the presenter into an instance type is
|
||||
/// follow-up work, not a seam concern).
|
||||
/// </summary>
|
||||
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||
{
|
||||
public bool TryCompileVertexShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||
state,
|
||||
evaluation,
|
||||
out var compiled,
|
||||
out error,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCompilePixelShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
state,
|
||||
evaluation,
|
||||
outputs,
|
||||
out var compiled,
|
||||
out error,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCompileComputeShader(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX,
|
||||
uint localSizeY,
|
||||
uint localSizeZ,
|
||||
out IGuestCompiledShader? shader,
|
||||
out string error)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
localSizeX,
|
||||
localSizeY,
|
||||
localSizeZ,
|
||||
out var compiled,
|
||||
out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void EnsureStarted(uint width, uint height) =>
|
||||
VulkanVideoPresenter.EnsureStarted(width, height);
|
||||
|
||||
public void HideSplashScreen() =>
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
|
||||
public void Submit(byte[] bgraFrame, uint width, uint height) =>
|
||||
VulkanVideoPresenter.Submit(bgraFrame, width, height);
|
||||
|
||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) =>
|
||||
VulkanVideoPresenter.SubmitGuestDraw(drawKind, width, height);
|
||||
|
||||
public void SubmitTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null) =>
|
||||
VulkanVideoPresenter.SubmitTranslatedDraw(
|
||||
Spirv(pixelShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
width,
|
||||
height,
|
||||
attributeCount,
|
||||
vertexShader is null ? null : Spirv(vertexShader),
|
||||
vertexCount,
|
||||
instanceCount,
|
||||
primitiveType,
|
||||
indexBuffer,
|
||||
vertexBuffers,
|
||||
renderState);
|
||||
|
||||
public void SubmitOffscreenTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null) =>
|
||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||
Spirv(pixelShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
attributeCount,
|
||||
targets,
|
||||
vertexShader is null ? null : Spirv(vertexShader),
|
||||
vertexCount,
|
||||
instanceCount,
|
||||
primitiveType,
|
||||
indexBuffer,
|
||||
vertexBuffers,
|
||||
renderState);
|
||||
|
||||
public void SubmitStorageTranslatedDraw(
|
||||
IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
uint width,
|
||||
uint height) =>
|
||||
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
||||
Spirv(pixelShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
attributeCount,
|
||||
width,
|
||||
height);
|
||||
|
||||
public void SubmitComputeDispatch(
|
||||
ulong shaderAddress,
|
||||
IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX,
|
||||
uint groupCountY,
|
||||
uint groupCountZ) =>
|
||||
VulkanVideoPresenter.SubmitComputeDispatch(
|
||||
shaderAddress,
|
||||
Spirv(computeShader),
|
||||
textures,
|
||||
globalMemoryBuffers,
|
||||
groupCountX,
|
||||
groupCountY,
|
||||
groupCountZ);
|
||||
|
||||
public bool TrySubmitGuestImage(
|
||||
ulong address,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitchInPixel) =>
|
||||
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
|
||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) =>
|
||||
VulkanVideoPresenter.IsGpuGuestImageAvailable(address, format, numberType);
|
||||
|
||||
public bool TrySubmitGuestImageBlit(
|
||||
ulong sourceAddress,
|
||||
uint sourceWidth,
|
||||
uint sourceHeight,
|
||||
uint sourceFormat,
|
||||
ulong destinationAddress,
|
||||
uint destinationWidth,
|
||||
uint destinationHeight,
|
||||
uint destinationFormat) =>
|
||||
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
||||
sourceAddress,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
sourceFormat,
|
||||
destinationAddress,
|
||||
destinationWidth,
|
||||
destinationHeight,
|
||||
destinationFormat);
|
||||
|
||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
||||
{
|
||||
if (VulkanVideoPresenter.TryDecodeRenderTargetFormat(dataFormat, numberType, out var format))
|
||||
{
|
||||
outputKind = format.OutputKind;
|
||||
return true;
|
||||
}
|
||||
|
||||
outputKind = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static byte[] Spirv(IGuestCompiledShader shader) =>
|
||||
shader is VulkanCompiledGuestShader vulkanShader
|
||||
? vulkanShader.Spirv
|
||||
: throw new InvalidOperationException(
|
||||
$"shader handle of type {shader.GetType().Name} was not compiled by the Vulkan backend");
|
||||
}
|
||||
@@ -589,66 +589,6 @@ public static class KernelEventQueueCompatExports
|
||||
return triggeredCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers every registered event on every queue that matches <paramref name="filter"/>
|
||||
/// regardless of the registration's <c>ident</c>. This is a workaround for PS5 AGC command
|
||||
/// buffers, where <c>IT_EVENT_WRITE</c> carries a hardware <c>EVENT_TYPE</c> that does not
|
||||
/// match the <c>eventId</c> the guest registered with <c>sceAgcDriverAddEqEvent</c>.
|
||||
/// See issue #173.
|
||||
/// </summary>
|
||||
public static int TriggerRegisteredEventsByFilter(
|
||||
short filter,
|
||||
ulong data)
|
||||
{
|
||||
List<ulong>? wakeHandles = null;
|
||||
var triggeredCount = 0;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
foreach (var (handle, registrations) in _registeredEvents)
|
||||
{
|
||||
foreach (var registration in registrations.Values)
|
||||
{
|
||||
if (registration.Filter != filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
QueueOrUpdateEvent(
|
||||
queue,
|
||||
new KernelQueuedEvent(
|
||||
registration.Ident,
|
||||
registration.Filter,
|
||||
0,
|
||||
1,
|
||||
data,
|
||||
registration.UserData));
|
||||
(wakeHandles ??= new List<ulong>()).Add(handle);
|
||||
triggeredCount++;
|
||||
|
||||
// A single queue only needs to be woken once, even if multiple
|
||||
// registrations matched.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wakeHandles is not null)
|
||||
{
|
||||
foreach (var handle in wakeHandles)
|
||||
{
|
||||
WakeEventQueue(handle);
|
||||
}
|
||||
}
|
||||
|
||||
return triggeredCount;
|
||||
}
|
||||
|
||||
private static bool TriggerRegisteredEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
|
||||
@@ -1816,8 +1816,6 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
// Not migratable to [GuestCString]: the local reader's TryReadCompat host-memory
|
||||
// fallback recovers paths in loader-mapped regions that ctx.Memory cannot see.
|
||||
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
|
||||
@@ -99,6 +99,20 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
// See PthreadMutexState.WakeKey.
|
||||
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
|
||||
|
||||
// A signal with no waiter stays pending; the guest often signals before the wait.
|
||||
public int PendingSignals { get; set; }
|
||||
|
||||
public bool TryConsumePendingSignal()
|
||||
{
|
||||
if (PendingSignals <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingSignals--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||
@@ -1361,8 +1375,9 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
state.Waiters++;
|
||||
var observedEpoch = state.SignalEpoch;
|
||||
var consumedPendingSignal = state.TryConsumePendingSignal();
|
||||
TracePthreadCond(
|
||||
"wait-enter",
|
||||
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
|
||||
condAddress,
|
||||
mutexAddress,
|
||||
state,
|
||||
@@ -1377,6 +1392,25 @@ public static class KernelPthreadCompatExports
|
||||
return unlockResult;
|
||||
}
|
||||
|
||||
if (consumedPendingSignal)
|
||||
{
|
||||
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||
|
||||
// Relock outside SyncRoot to preserve lock ordering.
|
||||
Monitor.Exit(state.SyncRoot);
|
||||
try
|
||||
{
|
||||
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Balance the surrounding lock statement.
|
||||
Monitor.Enter(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
@@ -1508,6 +1542,10 @@ public static class KernelPthreadCompatExports
|
||||
Monitor.Pulse(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.PendingSignals++;
|
||||
}
|
||||
|
||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -1717,59 +1717,41 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
private static ulong ResolveKernelTscFrequency()
|
||||
{
|
||||
var (frequencyHz, source) = SelectKernelTscFrequency(
|
||||
_rdtscReader is not null,
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TSC_FREQ_HZ"),
|
||||
TryCalibrateHostTscFrequency,
|
||||
TryResolveCpuidTscFrequency,
|
||||
Stopwatch.Frequency);
|
||||
TraceKernelTscFrequency(source, frequencyHz);
|
||||
return frequencyHz;
|
||||
}
|
||||
|
||||
internal delegate bool TryGetFrequency(out ulong frequencyHz);
|
||||
|
||||
// sceKernelReadTsc only returns the CPU's RDTSC when the host RDTSC reader is available
|
||||
// (currently 64-bit Windows); otherwise it falls back to the QPC-based Stopwatch. The
|
||||
// calibrated and CPUID frequencies both describe RDTSC, so reporting either while ReadTsc is
|
||||
// actually returning Stopwatch ticks makes sceKernelGetTscFrequency disagree with the counter,
|
||||
// and a guest computing elapsed = readTscDelta / frequency gets the wrong time on Linux and
|
||||
// macOS. Gate those on rdtscAvailable; otherwise report Stopwatch's own frequency so the pair
|
||||
// stays consistent.
|
||||
internal static (ulong FrequencyHz, string Source) SelectKernelTscFrequency(
|
||||
bool rdtscAvailable,
|
||||
string? overrideHzText,
|
||||
TryGetFrequency tryCalibrate,
|
||||
TryGetFrequency tryResolveCpuid,
|
||||
long stopwatchFrequency)
|
||||
{
|
||||
const ulong minSane = 1_000_000UL;
|
||||
|
||||
var overrideHzText = Environment.GetEnvironmentVariable("SHARPEMU_TSC_FREQ_HZ");
|
||||
if (!string.IsNullOrWhiteSpace(overrideHzText) &&
|
||||
ulong.TryParse(overrideHzText, out var overrideHz) &&
|
||||
overrideHz >= minSane)
|
||||
{
|
||||
return (overrideHz, "env");
|
||||
TraceKernelTscFrequency("env", overrideHz);
|
||||
return overrideHz;
|
||||
}
|
||||
|
||||
if (rdtscAvailable)
|
||||
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
|
||||
{
|
||||
if (tryCalibrate(out ulong calibratedHz) && calibratedHz >= minSane)
|
||||
{
|
||||
return (calibratedHz, "calibrated-rdtsc");
|
||||
}
|
||||
|
||||
if (tryResolveCpuid(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||
{
|
||||
return (cpuidHz, "cpuid");
|
||||
}
|
||||
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
|
||||
return calibratedHz;
|
||||
}
|
||||
|
||||
var hostQpc = stopwatchFrequency > 0
|
||||
? unchecked((ulong)stopwatchFrequency)
|
||||
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("cpuid", cpuidHz);
|
||||
return cpuidHz;
|
||||
}
|
||||
|
||||
var hostQpc = Stopwatch.Frequency > 0
|
||||
? unchecked((ulong)Stopwatch.Frequency)
|
||||
: DefaultKernelTscFrequency;
|
||||
return hostQpc >= minSane ? (hostQpc, "qpc") : (DefaultKernelTscFrequency, "default");
|
||||
if (hostQpc >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("qpc", hostQpc);
|
||||
return hostQpc;
|
||||
}
|
||||
|
||||
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
|
||||
return DefaultKernelTscFrequency;
|
||||
}
|
||||
|
||||
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
|
||||
|
||||
@@ -259,8 +259,11 @@ public static class KernelSemaphoreCompatExports
|
||||
ExportName = "sceKernelPollSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelPollSema(CpuContext ctx, uint handle, int needCount)
|
||||
public static int KernelPollSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
@@ -296,8 +299,11 @@ public static class KernelSemaphoreCompatExports
|
||||
ExportName = "sceKernelSignalSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSignalSema(CpuContext ctx, uint handle, int signalCount)
|
||||
public static int KernelSignalSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
@@ -335,8 +341,12 @@ public static class KernelSemaphoreCompatExports
|
||||
ExportName = "sceKernelCancelSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCancelSema(CpuContext ctx, uint handle, int setCount, ulong waitingThreadsAddress)
|
||||
public static int KernelCancelSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
|
||||
@@ -10,7 +10,6 @@ public static class NpManagerExports
|
||||
{
|
||||
private const int NpTitleIdSize = 16;
|
||||
private const int NpTitleSecretSize = 128;
|
||||
private const int NpReachabilityStateUnavailable = 0;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3Zl8BePTh9Y",
|
||||
@@ -76,15 +75,8 @@ public static class NpManagerExports
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||
{
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return ctx.TryWriteInt32(stateAddress, NpReachabilityStateUnavailable)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -84,12 +84,6 @@ public static class RtcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// TryConvertTickToDateTime yields a Utc-kind DateTime, but here the tick is a local
|
||||
// wall-clock time. ConvertTimeToUtc throws ArgumentException when a Utc-kind value is
|
||||
// paired with a non-UTC source zone, so on any host not set to UTC this would always
|
||||
// fail. Re-tag as Unspecified so the value is interpreted as local time and converted.
|
||||
localDateTime = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified);
|
||||
|
||||
DateTime utcDateTime;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -6,17 +6,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||
<!-- SysAbi export generator + analyzers (compile-time registry, NID validation). -->
|
||||
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The PS5 symbol catalog: lets the analyzer flag export names it doesn't know. -->
|
||||
<AdditionalFiles Include="..\..\scripts\ps5_names.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
@@ -94,7 +93,7 @@ public static class SystemServiceExports
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||
{
|
||||
GuestGpu.Current.HideSplashScreen();
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,8 +161,6 @@ public static class UserServiceExports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "D-CzAxQL0XI",
|
||||
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
||||
@@ -186,7 +184,6 @@ public static class UserServiceExports
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "woNpu+45RLk",
|
||||
|
||||
@@ -8,9 +8,6 @@ namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
internal static class PngSplashLoader
|
||||
{
|
||||
private const uint CrcPolynomial = 0xEDB88320;
|
||||
private static readonly uint[] CrcTable = BuildCrcTable();
|
||||
|
||||
private static ReadOnlySpan<byte> PngSignature =>
|
||||
[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
@@ -93,13 +90,6 @@ internal static class PngSplashLoader
|
||||
|
||||
var chunkType = png.Slice(offset + 4, 4);
|
||||
var chunkData = png.Slice(offset + 8, (int)chunkLength);
|
||||
var expectedCrc = BinaryPrimitives.ReadUInt32BigEndian(
|
||||
png.Slice(offset + 8 + (int)chunkLength, 4));
|
||||
if (CalculateCrc(chunkType, chunkData) != expectedCrc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chunkType.SequenceEqual("IHDR"u8))
|
||||
{
|
||||
if (chunkData.Length != 13)
|
||||
@@ -196,41 +186,6 @@ internal static class PngSplashLoader
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint CalculateCrc(ReadOnlySpan<byte> chunkType, ReadOnlySpan<byte> chunkData)
|
||||
{
|
||||
var crc = UpdateCrc(uint.MaxValue, chunkType);
|
||||
return ~UpdateCrc(crc, chunkData);
|
||||
}
|
||||
|
||||
private static uint UpdateCrc(uint crc, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
crc = CrcTable[(byte)(crc ^ value)] ^ (crc >> 8);
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
private static uint[] BuildCrcTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
for (var index = 0; index < table.Length; index++)
|
||||
{
|
||||
var value = (uint)index;
|
||||
for (var bit = 0; bit < 8; bit++)
|
||||
{
|
||||
value = (value & 1) != 0
|
||||
? CrcPolynomial ^ (value >> 1)
|
||||
: value >> 1;
|
||||
}
|
||||
|
||||
table[index] = value;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private static bool TryUnfilter(
|
||||
byte filter,
|
||||
ReadOnlySpan<byte> source,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Logging;
|
||||
@@ -813,7 +812,7 @@ public static class VideoOutExports
|
||||
bgraFrame[offset + 3] = rgbaFrame[offset + 3];
|
||||
}
|
||||
|
||||
GuestGpu.Current.Submit(bgraFrame, width, height);
|
||||
VulkanVideoPresenter.Submit(bgraFrame, width, height);
|
||||
}
|
||||
|
||||
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
|
||||
@@ -1204,7 +1203,7 @@ public static class VideoOutExports
|
||||
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
|
||||
{
|
||||
guestImageAddress = displayBuffer.Address;
|
||||
guestImageSubmitted = GuestGpu.Current.TrySubmitGuestImage(
|
||||
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
|
||||
displayBuffer.Address,
|
||||
displayBuffer.Width,
|
||||
displayBuffer.Height,
|
||||
@@ -1335,14 +1334,14 @@ public static class VideoOutExports
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
}
|
||||
GuestGpu.Current.EnsureStarted(attribute.Width, attribute.Height);
|
||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||
|
||||
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||
if (guestFormat != 0)
|
||||
{
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
GuestGpu.Current.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1574,7 +1573,7 @@ public static class VideoOutExports
|
||||
: 0u;
|
||||
|
||||
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||
// the backend keys its guest-image registry on (see VulkanVideoPresenter.
|
||||
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
|
||||
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
||||
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
|
||||
NormalizePixelFormat(pixelFormat) switch
|
||||
|
||||
@@ -7,9 +7,6 @@ using Silk.NET.Maths;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Silk.NET.Input;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
using Silk.NET.Vulkan.Extensions.KHR;
|
||||
using Silk.NET.Vulkan.Extensions.EXT;
|
||||
@@ -25,6 +22,111 @@ using VkSemaphore = Silk.NET.Vulkan.Semaphore;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
internal enum GuestDrawKind
|
||||
{
|
||||
None,
|
||||
FullscreenBarycentric,
|
||||
}
|
||||
|
||||
internal sealed record VulkanGuestDrawTexture(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Format,
|
||||
uint NumberType,
|
||||
byte[] RgbaPixels,
|
||||
bool IsFallback,
|
||||
bool IsStorage,
|
||||
uint MipLevels = 1,
|
||||
uint MipLevel = 0,
|
||||
uint Pitch = 0,
|
||||
uint TileMode = 0,
|
||||
uint DstSelect = 0xFAC,
|
||||
VulkanGuestSampler Sampler = default);
|
||||
|
||||
internal readonly record struct VulkanGuestSampler(
|
||||
uint Word0,
|
||||
uint Word1,
|
||||
uint Word2,
|
||||
uint Word3);
|
||||
|
||||
internal sealed record VulkanGuestMemoryBuffer(
|
||||
ulong BaseAddress,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record VulkanGuestVertexBuffer(
|
||||
uint Location,
|
||||
uint ComponentCount,
|
||||
uint DataFormat,
|
||||
uint NumberFormat,
|
||||
ulong BaseAddress,
|
||||
uint Stride,
|
||||
uint OffsetBytes,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record VulkanGuestIndexBuffer(
|
||||
byte[] Data,
|
||||
bool Is32Bit);
|
||||
|
||||
internal readonly record struct VulkanGuestRect(
|
||||
int X,
|
||||
int Y,
|
||||
uint Width,
|
||||
uint Height);
|
||||
|
||||
internal readonly record struct VulkanGuestViewport(
|
||||
float X,
|
||||
float Y,
|
||||
float Width,
|
||||
float Height,
|
||||
float MinDepth,
|
||||
float MaxDepth);
|
||||
|
||||
internal readonly record struct VulkanGuestBlendState(
|
||||
bool Enable,
|
||||
uint ColorSrcFactor,
|
||||
uint ColorDstFactor,
|
||||
uint ColorFunc,
|
||||
uint AlphaSrcFactor,
|
||||
uint AlphaDstFactor,
|
||||
uint AlphaFunc,
|
||||
bool SeparateAlphaBlend,
|
||||
uint WriteMask)
|
||||
{
|
||||
public static VulkanGuestBlendState Default { get; } = new(
|
||||
Enable: false,
|
||||
ColorSrcFactor: 1,
|
||||
ColorDstFactor: 0,
|
||||
ColorFunc: 0,
|
||||
AlphaSrcFactor: 1,
|
||||
AlphaDstFactor: 0,
|
||||
AlphaFunc: 0,
|
||||
SeparateAlphaBlend: false,
|
||||
WriteMask: 0xFu);
|
||||
}
|
||||
|
||||
internal sealed record VulkanGuestRenderState(
|
||||
IReadOnlyList<VulkanGuestBlendState> Blends,
|
||||
VulkanGuestRect? Scissor,
|
||||
VulkanGuestViewport? Viewport)
|
||||
{
|
||||
public static VulkanGuestRenderState Default { get; } = new(
|
||||
[VulkanGuestBlendState.Default],
|
||||
Scissor: null,
|
||||
Viewport: null);
|
||||
|
||||
public VulkanGuestBlendState Blend =>
|
||||
Blends.Count == 0 ? VulkanGuestBlendState.Default : Blends[0];
|
||||
}
|
||||
|
||||
internal sealed record VulkanGuestRenderTarget(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Format,
|
||||
uint NumberType,
|
||||
uint MipLevels = 1);
|
||||
|
||||
internal readonly record struct VulkanRenderTargetFormat(
|
||||
Format Format,
|
||||
Gen5PixelOutputKind OutputKind)
|
||||
@@ -35,26 +137,26 @@ internal readonly record struct VulkanRenderTargetFormat(
|
||||
internal sealed record VulkanTranslatedGuestDraw(
|
||||
byte[] VertexSpirv,
|
||||
byte[] PixelSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> Textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> GlobalMemoryBuffers,
|
||||
IReadOnlyList<GuestVertexBuffer> VertexBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> Textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> GlobalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestVertexBuffer> VertexBuffers,
|
||||
uint AttributeCount,
|
||||
uint VertexCount,
|
||||
uint InstanceCount,
|
||||
uint PrimitiveType,
|
||||
GuestIndexBuffer? IndexBuffer,
|
||||
GuestRenderState RenderState);
|
||||
VulkanGuestIndexBuffer? IndexBuffer,
|
||||
VulkanGuestRenderState RenderState);
|
||||
|
||||
internal sealed record VulkanOffscreenGuestDraw(
|
||||
VulkanTranslatedGuestDraw Draw,
|
||||
IReadOnlyList<GuestRenderTarget> Targets,
|
||||
IReadOnlyList<VulkanGuestRenderTarget> Targets,
|
||||
bool PublishTarget);
|
||||
|
||||
internal sealed record VulkanComputeGuestDispatch(
|
||||
ulong ShaderAddress,
|
||||
byte[] ComputeSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> Textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> GlobalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> Textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> GlobalMemoryBuffers,
|
||||
uint GroupCountX,
|
||||
uint GroupCountY,
|
||||
uint GroupCountZ);
|
||||
@@ -305,8 +407,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
public static void SubmitTranslatedDraw(
|
||||
byte[] pixelSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
@@ -314,9 +416,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null)
|
||||
VulkanGuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
|
||||
VulkanGuestRenderState? renderState = null)
|
||||
{
|
||||
if (pixelSpirv.Length == 0 || width == 0 || height == 0)
|
||||
{
|
||||
@@ -354,7 +456,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
instanceCount,
|
||||
primitiveType,
|
||||
indexBuffer,
|
||||
renderState ?? GuestRenderState.Default),
|
||||
renderState ?? VulkanGuestRenderState.Default),
|
||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||
IsSplash: false);
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
@@ -371,17 +473,17 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
public static void SubmitOffscreenTranslatedDraw(
|
||||
byte[] pixelSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
GuestRenderTarget target,
|
||||
VulkanGuestRenderTarget target,
|
||||
byte[]? vertexSpirv = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null)
|
||||
VulkanGuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
|
||||
VulkanGuestRenderState? renderState = null)
|
||||
{
|
||||
SubmitOffscreenTranslatedDraw(
|
||||
pixelSpirv,
|
||||
@@ -400,17 +502,17 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
public static void SubmitOffscreenTranslatedDraw(
|
||||
byte[] pixelSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
IReadOnlyList<VulkanGuestRenderTarget> targets,
|
||||
byte[]? vertexSpirv = null,
|
||||
uint vertexCount = 3,
|
||||
uint instanceCount = 1,
|
||||
uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null)
|
||||
VulkanGuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
|
||||
VulkanGuestRenderState? renderState = null)
|
||||
{
|
||||
if (pixelSpirv.Length == 0 ||
|
||||
targets.Count == 0 ||
|
||||
@@ -439,7 +541,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"{firstTarget.Width}x{firstTarget.Height} textures={textures.Count}");
|
||||
}
|
||||
|
||||
var effectiveRenderState = renderState ?? GuestRenderState.Default;
|
||||
var effectiveRenderState = renderState ?? VulkanGuestRenderState.Default;
|
||||
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
|
||||
{
|
||||
effectiveRenderState = effectiveRenderState with
|
||||
@@ -487,8 +589,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
public static void SubmitStorageTranslatedDraw(
|
||||
byte[] pixelSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount,
|
||||
uint width,
|
||||
uint height)
|
||||
@@ -521,8 +623,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
1,
|
||||
4,
|
||||
null,
|
||||
GuestRenderState.Default),
|
||||
[new GuestRenderTarget(
|
||||
VulkanGuestRenderState.Default),
|
||||
[new VulkanGuestRenderTarget(
|
||||
Address: 0,
|
||||
width,
|
||||
height,
|
||||
@@ -535,8 +637,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public static void SubmitComputeDispatch(
|
||||
ulong shaderAddress,
|
||||
byte[] computeSpirv,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
IReadOnlyList<VulkanGuestDrawTexture> textures,
|
||||
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX,
|
||||
uint groupCountY,
|
||||
uint groupCountZ)
|
||||
@@ -716,7 +818,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
SubmitOffscreenTranslatedDraw(
|
||||
fragmentSpirv,
|
||||
[
|
||||
new GuestDrawTexture(
|
||||
new VulkanGuestDrawTexture(
|
||||
sourceAddress,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
@@ -728,7 +830,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
],
|
||||
[],
|
||||
attributeCount: 1,
|
||||
new GuestRenderTarget(
|
||||
new VulkanGuestRenderTarget(
|
||||
destinationAddress,
|
||||
destinationWidth,
|
||||
destinationHeight,
|
||||
@@ -1261,7 +1363,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private readonly Dictionary<byte[], Pipeline> _computePipelines =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<GraphicsPipelineKey, Pipeline> _graphicsPipelines = new();
|
||||
private readonly Dictionary<GuestSampler, Sampler> _samplers = new();
|
||||
private readonly Dictionary<VulkanGuestSampler, Sampler> _samplers = new();
|
||||
private readonly Dictionary<byte[], string> _shaderDigests =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<DescriptorLayoutKey, DescriptorLayoutBundle>
|
||||
@@ -1316,9 +1418,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public uint VertexCount = 3;
|
||||
public uint InstanceCount = 1;
|
||||
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
|
||||
public GuestBlendState[] Blends = [GuestBlendState.Default];
|
||||
public GuestRect? Scissor;
|
||||
public GuestViewport? Viewport;
|
||||
public VulkanGuestBlendState[] Blends = [VulkanGuestBlendState.Default];
|
||||
public VulkanGuestRect? Scissor;
|
||||
public VulkanGuestViewport? Viewport;
|
||||
public RenderPass TransientRenderPass;
|
||||
public Framebuffer TransientFramebuffer;
|
||||
}
|
||||
@@ -1338,7 +1440,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public bool NeedsUpload;
|
||||
public bool OwnsStorage;
|
||||
public bool IsStorage;
|
||||
public GuestSampler SamplerState;
|
||||
public VulkanGuestSampler SamplerState;
|
||||
public Sampler Sampler;
|
||||
public GuestImageResource? GuestImage;
|
||||
public ulong CpuContentFingerprint;
|
||||
@@ -1638,11 +1740,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}";
|
||||
}
|
||||
|
||||
private static string GuestImageDebugName(GuestRenderTarget target, Format format) =>
|
||||
private static string GuestImageDebugName(VulkanGuestRenderTarget target, Format format) =>
|
||||
$"SharpEmu guest 0x{target.Address:X16} {target.Width}x{target.Height} " +
|
||||
$"fmt{target.Format}/{format}";
|
||||
|
||||
private static string TextureDebugName(GuestDrawTexture texture, Format format) =>
|
||||
private static string TextureDebugName(VulkanGuestDrawTexture texture, Format format) =>
|
||||
$"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " +
|
||||
$"fmt{texture.Format}/{format}";
|
||||
|
||||
@@ -3519,7 +3621,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private TextureResource ResolveTextureResource(GuestDrawTexture texture)
|
||||
private TextureResource ResolveTextureResource(VulkanGuestDrawTexture texture)
|
||||
{
|
||||
if (texture.IsStorage)
|
||||
{
|
||||
@@ -3593,7 +3695,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
private bool TryCreateCpuTextureRefreshResource(
|
||||
GuestDrawTexture texture,
|
||||
VulkanGuestDrawTexture texture,
|
||||
GuestImageResource guestImage,
|
||||
ImageView view,
|
||||
out TextureResource resource)
|
||||
@@ -3658,7 +3760,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
private static bool IsCompatibleGuestImageAlias(
|
||||
GuestDrawTexture texture,
|
||||
VulkanGuestDrawTexture texture,
|
||||
GuestImageResource guestImage)
|
||||
{
|
||||
if (guestImage.Width == texture.Width &&
|
||||
@@ -3679,7 +3781,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private TextureResource ResolveStorageImageResource(GuestDrawTexture texture)
|
||||
private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture)
|
||||
{
|
||||
if (texture.Address == 0)
|
||||
{
|
||||
@@ -3756,7 +3858,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return resource;
|
||||
}
|
||||
|
||||
private TextureResource CreateStorageScratchResource(GuestDrawTexture texture)
|
||||
private TextureResource CreateStorageScratchResource(VulkanGuestDrawTexture texture)
|
||||
{
|
||||
var width = Math.Max(texture.Width, 1);
|
||||
var height = Math.Max(texture.Height, 1);
|
||||
@@ -3846,7 +3948,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
}
|
||||
|
||||
private GuestImageResource ResolveStorageGuestImage(GuestDrawTexture texture)
|
||||
private GuestImageResource ResolveStorageGuestImage(VulkanGuestDrawTexture texture)
|
||||
{
|
||||
if (texture.Address == 0)
|
||||
{
|
||||
@@ -3855,7 +3957,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
var format = GetTextureFormat(texture.Format, texture.NumberType);
|
||||
var guestImage = GetOrCreateGuestImage(
|
||||
new GuestRenderTarget(
|
||||
new VulkanGuestRenderTarget(
|
||||
texture.Address,
|
||||
texture.Width,
|
||||
texture.Height,
|
||||
@@ -3872,7 +3974,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return guestImage;
|
||||
}
|
||||
|
||||
private TextureResource CreateTextureResource(GuestDrawTexture texture)
|
||||
private TextureResource CreateTextureResource(VulkanGuestDrawTexture texture)
|
||||
{
|
||||
var width = Math.Max(texture.Width, 1);
|
||||
var height = Math.Max(texture.Height, 1);
|
||||
@@ -4070,7 +4172,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
private void DumpTextureUpload(
|
||||
GuestDrawTexture texture,
|
||||
VulkanGuestDrawTexture texture,
|
||||
byte[] pixels,
|
||||
uint rowLength,
|
||||
uint width,
|
||||
@@ -4162,7 +4264,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static void WriteInt32(byte[] output, int offset, int value) =>
|
||||
WriteUInt32(output, offset, unchecked((uint)value));
|
||||
|
||||
private Sampler CreateSampler(GuestSampler sampler)
|
||||
private Sampler CreateSampler(VulkanGuestSampler sampler)
|
||||
{
|
||||
if (_samplers.TryGetValue(sampler, out var cachedSampler))
|
||||
{
|
||||
@@ -4240,7 +4342,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
private GlobalBufferResource CreateGlobalBufferResource(
|
||||
GuestMemoryBuffer guestBuffer)
|
||||
VulkanGuestMemoryBuffer guestBuffer)
|
||||
{
|
||||
var buffer = CreateHostBuffer(
|
||||
guestBuffer.Data,
|
||||
@@ -4269,7 +4371,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
private VertexBufferResource CreateVertexBufferResource(
|
||||
GuestVertexBuffer guestBuffer)
|
||||
VulkanGuestVertexBuffer guestBuffer)
|
||||
{
|
||||
var buffer = CreateHostBuffer(
|
||||
guestBuffer.Data,
|
||||
@@ -4492,7 +4594,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static uint GetDrawVertexCount(
|
||||
uint primitiveType,
|
||||
uint vertexCount,
|
||||
GuestIndexBuffer? indexBuffer)
|
||||
VulkanGuestIndexBuffer? indexBuffer)
|
||||
{
|
||||
if (primitiveType == GuestPrimitiveRectList && indexBuffer is null)
|
||||
{
|
||||
@@ -4538,41 +4640,41 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_ => BlendOp.Add,
|
||||
};
|
||||
|
||||
private static uint DecodeSamplerClampX(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerClampX(VulkanGuestSampler sampler) =>
|
||||
sampler.Word0 & 0x7u;
|
||||
|
||||
private static uint DecodeSamplerClampY(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerClampY(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word0 >> 3) & 0x7u;
|
||||
|
||||
private static uint DecodeSamplerClampZ(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerClampZ(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word0 >> 6) & 0x7u;
|
||||
|
||||
private static uint DecodeSamplerDepthCompare(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerDepthCompare(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word0 >> 12) & 0x7u;
|
||||
|
||||
private static float DecodeSamplerMinLod(GuestSampler sampler) =>
|
||||
private static float DecodeSamplerMinLod(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word1 & 0xFFFu) / 256.0f;
|
||||
|
||||
private static float DecodeSamplerMaxLod(GuestSampler sampler) =>
|
||||
private static float DecodeSamplerMaxLod(VulkanGuestSampler sampler) =>
|
||||
((sampler.Word1 >> 12) & 0xFFFu) / 256.0f;
|
||||
|
||||
private static float DecodeSamplerLodBias(GuestSampler sampler)
|
||||
private static float DecodeSamplerLodBias(VulkanGuestSampler sampler)
|
||||
{
|
||||
var raw = sampler.Word2 & 0x3FFFu;
|
||||
var signed = (short)((raw ^ 0x2000u) - 0x2000u);
|
||||
return signed / 256.0f;
|
||||
}
|
||||
|
||||
private static uint DecodeSamplerMagFilter(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerMagFilter(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word2 >> 20) & 0x3u;
|
||||
|
||||
private static uint DecodeSamplerMinFilter(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerMinFilter(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word2 >> 22) & 0x3u;
|
||||
|
||||
private static uint DecodeSamplerMipFilter(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerMipFilter(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word2 >> 26) & 0x3u;
|
||||
|
||||
private static uint DecodeSamplerBorderColor(GuestSampler sampler) =>
|
||||
private static uint DecodeSamplerBorderColor(VulkanGuestSampler sampler) =>
|
||||
(sampler.Word3 >> 30) & 0x3u;
|
||||
|
||||
private static SamplerAddressMode ToVkSamplerAddressMode(uint mode) =>
|
||||
@@ -4639,11 +4741,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static GuestRect ClampScissor(GuestRect? scissor, Extent2D extent)
|
||||
private static VulkanGuestRect ClampScissor(VulkanGuestRect? scissor, Extent2D extent)
|
||||
{
|
||||
if (scissor is not { } rect)
|
||||
{
|
||||
return new GuestRect(0, 0, extent.Width, extent.Height);
|
||||
return new VulkanGuestRect(0, 0, extent.Width, extent.Height);
|
||||
}
|
||||
|
||||
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
|
||||
@@ -4656,7 +4758,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
rect.Y + checked((int)rect.Height),
|
||||
top,
|
||||
checked((int)extent.Height));
|
||||
return new GuestRect(
|
||||
return new VulkanGuestRect(
|
||||
left,
|
||||
top,
|
||||
checked((uint)(right - left)),
|
||||
@@ -4671,7 +4773,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
? viewportEpsilon
|
||||
: 0f;
|
||||
|
||||
private static Viewport ClampViewport(GuestViewport? viewport, Extent2D extent)
|
||||
private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent)
|
||||
{
|
||||
if (viewport is not { } rect)
|
||||
{
|
||||
@@ -5374,7 +5476,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private GuestImageResource GetOrCreateGuestImage(
|
||||
GuestRenderTarget target,
|
||||
VulkanGuestRenderTarget target,
|
||||
Format format)
|
||||
{
|
||||
var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels);
|
||||
@@ -5897,15 +5999,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var gpuInFlight = _pendingGuestSubmissions.Count +
|
||||
(_presentationInFlight ? 1 : 0);
|
||||
var readCount = Interlocked.Read(
|
||||
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
|
||||
var readBytes = Interlocked.Read(
|
||||
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
|
||||
var readHits = Interlocked.Read(
|
||||
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
|
||||
var readPvmBytes = Interlocked.Read(
|
||||
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
|
||||
var readLibcBytes = Interlocked.Read(
|
||||
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
|
||||
var readsPerSecond =
|
||||
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
|
||||
var readMbPerSecond =
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Silk.NET.Input": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Input.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ namespace SharpEmu.Logging;
|
||||
/// </summary>
|
||||
public static class BuildInfo
|
||||
{
|
||||
private const string ProjectUrl = "https://github.com/sharpemu/sharpemu";
|
||||
private const string CanonicalRepository = "sharpemu/sharpemu";
|
||||
private const string ProjectUrl = "https://github.com/par274/sharpemu";
|
||||
private const string CanonicalRepository = "par274/sharpemu";
|
||||
|
||||
/// <summary>Short commit hash the build was produced from, or <c>null</c>.</summary>
|
||||
public static string? CommitSha { get; }
|
||||
@@ -76,9 +76,9 @@ public static class BuildInfo
|
||||
/// <summary>
|
||||
/// The multi-line banner, e.g.
|
||||
/// <code>
|
||||
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/sharpemu/sharpemu
|
||||
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/par274/sharpemu
|
||||
///
|
||||
/// Built from branch "main" of "sharpemu/sharpemu" by GitHub Actions workflow run https://github.com/sharpemu/sharpemu/actions/runs/123.
|
||||
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
|
||||
/// </code>
|
||||
/// Official release builds drop the <c>UNOFFICIAL</c> tag. Falls back to a
|
||||
/// local-build line when no CI provenance is present.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
// SPIR-V-specific shader artifact types. These stay beside the SPIR-V emitter (not in
|
||||
// the backend-neutral SharpEmu.ShaderCompiler project): each codegen owns its own
|
||||
// compiled-shader shape.
|
||||
public enum Gen5SpirvStage
|
||||
{
|
||||
Vertex,
|
||||
Pixel,
|
||||
Compute,
|
||||
}
|
||||
|
||||
public sealed record Gen5SpirvShader(
|
||||
byte[] Spirv,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
uint AttributeCount,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
|
||||
@@ -1,19 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- The SPIR-V codegen backend: consumes the backend-neutral shader IR from
|
||||
SharpEmu.ShaderCompiler and emits raw SPIR-V words. Deliberately has no
|
||||
dependency on Vulkan bindings — emitters produce bytes; renderers own APIs. -->
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler;
|
||||
|
||||
/// <summary>
|
||||
/// The Gen5 (gfx10) inline-constant operand table, shared by every codegen so backends
|
||||
/// cannot drift on constant semantics.
|
||||
/// </summary>
|
||||
public static class Gen5InlineConstants
|
||||
{
|
||||
public static bool TryDecode(uint encoded, out uint value)
|
||||
{
|
||||
if (encoded == 125)
|
||||
{
|
||||
value = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (encoded is >= 128 and <= 192)
|
||||
{
|
||||
value = encoded - 128;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (encoded is >= 193 and <= 208)
|
||||
{
|
||||
value = unchecked((uint)-(int)(encoded - 192));
|
||||
return true;
|
||||
}
|
||||
|
||||
var floatingPoint = encoded switch
|
||||
{
|
||||
240 => 0.5f,
|
||||
241 => -0.5f,
|
||||
242 => 1.0f,
|
||||
243 => -1.0f,
|
||||
244 => 2.0f,
|
||||
245 => -2.0f,
|
||||
246 => 4.0f,
|
||||
247 => -4.0f,
|
||||
248 => 1.0f / (2.0f * MathF.PI),
|
||||
_ => float.NaN,
|
||||
};
|
||||
if (float.IsNaN(floatingPoint))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BitConverter.SingleToUInt32Bits(floatingPoint);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler;
|
||||
|
||||
/// <summary>
|
||||
/// Guest draw patterns the decoder recognizes from known shader programs. Guest-domain,
|
||||
/// backend-neutral — it previously lived inside the Vulkan presenter, which is exactly
|
||||
/// the kind of placement this project exists to prevent.
|
||||
/// </summary>
|
||||
public enum GuestDrawKind
|
||||
{
|
||||
None,
|
||||
FullscreenBarycentric,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- The backend-neutral half of guest shader compilation: the Gen5 (gfx10) microcode
|
||||
decoder, the scalar evaluator, and the shader IR every codegen consumes. Emitters
|
||||
(SPIR-V, MSL, DXIL) live in sibling SharpEmu.ShaderCompiler.* projects; nothing
|
||||
here may depend on a host graphics API. -->
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Build.Framework;
|
||||
|
||||
namespace SharpEmu.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// Builds aerolib.bin (the runtime NID -> name catalog) from scripts/ps5_names.txt at
|
||||
/// build time, replacing scripts/generate_aerolib_binary.py and the committed binary.
|
||||
/// Format matches the python script exactly: uint32 entry count, then per entry a
|
||||
/// byte-length-prefixed NID and a ushort-length-prefixed name, little-endian UTF-8.
|
||||
///
|
||||
/// Implements ITask directly (Framework-only reference) and is an MSBuild task, not an
|
||||
/// analyzer — the file-IO ban that RS1035 enforces for compiler-loaded code does not
|
||||
/// apply to build tasks, hence the targeted suppressions.
|
||||
/// </summary>
|
||||
public sealed class GenerateAerolibBinaryTask : ITask
|
||||
{
|
||||
public IBuildEngine? BuildEngine { get; set; }
|
||||
|
||||
public ITaskHost? HostObject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string NamesFile { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string OutputFile { get; set; } = string.Empty;
|
||||
|
||||
#pragma warning disable RS1035 // File IO is the entire point of this build task.
|
||||
public bool Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
var names = new List<string>();
|
||||
foreach (var line in File.ReadAllLines(NamesFile))
|
||||
{
|
||||
var name = line.Trim();
|
||||
if (name.Length != 0)
|
||||
{
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(OutputFile);
|
||||
if (!string.IsNullOrEmpty(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
using (var sha1 = System.Security.Cryptography.SHA1.Create())
|
||||
using (var stream = File.Create(OutputFile))
|
||||
using (var writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write((uint)names.Count);
|
||||
foreach (var name in names)
|
||||
{
|
||||
var nidBytes = Encoding.UTF8.GetBytes(Ps5Nid.Compute(name, sha1));
|
||||
var nameBytes = Encoding.UTF8.GetBytes(name);
|
||||
if (nameBytes.Length > ushort.MaxValue)
|
||||
{
|
||||
// A silent (ushort) truncation would corrupt the catalog.
|
||||
throw new InvalidDataException(
|
||||
$"Symbol name exceeds the format's ushort length prefix ({nameBytes.Length} bytes): '{name.Substring(0, 64)}...'");
|
||||
}
|
||||
|
||||
writer.Write((byte)nidBytes.Length);
|
||||
writer.Write(nidBytes);
|
||||
writer.Write((ushort)nameBytes.Length);
|
||||
writer.Write(nameBytes);
|
||||
}
|
||||
}
|
||||
|
||||
BuildEngine?.LogMessageEvent(new BuildMessageEventArgs(
|
||||
$"aerolib: {names.Count} symbols -> {OutputFile}",
|
||||
helpKeyword: null,
|
||||
senderName: nameof(GenerateAerolibBinaryTask),
|
||||
MessageImportance.Normal));
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
BuildEngine?.LogErrorEvent(new BuildErrorEventArgs(
|
||||
subcategory: null,
|
||||
code: "SHEMAERO",
|
||||
file: NamesFile,
|
||||
lineNumber: 0,
|
||||
columnNumber: 0,
|
||||
endLineNumber: 0,
|
||||
endColumnNumber: 0,
|
||||
message: exception.ToString(),
|
||||
helpKeyword: null,
|
||||
senderName: nameof(GenerateAerolibBinaryTask)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#pragma warning restore RS1035
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// The PS NID derivation: base64 (with the '+','-' alphabet, no padding) of the
|
||||
/// byte-reversed first eight bytes of SHA1(symbolName + fixed suffix). The same
|
||||
/// algorithm scripts/generate_aerolib_binary.py uses to build the runtime catalog,
|
||||
/// in C# so the analyzer and generator can validate and derive NIDs at compile time.
|
||||
/// </summary>
|
||||
public static class Ps5Nid
|
||||
{
|
||||
private static readonly byte[] Suffix =
|
||||
[
|
||||
0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1,
|
||||
0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30,
|
||||
];
|
||||
|
||||
public static string Compute(string symbolName)
|
||||
{
|
||||
using var sha1 = SHA1.Create();
|
||||
return Compute(symbolName, sha1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk-callable overload: the aerolib build task hashes every catalog name
|
||||
/// (~150k), so the caller owns one SHA1 instance instead of churning one per name.
|
||||
/// </summary>
|
||||
public static string Compute(string symbolName, SHA1 sha1)
|
||||
{
|
||||
var nameBytes = Encoding.UTF8.GetBytes(symbolName);
|
||||
var input = new byte[nameBytes.Length + Suffix.Length];
|
||||
nameBytes.CopyTo(input, 0);
|
||||
Suffix.CopyTo(input, nameBytes.Length);
|
||||
|
||||
var hash = sha1.ComputeHash(input);
|
||||
|
||||
// The script reads the first eight bytes as a little-endian integer and
|
||||
// formats it big-endian before encoding — a byte reversal.
|
||||
var reversed = new byte[8];
|
||||
for (var index = 0; index < 8; index++)
|
||||
{
|
||||
reversed[index] = hash[7 - index];
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(reversed)
|
||||
.TrimEnd('=')
|
||||
.Replace('/', '-');
|
||||
}
|
||||
|
||||
/// <summary>Eleven characters of the '+','-' base64 alphabet.</summary>
|
||||
public static bool IsValidFormat(string nid)
|
||||
{
|
||||
if (nid.Length != 11)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var character in nid)
|
||||
{
|
||||
var valid = character is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z') or
|
||||
(>= '0' and <= '9') or '+' or '-';
|
||||
if (!valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Roslyn components (source generator + analyzers) for the SysAbi export system.
|
||||
Must target netstandard2.0 to load inside the compiler. Consumed by the emulator
|
||||
projects as an analyzer reference (OutputItemType="Analyzer"), never at runtime. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<!-- RS2008: analyzer release tracking files are overkill for an in-repo analyzer. -->
|
||||
<NoWarn>$(NoWarn);RS2008</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" PrivateAssets="all" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// Compile-time rules for [SysAbiExport] declarations. Everything here used to be a
|
||||
/// runtime discovery (console warning or InvalidOperationException at boot); the
|
||||
/// analyzer turns each one into a build failure.
|
||||
/// </summary>
|
||||
public static class SysAbiDiagnostics
|
||||
{
|
||||
private const string Category = "SharpEmu.SysAbi";
|
||||
|
||||
public static readonly DiagnosticDescriptor DuplicateNid = new(
|
||||
"SHEM001",
|
||||
"Duplicate SysAbi NID",
|
||||
"NID '{0}' is exported by both '{1}' and '{2}'",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor InvalidNidFormat = new(
|
||||
"SHEM002",
|
||||
"Invalid NID format",
|
||||
"NID '{0}' is not eleven characters of the PS base64 alphabet (A-Z a-z 0-9 + -)",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor InvalidHandlerSignature = new(
|
||||
"SHEM003",
|
||||
"Invalid SysAbi handler signature",
|
||||
"Method '{0}' must be a static, non-generic method returning int and taking no parameters, a single CpuContext parameter, or a CpuContext followed by up to six int/uint/long/ulong or [GuestCString] string parameters",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor NidNameMismatch = new(
|
||||
"SHEM004",
|
||||
"NID does not match export name",
|
||||
"NID '{0}' does not match export name '{1}' (computed NID is '{2}') — one of the two is wrong",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor UnresolvableExport = new(
|
||||
"SHEM005",
|
||||
"Export declares neither NID nor export name",
|
||||
"Method '{0}' must declare an ExportName (from which the NID is derived) or an explicit Nid",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor NameNotInCatalog = new(
|
||||
"SHEM006",
|
||||
"Export name not present in the PS5 symbol catalog",
|
||||
"Export name '{0}' is not in ps5_names.txt — likely a typo, or the catalog needs the new symbol",
|
||||
Category,
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor HandlerNotAccessible = new(
|
||||
"SHEM007",
|
||||
"SysAbi handler not accessible to generated registration",
|
||||
"Method '{0}' (or its containing type) must be at least internal so the generated registry can reference it",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor InvalidGuestCString = new(
|
||||
"SHEM008",
|
||||
"Invalid [GuestCString] usage",
|
||||
"Method '{0}' misuses [GuestCString]: it applies only to string parameters and requires a positive MaxLength",
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SharpEmu.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// Build-time enforcement for [SysAbiExport] declarations: duplicate NIDs, malformed
|
||||
/// NIDs, NIDs that contradict their export name (checked with the PS NID computation),
|
||||
/// handler signatures the dispatcher cannot call, and — when scripts/ps5_names.txt is
|
||||
/// wired up as an AdditionalFile — export names unknown to the symbol catalog.
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string CatalogFileName = "ps5_names.txt";
|
||||
|
||||
// The catalog is ~150k lines; parse it once per file snapshot instead of on every
|
||||
// compilation start (the IDE creates one per keystroke). A changed file arrives as
|
||||
// a fresh AdditionalText instance, which naturally misses the cache.
|
||||
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<AdditionalText, CatalogHolder> _catalogCache = new();
|
||||
|
||||
private sealed class CatalogHolder
|
||||
{
|
||||
public CatalogHolder(HashSet<string>? names) => Names = names;
|
||||
|
||||
public HashSet<string>? Names { get; }
|
||||
}
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
|
||||
[
|
||||
SysAbiDiagnostics.DuplicateNid,
|
||||
SysAbiDiagnostics.InvalidNidFormat,
|
||||
SysAbiDiagnostics.InvalidHandlerSignature,
|
||||
SysAbiDiagnostics.NidNameMismatch,
|
||||
SysAbiDiagnostics.UnresolvableExport,
|
||||
SysAbiDiagnostics.NameNotInCatalog,
|
||||
SysAbiDiagnostics.HandlerNotAccessible,
|
||||
SysAbiDiagnostics.InvalidGuestCString,
|
||||
];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.RegisterCompilationStartAction(static startContext =>
|
||||
{
|
||||
var catalogNames = LoadCatalog(startContext.Options.AdditionalFiles, startContext.CancellationToken);
|
||||
var exportsByNid = new ConcurrentDictionary<string, IMethodSymbol>();
|
||||
|
||||
startContext.RegisterSymbolAction(
|
||||
symbolContext => AnalyzeMethod(symbolContext, catalogNames, exportsByNid),
|
||||
SymbolKind.Method);
|
||||
});
|
||||
}
|
||||
|
||||
private static HashSet<string>? LoadCatalog(
|
||||
ImmutableArray<AdditionalText> additionalFiles,
|
||||
System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var file in additionalFiles)
|
||||
{
|
||||
if (!file.Path.EndsWith(CatalogFileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return _catalogCache.GetValue(file, static f => new CatalogHolder(ParseCatalog(f))).Names;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static HashSet<string>? ParseCatalog(AdditionalText file)
|
||||
{
|
||||
var text = file.GetText();
|
||||
if (text is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var names = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var line in text.Lines)
|
||||
{
|
||||
var name = line.ToString().Trim();
|
||||
if (name.Length != 0)
|
||||
{
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
private static void AnalyzeMethod(
|
||||
SymbolAnalysisContext context,
|
||||
HashSet<string>? catalogNames,
|
||||
ConcurrentDictionary<string, IMethodSymbol> exportsByNid)
|
||||
{
|
||||
var method = (IMethodSymbol)context.Symbol;
|
||||
AttributeData? exportAttribute = null;
|
||||
foreach (var attribute in method.GetAttributes())
|
||||
{
|
||||
if (SysAbiExportShape.IsSysAbiExportAttribute(attribute.AttributeClass))
|
||||
{
|
||||
exportAttribute = attribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exportAttribute is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var location = method.Locations.Length != 0 ? method.Locations[0] : Location.None;
|
||||
var methodDisplay = $"{method.ContainingType.ToDisplayString()}.{method.Name}";
|
||||
|
||||
if (SysAbiExportShape.Classify(method, out _, out var invalidGuestCString) == SysAbiExportShape.HandlerShape.Invalid)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
invalidGuestCString ? SysAbiDiagnostics.InvalidGuestCString : SysAbiDiagnostics.InvalidHandlerSignature,
|
||||
location,
|
||||
methodDisplay));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SysAbiDiagnostics.HandlerNotAccessible, location, methodDisplay));
|
||||
}
|
||||
|
||||
var arguments = SysAbiExportShape.ReadArguments(exportAttribute);
|
||||
var hasNid = !string.IsNullOrWhiteSpace(arguments.Nid);
|
||||
var hasName = !string.IsNullOrWhiteSpace(arguments.ExportName);
|
||||
|
||||
if (!hasNid && !hasName)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SysAbiDiagnostics.UnresolvableExport, location, methodDisplay));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNid && !Ps5Nid.IsValidFormat(arguments.Nid))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SysAbiDiagnostics.InvalidNidFormat, location, arguments.Nid));
|
||||
return;
|
||||
}
|
||||
|
||||
var effectiveNid = arguments.Nid;
|
||||
if (hasName)
|
||||
{
|
||||
var computed = Ps5Nid.Compute(arguments.ExportName);
|
||||
|
||||
var nameInCatalog = catalogNames is not null && catalogNames.Contains(arguments.ExportName);
|
||||
|
||||
// A declared NID that contradicts its name is only provably wrong when the
|
||||
// name is a real catalog symbol. Names outside the catalog are synthetic
|
||||
// labels for NIDs whose true symbol is unknown (the "sceAgcUnknown..."
|
||||
// convention) — for those the NID is authoritative and only SHEM006 applies.
|
||||
// With no catalog wired, every name is validated (fail closed).
|
||||
var nameIsKnown = catalogNames is null || nameInCatalog;
|
||||
if (hasNid && nameIsKnown && !string.Equals(computed, arguments.Nid, StringComparison.Ordinal))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SysAbiDiagnostics.NidNameMismatch,
|
||||
location,
|
||||
arguments.Nid,
|
||||
arguments.ExportName,
|
||||
computed));
|
||||
}
|
||||
|
||||
if (!hasNid)
|
||||
{
|
||||
effectiveNid = computed;
|
||||
}
|
||||
|
||||
if (catalogNames is not null && !nameInCatalog)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SysAbiDiagnostics.NameNotInCatalog, location, arguments.ExportName));
|
||||
}
|
||||
}
|
||||
|
||||
var existing = exportsByNid.GetOrAdd(effectiveNid, method);
|
||||
if (!SymbolEqualityComparer.Default.Equals(existing, method))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SysAbiDiagnostics.DuplicateNid,
|
||||
location,
|
||||
effectiveNid,
|
||||
$"{existing.ContainingType.ToDisplayString()}.{existing.Name}",
|
||||
methodDisplay));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace SharpEmu.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// Emits the SysAbi export registry at compile time: one static class per assembly whose
|
||||
/// CreateExports(Generation) lists every [SysAbiExport] handler — generation filtering,
|
||||
/// name fallback, and library default preserved from the retired reflection scan. NIDs
|
||||
/// omitted from attributes are derived from the export name with the PS NID algorithm
|
||||
/// (the same computation the runtime symbol catalog was built from). Handlers written
|
||||
/// with typed signatures get a SysV register-unmarshalling thunk emitted here.
|
||||
///
|
||||
/// Invalid declarations are skipped here and rejected by SysAbiExportAnalyzer as build
|
||||
/// errors, so nothing can be silently dropped.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeMetadataName = SysAbiExportShape.SysAbiExportAttributeName;
|
||||
|
||||
private sealed class ExportModel : IEquatable<ExportModel>
|
||||
{
|
||||
public ExportModel(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target)
|
||||
{
|
||||
ContainingType = containingType;
|
||||
MethodName = methodName;
|
||||
Shape = shape;
|
||||
TypedParameterKinds = typedParameterKinds;
|
||||
LibraryName = libraryName;
|
||||
Nid = nid;
|
||||
ExportName = exportName;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public string ContainingType { get; }
|
||||
public string MethodName { get; }
|
||||
public SysAbiExportShape.HandlerShape Shape { get; }
|
||||
|
||||
// Deliberately a comma-joined string ("uint,int,cstring:4096") rather than an
|
||||
// array: the model must be equatable for incremental-generator caching, and a
|
||||
// string gets that for free where an array would need a custom comparer.
|
||||
public string TypedParameterKinds { get; }
|
||||
public string LibraryName { get; }
|
||||
public string Nid { get; }
|
||||
public string ExportName { get; }
|
||||
public int Target { get; }
|
||||
|
||||
public bool Equals(ExportModel? other) =>
|
||||
other is not null &&
|
||||
ContainingType == other.ContainingType &&
|
||||
MethodName == other.MethodName &&
|
||||
Shape == other.Shape &&
|
||||
TypedParameterKinds == other.TypedParameterKinds &&
|
||||
LibraryName == other.LibraryName &&
|
||||
Nid == other.Nid &&
|
||||
ExportName == other.ExportName &&
|
||||
Target == other.Target;
|
||||
|
||||
public override bool Equals(object? obj) => Equals(obj as ExportModel);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hash = 17;
|
||||
hash = (hash * 31) + ContainingType.GetHashCode();
|
||||
hash = (hash * 31) + MethodName.GetHashCode();
|
||||
hash = (hash * 31) + Nid.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var exports = context.SyntaxProvider
|
||||
.ForAttributeWithMetadataName(
|
||||
AttributeMetadataName,
|
||||
static (node, _) => node is MethodDeclarationSyntax,
|
||||
static (attributeContext, _) => CreateModel(attributeContext))
|
||||
.Where(static model => model is not null)
|
||||
.Collect();
|
||||
|
||||
var assemblyName = context.CompilationProvider
|
||||
.Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly");
|
||||
|
||||
context.RegisterSourceOutput(
|
||||
exports.Combine(assemblyName),
|
||||
static (productionContext, source) => Emit(productionContext, source.Left!, source.Right));
|
||||
}
|
||||
|
||||
private static ExportModel? CreateModel(GeneratorAttributeSyntaxContext context)
|
||||
{
|
||||
if (context.TargetSymbol is not IMethodSymbol method ||
|
||||
!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds);
|
||||
if (shape == SysAbiExportShape.HandlerShape.Invalid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var attribute = context.Attributes[0];
|
||||
var arguments = SysAbiExportShape.ReadArguments(attribute);
|
||||
var nid = arguments.Nid;
|
||||
var exportName = arguments.ExportName;
|
||||
|
||||
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the export
|
||||
// name (algorithmically — equivalent to the runtime catalog lookup, which was
|
||||
// built with the same computation); a missing name falls back to the method name.
|
||||
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
|
||||
{
|
||||
nid = Ps5Nid.Compute(exportName);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(exportName))
|
||||
{
|
||||
exportName = method.Name;
|
||||
}
|
||||
|
||||
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
|
||||
return new ExportModel(
|
||||
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
method.Name,
|
||||
shape,
|
||||
typedParameterKinds,
|
||||
libraryName,
|
||||
nid!,
|
||||
exportName!,
|
||||
arguments.Target);
|
||||
}
|
||||
|
||||
private static void Emit(
|
||||
SourceProductionContext context,
|
||||
ImmutableArray<ExportModel?> exports,
|
||||
string assemblyName)
|
||||
{
|
||||
// No exports, no registry: an assembly that merely references the analyzer
|
||||
// (e.g. SharpEmu.HLE itself) must not mint a colliding
|
||||
// SharpEmu.Generated.SysAbiExportRegistry type.
|
||||
if (exports.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("// <auto-generated by SharpEmu.SourceGenerators/SysAbiExportGenerator />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("namespace SharpEmu.Generated;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("/// <summary>Compile-time SysAbi export registry for " + assemblyName + ".</summary>");
|
||||
builder.AppendLine("public static class SysAbiExportRegistry");
|
||||
builder.AppendLine("{");
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine(" /// Exports effective for the given registration generation, with the same");
|
||||
builder.AppendLine(" /// semantics as the reflection scan: an attribute Target of None inherits the");
|
||||
builder.AppendLine(" /// registration generation, and exports outside it are skipped.");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<global::SharpEmu.HLE.ExportedFunction> CreateExports(");
|
||||
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exports.Length});");
|
||||
|
||||
foreach (var export in exports)
|
||||
{
|
||||
if (export is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var function = export.Shape switch
|
||||
{
|
||||
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
|
||||
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
|
||||
_ => TypedThunk(export),
|
||||
};
|
||||
builder.AppendLine(
|
||||
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
|
||||
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, {function});");
|
||||
}
|
||||
|
||||
builder.AppendLine(" return exports;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" private static void Add(");
|
||||
builder.AppendLine(" global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction> exports,");
|
||||
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration,");
|
||||
builder.AppendLine(" string libraryName,");
|
||||
builder.AppendLine(" string nid,");
|
||||
builder.AppendLine(" string exportName,");
|
||||
builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,");
|
||||
builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;");
|
||||
builder.AppendLine(" if ((target & registrationGeneration) == 0)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function));");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine("}");
|
||||
|
||||
context.AddSource("SysAbiExportRegistry.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SysV integer-register unmarshalling: parameter i reads argument register i as a
|
||||
/// raw ulong and reinterprets it with an unchecked cast, exactly the idiom
|
||||
/// hand-written handlers use today. [GuestCString] parameters read the register as
|
||||
/// a guest pointer and marshal the null-terminated UTF-8 string up front, failing
|
||||
/// the call with ORBIS_GEN2_ERROR_MEMORY_FAULT before the handler runs.
|
||||
/// </summary>
|
||||
private static string TypedThunk(ExportModel export)
|
||||
{
|
||||
var kinds = export.TypedParameterKinds.Split(',');
|
||||
var arguments = new string[kinds.Length];
|
||||
var reads = new StringBuilder();
|
||||
for (var index = 0; index < kinds.Length; index++)
|
||||
{
|
||||
var register = "ctx[global::SharpEmu.HLE.CpuRegister." + SysAbiExportShape.ArgumentRegisters[index] + "]";
|
||||
if (kinds[index].StartsWith("cstring:", StringComparison.Ordinal))
|
||||
{
|
||||
var maxLength = kinds[index].Substring("cstring:".Length);
|
||||
var variable = "guestString" + index;
|
||||
reads.AppendLine($" if (!ctx.TryReadNullTerminatedUtf8({register}, {maxLength}, out var {variable}))");
|
||||
reads.AppendLine(" {");
|
||||
reads.AppendLine(" return ctx.SetReturn(global::SharpEmu.HLE.OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);");
|
||||
reads.AppendLine(" }");
|
||||
reads.AppendLine();
|
||||
arguments[index] = variable;
|
||||
continue;
|
||||
}
|
||||
|
||||
arguments[index] = kinds[index] == "ulong"
|
||||
? register
|
||||
: "unchecked((" + kinds[index] + ")" + register + ")";
|
||||
}
|
||||
|
||||
var invocation = export.ContainingType + "." + export.MethodName + "(ctx, " + string.Join(", ", arguments) + ")";
|
||||
if (reads.Length == 0)
|
||||
{
|
||||
return "static ctx => " + invocation;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("static ctx =>");
|
||||
builder.AppendLine(" {");
|
||||
builder.Append(reads);
|
||||
builder.AppendLine(" return " + invocation + ";");
|
||||
builder.Append(" }");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string Literal(string value) =>
|
||||
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// Shared shape rules for [SysAbiExport] methods, used by both the generator (to decide
|
||||
/// what it can emit) and the analyzer (to reject everything else as build errors), so
|
||||
/// the two can never disagree about what a valid handler is.
|
||||
/// </summary>
|
||||
public static class SysAbiExportShape
|
||||
{
|
||||
/// <summary>Single source of truth so the generator and analyzer can never
|
||||
/// disagree about which attribute marks an export.</summary>
|
||||
public const string SysAbiExportAttributeName = "SharpEmu.HLE.SysAbiExportAttribute";
|
||||
|
||||
public readonly struct Arguments
|
||||
{
|
||||
public Arguments(string libraryName, string nid, string exportName, int target)
|
||||
{
|
||||
LibraryName = libraryName;
|
||||
Nid = nid;
|
||||
ExportName = exportName;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public string LibraryName { get; }
|
||||
public string Nid { get; }
|
||||
public string ExportName { get; }
|
||||
public int Target { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SysV integer argument registers in call order; typed handler parameters map to
|
||||
/// these positionally.
|
||||
/// </summary>
|
||||
public static readonly string[] ArgumentRegisters = ["Rdi", "Rsi", "Rdx", "Rcx", "R8", "R9"];
|
||||
|
||||
public enum HandlerShape
|
||||
{
|
||||
Invalid,
|
||||
/// <summary>int M(CpuContext) — the classic raw-register shape.</summary>
|
||||
ContextOnly,
|
||||
/// <summary>int M() — no guest state needed.</summary>
|
||||
Parameterless,
|
||||
/// <summary>int M(CpuContext, up to six int/uint/long/ulong args) — the
|
||||
/// generator emits the SysV register unmarshalling thunk.</summary>
|
||||
Typed,
|
||||
}
|
||||
|
||||
private const string GuestCStringAttributeName = "SharpEmu.HLE.GuestCStringAttribute";
|
||||
|
||||
/// <summary>Static, non-generic, returns int, takes one of the supported shapes.</summary>
|
||||
public static HandlerShape Classify(IMethodSymbol method, out string typedParameterKinds) =>
|
||||
Classify(method, out typedParameterKinds, out _);
|
||||
|
||||
/// <summary>
|
||||
/// <paramref name="invalidGuestCString"/> distinguishes a misused [GuestCString]
|
||||
/// (wrong parameter type, non-positive MaxLength) from a plain signature mismatch,
|
||||
/// so the analyzer can point at the marshalling attribute instead of the shape.
|
||||
/// </summary>
|
||||
public static HandlerShape Classify(IMethodSymbol method, out string typedParameterKinds, out bool invalidGuestCString)
|
||||
{
|
||||
typedParameterKinds = string.Empty;
|
||||
invalidGuestCString = false;
|
||||
if (!method.IsStatic ||
|
||||
method.IsGenericMethod ||
|
||||
method.ReturnType.SpecialType != SpecialType.System_Int32)
|
||||
{
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
if (method.Parameters.Length == 0)
|
||||
{
|
||||
return HandlerShape.Parameterless;
|
||||
}
|
||||
|
||||
if (method.Parameters[0].RefKind != RefKind.None || !IsCpuContext(method.Parameters[0].Type))
|
||||
{
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
if (method.Parameters.Length == 1)
|
||||
{
|
||||
return HandlerShape.ContextOnly;
|
||||
}
|
||||
|
||||
if (method.Parameters.Length > 1 + ArgumentRegisters.Length)
|
||||
{
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
var kinds = new string[method.Parameters.Length - 1];
|
||||
for (var index = 1; index < method.Parameters.Length; index++)
|
||||
{
|
||||
var parameter = method.Parameters[index];
|
||||
if (parameter.RefKind != RefKind.None)
|
||||
{
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
var hasGuestCString = TryGetGuestCStringMaxLength(parameter, out var maxLength);
|
||||
if (parameter.Type.SpecialType == SpecialType.System_String)
|
||||
{
|
||||
if (!hasGuestCString)
|
||||
{
|
||||
// A bare string has no register representation; the guest pointer
|
||||
// must be marshalled explicitly via [GuestCString].
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
if (maxLength <= 0)
|
||||
{
|
||||
invalidGuestCString = true;
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
kinds[index - 1] = "cstring:" + maxLength.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasGuestCString)
|
||||
{
|
||||
invalidGuestCString = true;
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
|
||||
kinds[index - 1] = parameter.Type.SpecialType switch
|
||||
{
|
||||
SpecialType.System_Int32 => "int",
|
||||
SpecialType.System_UInt32 => "uint",
|
||||
SpecialType.System_Int64 => "long",
|
||||
SpecialType.System_UInt64 => "ulong",
|
||||
_ => string.Empty,
|
||||
};
|
||||
if (kinds[index - 1].Length == 0)
|
||||
{
|
||||
return HandlerShape.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
typedParameterKinds = string.Join(",", kinds);
|
||||
return HandlerShape.Typed;
|
||||
}
|
||||
|
||||
// Symbol names are compared with an explicit fully-qualified format so
|
||||
// classification can never depend on a display-format default.
|
||||
private static bool IsCpuContext(ITypeSymbol type) =>
|
||||
type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::SharpEmu.HLE.CpuContext";
|
||||
|
||||
public static bool IsSysAbiExportAttribute(INamedTypeSymbol? attributeClass) =>
|
||||
attributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::" + SysAbiExportAttributeName;
|
||||
|
||||
private static bool TryGetGuestCStringMaxLength(IParameterSymbol parameter, out int maxLength)
|
||||
{
|
||||
maxLength = 0;
|
||||
foreach (var attribute in parameter.GetAttributes())
|
||||
{
|
||||
if (attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::" + GuestCStringAttributeName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute.ConstructorArguments.Length == 1 &&
|
||||
attribute.ConstructorArguments[0].Value is int value)
|
||||
{
|
||||
maxLength = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsValidHandler(IMethodSymbol method) =>
|
||||
Classify(method, out _) != HandlerShape.Invalid;
|
||||
|
||||
/// <summary>The generated registry lives in the same assembly: internal suffices.</summary>
|
||||
public static bool IsAccessibleFromGeneratedCode(IMethodSymbol method)
|
||||
{
|
||||
if (method.DeclaredAccessibility is Accessibility.Private or Accessibility.ProtectedOrInternal
|
||||
or Accessibility.Protected or Accessibility.ProtectedAndInternal)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var type = method.ContainingType; type is not null; type = type.ContainingType)
|
||||
{
|
||||
if (type.DeclaredAccessibility is Accessibility.Private or Accessibility.Protected
|
||||
or Accessibility.ProtectedOrInternal or Accessibility.ProtectedAndInternal)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Arguments ReadArguments(AttributeData attribute)
|
||||
{
|
||||
var libraryName = string.Empty;
|
||||
var nid = string.Empty;
|
||||
var exportName = string.Empty;
|
||||
var target = 0;
|
||||
foreach (var argument in attribute.NamedArguments)
|
||||
{
|
||||
switch (argument.Key)
|
||||
{
|
||||
case "LibraryName":
|
||||
libraryName = argument.Value.Value as string ?? string.Empty;
|
||||
break;
|
||||
case "Nid":
|
||||
nid = argument.Value.Value as string ?? string.Empty;
|
||||
break;
|
||||
case "ExportName":
|
||||
exportName = argument.Value.Value as string ?? string.Empty;
|
||||
break;
|
||||
case "Target":
|
||||
target = argument.Value.Value is int value ? value : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Arguments(libraryName, nid, exportName, target);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Guards the build-time-generated aerolib.bin embedding: the catalog must load from
|
||||
/// the assembly and resolve both directions, or the loader's import naming, the
|
||||
/// not-implemented diagnostics, and runtime dlsym all silently degrade.
|
||||
/// </summary>
|
||||
public sealed class AerolibCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmbeddedCatalogResolvesKnownSymbolBothWays()
|
||||
{
|
||||
Assert.True(Aerolib.Instance.TryGetByExportName("sceKernelWaitSema", out var byName));
|
||||
Assert.Equal("Zxa0VhQVTsk", byName.Nid);
|
||||
|
||||
Assert.True(Aerolib.Instance.TryGetByNid("Zxa0VhQVTsk", out var byNid));
|
||||
Assert.Equal("sceKernelWaitSema", byNid.ExportName);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +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.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// IT_EVENT_WRITE carries a 6-bit hardware EVENT_TYPE, but sceAgcDriverAddEqEvent registers the
|
||||
// listener with a guest-defined eventId. Those two values are not the same numbering scheme, so
|
||||
// exact ident matching never wakes anything (issue #173). TriggerRegisteredEventsByFilter wakes
|
||||
// every graphics registration instead.
|
||||
public sealed class AgcEventQueueTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const int MemorySize = 0x2000;
|
||||
|
||||
[Fact]
|
||||
public void TriggerRegisteredEventsByFilter_DifferentIdentThanEventType_WakesGraphicsWaiter()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
const ulong handleOutAddress = BaseAddress + 0x100;
|
||||
const ulong eventsAddress = BaseAddress + 0x200;
|
||||
const ulong outCountAddress = BaseAddress + 0x300;
|
||||
const ulong timeoutAddress = BaseAddress + 0x400;
|
||||
|
||||
// Create an event queue.
|
||||
ctx[CpuRegister.Rdi] = handleOutAddress;
|
||||
var createResult = KernelEventQueueCompatExports.KernelCreateEqueue(ctx);
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, createResult);
|
||||
|
||||
var handle = ReadUInt64(memory, handleOutAddress);
|
||||
|
||||
// Register a graphics event with eventId 0x20 (as Poppy Playtime does).
|
||||
const ulong registeredEventId = 0x20;
|
||||
const ulong userData = 0xDEAD_BEEF;
|
||||
var registered = KernelEventQueueCompatExports.RegisterEvent(
|
||||
handle,
|
||||
registeredEventId,
|
||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||
userData);
|
||||
Assert.True(registered);
|
||||
|
||||
// The command buffer fires EVENT_WRITE with eventType 0x07. This does not match 0x20.
|
||||
const ulong eventType = 0x07;
|
||||
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||
eventType);
|
||||
Assert.Equal(1, triggered);
|
||||
|
||||
// Wait with timeout=0. The event is already pending, so this returns immediately.
|
||||
WriteUInt64(memory, timeoutAddress, 0);
|
||||
ctx[CpuRegister.Rdi] = handle;
|
||||
ctx[CpuRegister.Rsi] = eventsAddress;
|
||||
ctx[CpuRegister.Rdx] = 4;
|
||||
ctx[CpuRegister.Rcx] = outCountAddress;
|
||||
ctx[CpuRegister.R8] = timeoutAddress;
|
||||
var waitResult = KernelEventQueueCompatExports.KernelWaitEqueue(ctx);
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, waitResult);
|
||||
Assert.Equal(1u, ReadUInt32(memory, outCountAddress));
|
||||
|
||||
// Verify the queued event carries the registered ident and the event type as data.
|
||||
Assert.Equal(registeredEventId, ReadUInt64(memory, eventsAddress + 0x00));
|
||||
Assert.Equal(KernelEventQueueCompatExports.KernelEventFilterGraphics, ReadInt16(memory, eventsAddress + 0x08));
|
||||
Assert.Equal(0u, ReadUInt16(memory, eventsAddress + 0x0A));
|
||||
Assert.Equal(1u, ReadUInt32(memory, eventsAddress + 0x0C));
|
||||
Assert.Equal(eventType, ReadUInt64(memory, eventsAddress + 0x10));
|
||||
Assert.Equal(userData, ReadUInt64(memory, eventsAddress + 0x18));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TriggerRegisteredEventsByFilter_NoGraphicsRegistrations_ReturnsZero()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
const ulong handleOutAddress = BaseAddress + 0x100;
|
||||
ctx[CpuRegister.Rdi] = handleOutAddress;
|
||||
var createResult = KernelEventQueueCompatExports.KernelCreateEqueue(ctx);
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, createResult);
|
||||
|
||||
var handle = ReadUInt64(memory, handleOutAddress);
|
||||
|
||||
// Register a user event, not a graphics event.
|
||||
KernelEventQueueCompatExports.RegisterEvent(
|
||||
handle,
|
||||
0x1,
|
||||
KernelEventQueueCompatExports.KernelEventFilterUser,
|
||||
0);
|
||||
|
||||
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||
0x07);
|
||||
|
||||
Assert.Equal(0, triggered);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt16LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static short ReadInt16(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadInt16LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public sealed class JsonExportRegistrationTests
|
||||
private static ModuleManager CreateRegisteredManager()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
manager.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
|
||||
return manager;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
// sceKernelGetTscFrequency must describe the same clock that sceKernelReadTsc returns. ReadTsc
|
||||
// only returns the CPU's RDTSC when the host RDTSC reader is available (64-bit Windows) and
|
||||
// otherwise falls back to the QPC-based Stopwatch, so the frequency selection has to follow suit.
|
||||
public sealed class KernelRuntimeCompatExportsTests
|
||||
{
|
||||
private static KernelRuntimeCompatExports.TryGetFrequency Yields(ulong hz) =>
|
||||
(out ulong frequencyHz) =>
|
||||
{
|
||||
frequencyHz = hz;
|
||||
return true;
|
||||
};
|
||||
|
||||
private static readonly KernelRuntimeCompatExports.TryGetFrequency Fails =
|
||||
(out ulong frequencyHz) =>
|
||||
{
|
||||
frequencyHz = 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void WithoutHostRdtsc_ReportsStopwatchFrequency_NotHardwareTsc()
|
||||
{
|
||||
// Regression: on Linux/macOS ReadTsc returns the Stopwatch counter, so the reported
|
||||
// frequency must be the Stopwatch's, never the CPU's much larger hardware TSC frequency.
|
||||
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable: false,
|
||||
overrideHzText: null,
|
||||
tryCalibrate: Yields(2_400_000_000UL),
|
||||
tryResolveCpuid: Yields(3_000_000_000UL),
|
||||
stopwatchFrequency: 10_000_000);
|
||||
|
||||
Assert.Equal(10_000_000UL, frequencyHz);
|
||||
Assert.Equal("qpc", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithHostRdtsc_PrefersCalibratedFrequency()
|
||||
{
|
||||
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable: true,
|
||||
overrideHzText: null,
|
||||
tryCalibrate: Yields(2_400_000_000UL),
|
||||
tryResolveCpuid: Yields(3_000_000_000UL),
|
||||
stopwatchFrequency: 10_000_000);
|
||||
|
||||
Assert.Equal(2_400_000_000UL, frequencyHz);
|
||||
Assert.Equal("calibrated-rdtsc", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithHostRdtsc_FallsBackToCpuid_WhenCalibrationFails()
|
||||
{
|
||||
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable: true,
|
||||
overrideHzText: null,
|
||||
tryCalibrate: Fails,
|
||||
tryResolveCpuid: Yields(3_000_000_000UL),
|
||||
stopwatchFrequency: 10_000_000);
|
||||
|
||||
Assert.Equal(3_000_000_000UL, frequencyHz);
|
||||
Assert.Equal("cpuid", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithHostRdtsc_UsesStopwatch_WhenRdtscFrequencyUnknown()
|
||||
{
|
||||
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable: true,
|
||||
overrideHzText: null,
|
||||
tryCalibrate: Fails,
|
||||
tryResolveCpuid: Fails,
|
||||
stopwatchFrequency: 10_000_000);
|
||||
|
||||
Assert.Equal(10_000_000UL, frequencyHz);
|
||||
Assert.Equal("qpc", source);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void EnvOverride_Wins_WhenSane(bool rdtscAvailable)
|
||||
{
|
||||
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable,
|
||||
overrideHzText: "1500000000",
|
||||
tryCalibrate: Yields(2_400_000_000UL),
|
||||
tryResolveCpuid: Yields(3_000_000_000UL),
|
||||
stopwatchFrequency: 10_000_000);
|
||||
|
||||
Assert.Equal(1_500_000_000UL, frequencyHz);
|
||||
Assert.Equal("env", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvOverride_BelowMinimum_IsIgnored()
|
||||
{
|
||||
// 500 kHz is below the sanity floor, so it is dropped; with rdtsc unavailable the
|
||||
// hardware-TSC path is gated off and the Stopwatch frequency is used.
|
||||
var (frequencyHz, _) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable: false,
|
||||
overrideHzText: "500000",
|
||||
tryCalibrate: Fails,
|
||||
tryResolveCpuid: Yields(3_000_000_000UL),
|
||||
stopwatchFrequency: 10_000_000);
|
||||
|
||||
Assert.Equal(10_000_000UL, frequencyHz);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonPositiveStopwatchFrequency_FallsBackToDefault()
|
||||
{
|
||||
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
||||
rdtscAvailable: false,
|
||||
overrideHzText: null,
|
||||
tryCalibrate: Fails,
|
||||
tryResolveCpuid: Fails,
|
||||
stopwatchFrequency: 0);
|
||||
|
||||
Assert.Equal(10_000_000UL, frequencyHz); // DefaultKernelTscFrequency
|
||||
Assert.Equal("qpc", source);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Pthread;
|
||||
|
||||
// POSIX condition variables are edges, not semaphore credits. A signal with no waiter
|
||||
// must have no effect. This was violated by the previous implementation which persisted
|
||||
// signals via PendingSignals, causing lock inversions and predicate bypasses.
|
||||
// See issue #113.
|
||||
public sealed class PthreadCondSemanticsTests
|
||||
{
|
||||
[Fact]
|
||||
public void PthreadCondState_DoesNotHavePendingSignals()
|
||||
{
|
||||
// Verify that PthreadCondState no longer has the PendingSignals property.
|
||||
// This is a regression test to ensure the POSIX-correct behavior is maintained.
|
||||
var stateType = typeof(KernelPthreadCompatExports).GetNestedType("PthreadCondState", BindingFlags.NonPublic);
|
||||
Assert.NotNull(stateType);
|
||||
|
||||
var pendingSignalsProp = stateType.GetProperty("PendingSignals");
|
||||
Assert.Null(pendingSignalsProp);
|
||||
|
||||
var tryConsumeMethod = stateType.GetMethod("TryConsumePendingSignal");
|
||||
Assert.Null(tryConsumeMethod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadCondSignal_WithNoWaiter_DoesNotPersist()
|
||||
{
|
||||
// This test verifies the semantic contract: signal without waiter is a no-op.
|
||||
// We can't easily test the full pthread flow without the scheduler, but we can
|
||||
// verify the code path by checking that SignalEpoch advances but no state persists.
|
||||
var stateType = typeof(KernelPthreadCompatExports).GetNestedType("PthreadCondState", BindingFlags.NonPublic);
|
||||
Assert.NotNull(stateType);
|
||||
|
||||
// Create an instance via reflection
|
||||
var state = Activator.CreateInstance(stateType);
|
||||
Assert.NotNull(state);
|
||||
|
||||
var syncRootProp = stateType.GetProperty("SyncRoot");
|
||||
var signalEpochProp = stateType.GetProperty("SignalEpoch");
|
||||
var waitersProp = stateType.GetProperty("Waiters");
|
||||
|
||||
Assert.NotNull(syncRootProp);
|
||||
Assert.NotNull(signalEpochProp);
|
||||
Assert.NotNull(waitersProp);
|
||||
|
||||
var syncRoot = syncRootProp.GetValue(state);
|
||||
Assert.NotNull(syncRoot);
|
||||
|
||||
// Initial state
|
||||
Assert.Equal(0UL, (ulong)signalEpochProp.GetValue(state)!);
|
||||
Assert.Equal(0, (int)waitersProp.GetValue(state)!);
|
||||
|
||||
// Simulate signal with no waiter (this would have incremented PendingSignals before)
|
||||
lock (syncRoot)
|
||||
{
|
||||
signalEpochProp.SetValue(state, (ulong)signalEpochProp.GetValue(state)! + 1);
|
||||
// Note: we don't increment PendingSignals because it doesn't exist
|
||||
}
|
||||
|
||||
// Verify epoch advanced but no persistent signal state
|
||||
Assert.Equal(1UL, (ulong)signalEpochProp.GetValue(state)!);
|
||||
|
||||
// A new waiter arriving should see the new epoch but not consume any "pending" signal
|
||||
// (because there's no such concept anymore)
|
||||
lock (syncRoot)
|
||||
{
|
||||
var observedEpoch = (ulong)signalEpochProp.GetValue(state)!;
|
||||
waitersProp.SetValue(state, (int)waitersProp.GetValue(state)! + 1);
|
||||
|
||||
// Waiter sees epoch=1, will block until epoch changes again
|
||||
Assert.Equal(1UL, observedEpoch);
|
||||
Assert.Equal(1, (int)waitersProp.GetValue(state)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,331 +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.Rtc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Rtc;
|
||||
|
||||
// libSceRtc is pure calendar/tick arithmetic, so it can be exercised end to end without a live
|
||||
// guest: the exports read their operands from CPU registers and guest memory and write results
|
||||
// back the same way. A "tick" is microseconds since 0001-01-01 (i.e. DateTime.Ticks / 10), which
|
||||
// is what these tests assert against.
|
||||
public sealed class RtcExportsTests
|
||||
{
|
||||
private const ulong Base = 0x1_0000_0000;
|
||||
private const ulong TimeAddress = Base + 0x100;
|
||||
private const ulong OutAddress = Base + 0x200;
|
||||
private const ulong TickAddress = Base + 0x300;
|
||||
private const ulong SecondTickAddress = Base + 0x400;
|
||||
|
||||
// Reference constants (verified against System.DateTime): microseconds since 0001-01-01.
|
||||
private const ulong UnixEpochTick = 62_135_596_800_000_000UL; // 1970-01-01T00:00:00
|
||||
private const ulong Y2KTick = 63_082_281_600_000_000UL; // 2000-01-01T00:00:00
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(Base, 0x10000);
|
||||
private readonly CpuContext _ctx;
|
||||
|
||||
public RtcExportsTests()
|
||||
{
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTick_UnixEpoch_MatchesReferenceTick()
|
||||
{
|
||||
WriteRtc(TimeAddress, 1970, 1, 1, 0, 0, 0, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = TickAddress;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcGetTick(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(TickAddress, out var tick));
|
||||
Assert.Equal(UnixEpochTick, tick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTick_Y2K_MatchesReferenceTick()
|
||||
{
|
||||
WriteRtc(TimeAddress, 2000, 1, 1, 0, 0, 0, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = TickAddress;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcGetTick(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(TickAddress, out var tick));
|
||||
Assert.Equal(Y2KTick, tick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTickThenSetTick_RoundTripsWithMicroseconds()
|
||||
{
|
||||
// A leap-day timestamp with sub-second precision stresses both the calendar math and the
|
||||
// microsecond field surviving the tick <-> struct conversion.
|
||||
WriteRtc(TimeAddress, 2020, 2, 29, 13, 45, 30, 123_456);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = TickAddress;
|
||||
Assert.Equal(0, RtcExports.RtcGetTick(_ctx));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = OutAddress;
|
||||
_ctx[CpuRegister.Rsi] = TickAddress;
|
||||
Assert.Equal(0, RtcExports.RtcSetTick(_ctx));
|
||||
|
||||
AssertRtc(OutAddress, 2020, 2, 29, 13, 45, 30, 123_456);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTimeT_ConvertsToUnixSeconds()
|
||||
{
|
||||
WriteRtc(TimeAddress, 2000, 1, 1, 0, 0, 0, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = OutAddress;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcGetTimeT(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(OutAddress, out var unixSeconds));
|
||||
Assert.Equal(946_684_800UL, unixSeconds); // well-known 2000-01-01 UTC unix timestamp
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTimeT_BeforeUnixEpoch_ClampsToZero()
|
||||
{
|
||||
WriteRtc(TimeAddress, 1960, 1, 1, 0, 0, 0, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = OutAddress;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcGetTimeT(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(OutAddress, out var unixSeconds));
|
||||
Assert.Equal(0UL, unixSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetTimeT_ConvertsUnixSecondsToDate()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = 946_684_800UL;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcSetTimeT(_ctx));
|
||||
AssertRtc(TimeAddress, 2000, 1, 1, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWin32FileTime_UnixEpoch_MatchesKnownFileTime()
|
||||
{
|
||||
WriteRtc(TimeAddress, 1970, 1, 1, 0, 0, 0, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = OutAddress;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcGetWin32FileTime(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(OutAddress, out var fileTime));
|
||||
Assert.Equal(116_444_736_000_000_000UL, fileTime); // FILETIME of the unix epoch (100ns units)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDosTime_PacksFields()
|
||||
{
|
||||
WriteRtc(TimeAddress, 2021, 6, 15, 13, 45, 30, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = OutAddress;
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcGetDosTime(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt32(OutAddress, out var dosTime));
|
||||
Assert.Equal(1_389_325_743U, dosTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetDosTimeThenGetDosTime_RoundTripsFieldsAndValue()
|
||||
{
|
||||
const uint dosValue = 1_389_325_743U; // 2021-06-15 13:45:30, even second => no 2s-resolution loss
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = dosValue;
|
||||
Assert.Equal(0, RtcExports.RtcSetDosTime(_ctx));
|
||||
AssertRtc(TimeAddress, 2021, 6, 15, 13, 45, 30, 0);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = OutAddress;
|
||||
Assert.Equal(0, RtcExports.RtcGetDosTime(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt32(OutAddress, out var packed));
|
||||
Assert.Equal(dosValue, packed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTickResolution_IsOneMicrosecond()
|
||||
{
|
||||
Assert.Equal(1_000_000, RtcExports.RtcGetTickResolution(_ctx));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2000, 1)] // divisible by 400
|
||||
[InlineData(2004, 1)] // divisible by 4
|
||||
[InlineData(2021, 0)]
|
||||
[InlineData(1900, 0)] // divisible by 100 but not 400
|
||||
public void IsLeapYear_MatchesGregorianRule(int year, int expected)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = (ulong)year;
|
||||
Assert.Equal(expected, RtcExports.RtcIsLeapYear(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsLeapYear_OutOfRange_ReturnsInvalidYear()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0;
|
||||
Assert.Equal(unchecked((int)0x80B50008), RtcExports.RtcIsLeapYear(_ctx));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2021, 2, 28)]
|
||||
[InlineData(2020, 2, 29)]
|
||||
[InlineData(2021, 4, 30)]
|
||||
[InlineData(2021, 12, 31)]
|
||||
public void GetDaysInMonth_ReturnsCalendarLength(int year, int month, int expected)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = (ulong)year;
|
||||
_ctx[CpuRegister.Rsi] = (ulong)month;
|
||||
Assert.Equal(expected, RtcExports.RtcGetDaysInMonth(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDaysInMonth_InvalidMonth_ReturnsInvalidMonthCode()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 2021;
|
||||
_ctx[CpuRegister.Rsi] = 13;
|
||||
Assert.Equal(unchecked((int)0x80B50009), RtcExports.RtcGetDaysInMonth(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDayOfWeek_ReturnsSundayZeroBasedIndex()
|
||||
{
|
||||
// 2021-06-15 is a Tuesday; DayOfWeek numbers Sunday as 0, so Tuesday == 2.
|
||||
_ctx[CpuRegister.Rdi] = 2021;
|
||||
_ctx[CpuRegister.Rsi] = 6;
|
||||
_ctx[CpuRegister.Rdx] = 15;
|
||||
Assert.Equal(2, RtcExports.RtcGetDayOfWeek(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDayOfWeek_InvalidDate_ReturnsError()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 2021;
|
||||
_ctx[CpuRegister.Rsi] = 2;
|
||||
_ctx[CpuRegister.Rdx] = 30; // February never has 30 days
|
||||
Assert.Equal(unchecked((int)0x80B50004), RtcExports.RtcGetDayOfWeek(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckValid_ValidDate_ReturnsOk()
|
||||
{
|
||||
WriteRtc(TimeAddress, 2021, 6, 15, 13, 45, 30, 500_000);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
Assert.Equal(0, RtcExports.RtcCheckValid(_ctx));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 6, 15, 12, 0, 0, 0, 0x80B50008)] // year out of range
|
||||
[InlineData(2021, 13, 15, 12, 0, 0, 0, 0x80B50009)] // month out of range
|
||||
[InlineData(2021, 2, 30, 12, 0, 0, 0, 0x80B5000A)] // day exceeds month length
|
||||
[InlineData(2021, 6, 15, 24, 0, 0, 0, 0x80B5000B)] // hour out of range
|
||||
[InlineData(2021, 6, 15, 12, 60, 0, 0, 0x80B5000C)] // minute out of range
|
||||
[InlineData(2021, 6, 15, 12, 0, 60, 0, 0x80B5000D)] // second out of range
|
||||
[InlineData(2021, 6, 15, 12, 0, 0, 1_000_000, 0x80B5000E)] // microsecond out of range
|
||||
public void CheckValid_InvalidField_ReturnsMatchingErrorCode(
|
||||
int year, int month, int day, int hour, int minute, int second, uint microsecond, long expected)
|
||||
{
|
||||
WriteRtc(TimeAddress, year, month, day, hour, minute, second, microsecond);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
Assert.Equal(unchecked((int)expected), RtcExports.RtcCheckValid(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareTick_OrdersByValue()
|
||||
{
|
||||
Assert.True(_ctx.TryWriteUInt64(TickAddress, 100));
|
||||
Assert.True(_ctx.TryWriteUInt64(SecondTickAddress, 200));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = TickAddress;
|
||||
_ctx[CpuRegister.Rsi] = SecondTickAddress;
|
||||
Assert.Equal(-1, RtcExports.RtcCompareTick(_ctx));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = SecondTickAddress;
|
||||
_ctx[CpuRegister.Rsi] = TickAddress;
|
||||
Assert.Equal(1, RtcExports.RtcCompareTick(_ctx));
|
||||
|
||||
_ctx[CpuRegister.Rsi] = SecondTickAddress;
|
||||
_ctx[CpuRegister.Rdi] = SecondTickAddress;
|
||||
Assert.Equal(0, RtcExports.RtcCompareTick(_ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TickAddDays_AdvancesByWholeDay()
|
||||
{
|
||||
Assert.True(_ctx.TryWriteUInt64(TickAddress, Y2KTick));
|
||||
_ctx[CpuRegister.Rdi] = OutAddress; // destination
|
||||
_ctx[CpuRegister.Rsi] = TickAddress; // source
|
||||
_ctx[CpuRegister.Rdx] = 1; // +1 day
|
||||
|
||||
Assert.Equal(0, RtcExports.RtcTickAddDays(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(OutAddress, out var result));
|
||||
Assert.Equal(Y2KTick + 86_400_000_000UL, result);
|
||||
}
|
||||
|
||||
// Regression test for the sceRtcConvertLocalTimeToUtc DateTimeKind bug: the guest tick was
|
||||
// decoded as Utc and then handed to TimeZoneInfo.ConvertTimeToUtc together with the (non-UTC)
|
||||
// local zone, which throws ArgumentException, so the export always returned INVALID_ARGUMENT on
|
||||
// any host not set to UTC. It must succeed and round-trip against sceRtcConvertUtcToLocalTime.
|
||||
[Fact]
|
||||
public void ConvertUtcToLocalTimeThenBack_RoundTrips()
|
||||
{
|
||||
// Noon in mid-June is never an invalid/ambiguous wall-clock time in any real zone, so the
|
||||
// conversion is an exact inverse regardless of the CI machine's local time zone.
|
||||
WriteRtc(TimeAddress, 2021, 6, 15, 12, 0, 0, 0);
|
||||
_ctx[CpuRegister.Rdi] = TimeAddress;
|
||||
_ctx[CpuRegister.Rsi] = TickAddress;
|
||||
Assert.Equal(0, RtcExports.RtcGetTick(_ctx));
|
||||
Assert.True(_ctx.TryReadUInt64(TickAddress, out var utcTick));
|
||||
|
||||
// UTC tick -> local tick
|
||||
_ctx[CpuRegister.Rdi] = TickAddress; // utc in
|
||||
_ctx[CpuRegister.Rsi] = SecondTickAddress; // local out
|
||||
Assert.Equal(0, RtcExports.RtcConvertUtcToLocalTime(_ctx));
|
||||
|
||||
// local tick -> UTC tick (the previously broken direction)
|
||||
_ctx[CpuRegister.Rdi] = SecondTickAddress; // local in
|
||||
_ctx[CpuRegister.Rsi] = OutAddress; // utc out
|
||||
Assert.Equal(0, RtcExports.RtcConvertLocalTimeToUtc(_ctx));
|
||||
|
||||
Assert.True(_ctx.TryReadUInt64(OutAddress, out var roundTripTick));
|
||||
Assert.Equal(utcTick, roundTripTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertLocalTimeToUtc_NullPointer_ReturnsError()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0;
|
||||
_ctx[CpuRegister.Rsi] = OutAddress;
|
||||
Assert.Equal(unchecked((int)0x80B50002), RtcExports.RtcConvertLocalTimeToUtc(_ctx));
|
||||
}
|
||||
|
||||
private void WriteRtc(ulong address, int year, int month, int day, int hour, int minute, int second, uint microsecond)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[0..2], (ushort)year);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[2..4], (ushort)month);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..6], (ushort)day);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[6..8], (ushort)hour);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], (ushort)minute);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], (ushort)second);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], microsecond);
|
||||
Assert.True(_memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private void AssertRtc(ulong address, int year, int month, int day, int hour, int minute, int second, uint microsecond)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
Assert.True(_memory.TryRead(address, buffer));
|
||||
Assert.Equal(year, BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]));
|
||||
Assert.Equal(month, BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]));
|
||||
Assert.Equal(day, BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]));
|
||||
Assert.Equal(hour, BinaryPrimitives.ReadUInt16LittleEndian(buffer[6..8]));
|
||||
Assert.Equal(minute, BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]));
|
||||
Assert.Equal(second, BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]));
|
||||
Assert.Equal(microsecond, BinaryPrimitives.ReadUInt32LittleEndian(buffer[12..16]));
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Content invariants over the compile-time generated export registry
|
||||
/// (SharpEmu.Generated.SysAbiExportRegistry), which is the runtime's sole registration
|
||||
/// source. Replaces the parity test that pinned the registry to the retired reflection
|
||||
/// scan while both existed; equality with the scan proved the swap, these pin what must
|
||||
/// stay true now that only the registry remains.
|
||||
/// </summary>
|
||||
public sealed class SysAbiRegistryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Generation.Gen4)]
|
||||
[InlineData(Generation.Gen5)]
|
||||
[InlineData(Generation.Gen4 | Generation.Gen5)]
|
||||
public void RegistryIsDuplicateFree(Generation generation)
|
||||
{
|
||||
var exports = SharpEmu.Generated.SysAbiExportRegistry.CreateExports(generation);
|
||||
var manager = new ModuleManager();
|
||||
|
||||
// RegisterExports skips NIDs it has already seen, so a shortfall here means the
|
||||
// generated table carries a duplicate the SHEM001 analyzer should have caught.
|
||||
Assert.Equal(exports.Count, manager.RegisterExports(exports));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegistryCoversTheFullExportSurface()
|
||||
{
|
||||
var exports = SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5);
|
||||
|
||||
// 715 exports existed when the registry replaced the scan; shrinkage means the
|
||||
// generator silently dropped handlers.
|
||||
Assert.True(exports.Count >= 715, $"registry shrank to {exports.Count} exports");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegistryResolvesKnownExportWithCatalogIdentity()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport("Zxa0VhQVTsk", out var export));
|
||||
Assert.Equal("sceKernelWaitSema", export.Name);
|
||||
Assert.Equal("libKernel", export.LibraryName);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +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;
|
||||
|
||||
public sealed class PngSplashLoaderTests
|
||||
{
|
||||
private const int IhdrCrcOffset = 29;
|
||||
private const int IdatCrcOffset = 53;
|
||||
private const int IendCrcOffset = 65;
|
||||
private const string ValidRgbPng =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGMQMgkDAAD4AJ3MaiF4AAAAAElFTkSuQmCC";
|
||||
|
||||
[Theory]
|
||||
[InlineData(IhdrCrcOffset)]
|
||||
[InlineData(IdatCrcOffset)]
|
||||
[InlineData(IendCrcOffset)]
|
||||
public void TryLoadRejectsInvalidChunkCrc(int crcOffset)
|
||||
{
|
||||
var app0Root = Path.Combine(Path.GetTempPath(), $"sharpemu-png-{Guid.NewGuid():N}");
|
||||
var sceSysPath = Path.Combine(app0Root, "sce_sys");
|
||||
var pngPath = Path.Combine(sceSysPath, "pic0.png");
|
||||
var previousApp0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
|
||||
Directory.CreateDirectory(sceSysPath);
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", app0Root);
|
||||
|
||||
var png = Convert.FromBase64String(ValidRgbPng);
|
||||
File.WriteAllBytes(pngPath, png);
|
||||
|
||||
Assert.True(PngSplashLoader.TryLoad(out var pixels, out var width, out var height));
|
||||
Assert.Equal(1U, width);
|
||||
Assert.Equal(1U, height);
|
||||
Assert.Equal([0x56, 0x34, 0x12, 0xFF], pixels);
|
||||
|
||||
png[crcOffset] ^= 0x01;
|
||||
File.WriteAllBytes(pngPath, png);
|
||||
|
||||
Assert.False(PngSplashLoader.TryLoad(out _, out _, out _));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", previousApp0Root);
|
||||
Directory.Delete(app0Root, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Microsoft.NET.Test.Sdk": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.14.1, )",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "17.14.1",
|
||||
"Microsoft.TestPlatform.TestHost": "17.14.1"
|
||||
}
|
||||
},
|
||||
"xunit": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.9.3, )",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.18.0",
|
||||
"xunit.assert": "2.9.3",
|
||||
"xunit.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.runner.visualstudio": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.1, )",
|
||||
"resolved": "3.1.1",
|
||||
"contentHash": "gNu2zhnuwjq5vQlU4S7yK/lfaKZDLmtcu+vTjnhfTlMAUYn+Hmgu8IIX0UCwWepYkk+Szx03DHx1bDnc9Fd+9w=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ=="
|
||||
},
|
||||
"Microsoft.TestPlatform.TestHost": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.TestPlatform.ObjectModel": "17.14.1",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Input.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"xunit.abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.18.0",
|
||||
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
||||
},
|
||||
"xunit.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||
},
|
||||
"xunit.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.3]",
|
||||
"xunit.extensibility.execution": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
||||
"dependencies": {
|
||||
"xunit.abstractions": "2.0.3"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.execution": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Iced": "[1.21.0, )",
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"SharpEmu.Libs": "[1.0.0, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Input": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Input": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Input.Common": "2.23.0",
|
||||
"Silk.NET.Input.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.SourceGenerators;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.SourceGenerators.Tests;
|
||||
|
||||
public sealed class Ps5NidTests
|
||||
{
|
||||
// Real pairs taken from [SysAbiExport] attributes in the emulator: the computation
|
||||
// must reproduce the catalog's NIDs exactly or every derived registration is wrong.
|
||||
[Theory]
|
||||
[InlineData("sceKernelWaitSema", "Zxa0VhQVTsk")]
|
||||
[InlineData("sceKernelSignalSema", "4czppHBiriw")]
|
||||
[InlineData("sceKernelCreateSema", "188x57JYp0g")]
|
||||
[InlineData("sceAudioOutOpen", "ekNvsT22rsY")]
|
||||
[InlineData("sceAudioOutOutput", "QOQtbeDqsT4")]
|
||||
[InlineData("memcpy", "Q3VBxCXhUHs")]
|
||||
[InlineData("memmove", "+P6FRGH4LfA")]
|
||||
public void ComputeMatchesKnownCatalogPairs(string exportName, string expectedNid) =>
|
||||
Assert.Equal(expectedNid, Ps5Nid.Compute(exportName));
|
||||
|
||||
[Theory]
|
||||
[InlineData("Zxa0VhQVTsk", true)]
|
||||
[InlineData("+P6FRGH4LfA", true)]
|
||||
[InlineData("4R6-OvI2cEA", true)]
|
||||
[InlineData("Zxa0VhQVTs", false)] // ten characters
|
||||
[InlineData("Zxa0VhQVTskk", false)] // twelve characters
|
||||
[InlineData("Zxa0VhQVTs=", false)] // padding character
|
||||
[InlineData("Zxa0VhQVT/k", false)] // '/' is remapped to '-' in this alphabet
|
||||
public void IsValidFormatChecksLengthAndAlphabet(string nid, bool expected) =>
|
||||
Assert.Equal(expected, Ps5Nid.IsValidFormat(nid));
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.SourceGenerators.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Builds in-memory compilations for driving the generator and analyzer: the test-host
|
||||
/// runtime assemblies plus the real SharpEmu.HLE (for SysAbiExportAttribute,
|
||||
/// ExportedFunction, and friends), so generated output is compiled against the exact
|
||||
/// types it will target in the emulator.
|
||||
/// </summary>
|
||||
internal static class RoslynTestHost
|
||||
{
|
||||
private static readonly Lazy<IReadOnlyList<MetadataReference>> References = new(static () =>
|
||||
{
|
||||
var references = new List<MetadataReference>();
|
||||
var trustedAssemblies = (string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!;
|
||||
foreach (var path in trustedAssemblies.Split(Path.PathSeparator))
|
||||
{
|
||||
if (path.Length != 0)
|
||||
{
|
||||
references.Add(MetadataReference.CreateFromFile(path));
|
||||
}
|
||||
}
|
||||
|
||||
references.Add(MetadataReference.CreateFromFile(typeof(SysAbiExportAttribute).Assembly.Location));
|
||||
return references;
|
||||
});
|
||||
|
||||
public static CSharpCompilation Compile(params string[] sources)
|
||||
{
|
||||
var trees = new SyntaxTree[sources.Length];
|
||||
for (var index = 0; index < sources.Length; index++)
|
||||
{
|
||||
trees[index] = CSharpSyntaxTree.ParseText(
|
||||
sources[index],
|
||||
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest));
|
||||
}
|
||||
|
||||
return CSharpCompilation.Create(
|
||||
"SysAbiGeneratorTests",
|
||||
trees,
|
||||
References.Value,
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
}
|
||||
|
||||
public static (Compilation Updated, string GeneratedSource) RunGenerator(CSharpCompilation compilation)
|
||||
{
|
||||
var driver = CSharpGeneratorDriver.Create(new SysAbiExportGenerator());
|
||||
driver.RunGeneratorsAndUpdateCompilation(compilation, out var updated, out _);
|
||||
var generated = string.Empty;
|
||||
foreach (var tree in updated.SyntaxTrees)
|
||||
{
|
||||
if (tree.FilePath.EndsWith("SysAbiExportRegistry.g.cs", StringComparison.Ordinal))
|
||||
{
|
||||
generated = tree.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return (updated, generated);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<Diagnostic> RunAnalyzer(
|
||||
CSharpCompilation compilation,
|
||||
params AdditionalText[] additionalFiles)
|
||||
{
|
||||
var withAnalyzers = compilation.WithAnalyzers(
|
||||
[new SysAbiExportAnalyzer()],
|
||||
new AnalyzerOptions([.. additionalFiles]));
|
||||
var diagnostics = withAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult();
|
||||
var result = new List<Diagnostic>(diagnostics.Length);
|
||||
foreach (var diagnostic in diagnostics)
|
||||
{
|
||||
result.Add(diagnostic);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void AssertCompiles(Compilation compilation)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
foreach (var diagnostic in compilation.GetDiagnostics())
|
||||
{
|
||||
if (diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
{
|
||||
errors.Add(diagnostic.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Xunit.Assert.True(errors.Count == 0, string.Join(Environment.NewLine, errors));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InMemoryAdditionalText(string path, string content) : AdditionalText
|
||||
{
|
||||
public override string Path { get; } = path;
|
||||
|
||||
public override SourceText GetText(CancellationToken cancellationToken = default) =>
|
||||
SourceText.From(content);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Self-contained (the repo's test-project convention): drives the generator and
|
||||
analyzer in-process with a Roslyn compilation built from inline sources, and
|
||||
references SharpEmu.HLE only to supply the real attribute/registry metadata the
|
||||
generated code compiles against. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,240 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.SourceGenerators.Tests;
|
||||
|
||||
public sealed class SysAbiExportAnalyzerTests
|
||||
{
|
||||
private static IReadOnlyList<Diagnostic> Analyze(string source, params AdditionalText[] additionalFiles) =>
|
||||
RoslynTestHost.RunAnalyzer(RoslynTestHost.Compile(source), additionalFiles);
|
||||
|
||||
private static void AssertSingle(IReadOnlyList<Diagnostic> diagnostics, string id)
|
||||
{
|
||||
Assert.Single(diagnostics);
|
||||
Assert.Equal(id, diagnostics[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CleanExportProducesNoDiagnostics()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema", Target = Generation.Gen5)]
|
||||
public static int WaitSema(CpuContext ctx) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuplicateNidIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
|
||||
public static int First(CpuContext ctx) => 0;
|
||||
|
||||
// Same NID via derivation: duplicates must be caught across both forms.
|
||||
[SysAbiExport(ExportName = "sceKernelWaitSema")]
|
||||
public static int Second(CpuContext ctx) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
AssertSingle(diagnostics, "SHEM001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MalformedNidIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "not_a_nid", ExportName = "sceKernelWaitSema")]
|
||||
public static int WaitSema(CpuContext ctx) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
AssertSingle(diagnostics, "SHEM002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrongSignatureIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
|
||||
public static void WaitSema(CpuContext ctx) { }
|
||||
}
|
||||
""");
|
||||
|
||||
AssertSingle(diagnostics, "SHEM003");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedHandlerShapeIsAccepted()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "12wOHk8ywb0", ExportName = "sceKernelPollSema")]
|
||||
public static int PollSema(CpuContext ctx, uint handle, int needCount) => 0;
|
||||
|
||||
[SysAbiExport(Nid = "4DM06U2BNEY", ExportName = "sceKernelCancelSema")]
|
||||
public static int CancelSema(CpuContext ctx, uint a, int b, ulong c, long d, uint e, int f) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedHandlerBeyondRegisterArgsOrWithUnsupportedTypeIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
// Seven args exceed the six SysV integer registers.
|
||||
[SysAbiExport(Nid = "12wOHk8ywb0", ExportName = "sceKernelPollSema")]
|
||||
public static int TooMany(CpuContext ctx, uint a, int b, ulong c, long d, uint e, int f, int g) => 0;
|
||||
|
||||
// string is not register-representable (that is phase 3's marshalling).
|
||||
[SysAbiExport(Nid = "4czppHBiriw", ExportName = "sceKernelSignalSema")]
|
||||
public static int WrongKind(CpuContext ctx, string name) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
// Order-insensitive: analyzers run concurrently and diagnostic order is unstable.
|
||||
Assert.Equal(2, diagnostics.Count);
|
||||
Assert.All(diagnostics, diagnostic => Assert.Equal("SHEM003", diagnostic.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuestCStringParameterIsAccepted()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
|
||||
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MisusedGuestCStringIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
// On a non-string parameter the attribute is meaningless.
|
||||
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
|
||||
public static int OnInt(CpuContext ctx, [GuestCString(4096)] int flags) => 0;
|
||||
|
||||
// A read bounded at zero bytes can never succeed.
|
||||
[SysAbiExport(Nid = "6c3rCVE-fTU", ExportName = "_open")]
|
||||
public static int ZeroLength(CpuContext ctx, [GuestCString(0)] string path) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
// Order-insensitive: analyzers run concurrently and diagnostic order is unstable.
|
||||
Assert.Equal(2, diagnostics.Count);
|
||||
Assert.All(diagnostics, diagnostic => Assert.Equal("SHEM008", diagnostic.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NidContradictingExportNameIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
// This NID belongs to sceKernelSignalSema, not sceKernelWaitSema.
|
||||
[SysAbiExport(Nid = "4czppHBiriw", ExportName = "sceKernelWaitSema")]
|
||||
public static int WaitSema(CpuContext ctx) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
AssertSingle(diagnostics, "SHEM004");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExportWithNeitherNidNorNameIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Target = Generation.Gen5)]
|
||||
public static int Mystery(CpuContext ctx) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
AssertSingle(diagnostics, "SHEM005");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NameOutsideTheCatalogWarnsOnlyWhenCatalogIsWired()
|
||||
{
|
||||
const string source = """
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(ExportName = "sceKernelWiatSema", Target = Generation.Gen5)]
|
||||
public static int Typo(CpuContext ctx) => 0;
|
||||
}
|
||||
""";
|
||||
|
||||
var withoutCatalog = Analyze(source);
|
||||
Assert.Empty(withoutCatalog);
|
||||
|
||||
var catalog = new InMemoryAdditionalText(
|
||||
"/repo/scripts/ps5_names.txt",
|
||||
"sceKernelWaitSema\nsceKernelSignalSema\n");
|
||||
var withCatalog = Analyze(source, catalog);
|
||||
AssertSingle(withCatalog, "SHEM006");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivateHandlerIsReported()
|
||||
{
|
||||
var diagnostics = Analyze("""
|
||||
using SharpEmu.HLE;
|
||||
|
||||
public static class Exports
|
||||
{
|
||||
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
|
||||
private static int Hidden(CpuContext ctx) => 0;
|
||||
}
|
||||
""");
|
||||
|
||||
AssertSingle(diagnostics, "SHEM007");
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.SourceGenerators.Tests;
|
||||
|
||||
public sealed class SysAbiExportGeneratorTests
|
||||
{
|
||||
private const string HandlerSource = """
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace TestExports;
|
||||
|
||||
public static class SampleExports
|
||||
{
|
||||
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||
public static int WaitSema(CpuContext ctx) => 0;
|
||||
|
||||
// NID omitted on purpose: the generator must derive it from the name.
|
||||
[SysAbiExport(ExportName = "sceKernelSignalSema", Target = Generation.Gen5)]
|
||||
public static int SignalSema(CpuContext ctx) => 0;
|
||||
|
||||
// Parameterless handler shape: must be wrapped to the SysAbiFunction contract.
|
||||
[SysAbiExport(Nid = "ekNvsT22rsY", ExportName = "sceAudioOutOpen")]
|
||||
public static int Open() => 0;
|
||||
|
||||
// Typed handler shape: the generator emits the SysV register thunk.
|
||||
[SysAbiExport(Nid = "12wOHk8ywb0", ExportName = "sceKernelPollSema")]
|
||||
public static int PollSema(CpuContext ctx, uint handle, int needCount) => 0;
|
||||
|
||||
// All four integer kinds across all six argument registers.
|
||||
[SysAbiExport(Nid = "4DM06U2BNEY", ExportName = "sceKernelCancelSema")]
|
||||
public static int CancelSema(CpuContext ctx, uint a, int b, ulong c, long d, uint e, int f) => 0;
|
||||
|
||||
// Guest string marshalling: the thunk reads the pointer before the handler.
|
||||
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
|
||||
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
|
||||
}
|
||||
""";
|
||||
|
||||
[Fact]
|
||||
public void GeneratedRegistryCompilesAgainstRealHleTypes()
|
||||
{
|
||||
var compilation = RoslynTestHost.Compile(HandlerSource);
|
||||
var (updated, generated) = RoslynTestHost.RunGenerator(compilation);
|
||||
|
||||
Assert.NotEqual(string.Empty, generated);
|
||||
RoslynTestHost.AssertCompiles(updated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegistryContainsDeclaredDerivedAndWrappedExports()
|
||||
{
|
||||
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
||||
|
||||
// Declared NID passes through verbatim.
|
||||
Assert.Contains("\"Zxa0VhQVTsk\"", generated, StringComparison.Ordinal);
|
||||
Assert.Contains("global::TestExports.SampleExports.WaitSema", generated, StringComparison.Ordinal);
|
||||
|
||||
// Omitted NID is derived from the export name at compile time.
|
||||
Assert.Contains("\"4czppHBiriw\"", generated, StringComparison.Ordinal);
|
||||
|
||||
// Parameterless handlers are adapted to the SysAbiFunction shape.
|
||||
Assert.Contains("static _ => global::TestExports.SampleExports.Open()", generated, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedHandlersGetSysVRegisterThunks()
|
||||
{
|
||||
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
||||
|
||||
// Parameters map positionally to RDI/RSI/... with the same unchecked-cast idiom
|
||||
// hand-written handlers use; ulong reads the register raw.
|
||||
Assert.Contains(
|
||||
"static ctx => global::TestExports.SampleExports.PollSema(ctx, " +
|
||||
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
|
||||
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]))",
|
||||
generated,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"static ctx => global::TestExports.SampleExports.CancelSema(ctx, " +
|
||||
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
|
||||
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]), " +
|
||||
"ctx[global::SharpEmu.HLE.CpuRegister.Rdx], " +
|
||||
"unchecked((long)ctx[global::SharpEmu.HLE.CpuRegister.Rcx]), " +
|
||||
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.R8]), " +
|
||||
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.R9]))",
|
||||
generated,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuestCStringParametersAreMarshalledWithAFaultPath()
|
||||
{
|
||||
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
||||
|
||||
// The string is read from the pointer in RDI before the handler runs, and a
|
||||
// failed read returns MEMORY_FAULT to the guest without invoking the handler.
|
||||
Assert.Contains(
|
||||
"if (!ctx.TryReadNullTerminatedUtf8(ctx[global::SharpEmu.HLE.CpuRegister.Rdi], 4096, out var guestString0))",
|
||||
generated,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"return ctx.SetReturn(global::SharpEmu.HLE.OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);",
|
||||
generated,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"return global::TestExports.SampleExports.KernelOpen(ctx, guestString0, " +
|
||||
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]));",
|
||||
generated,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerationFilteringMatchesTheReflectionScanSemantics()
|
||||
{
|
||||
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
||||
|
||||
// The Add helper reproduces ResolveExportInfo: None inherits the registration
|
||||
// generation, and exports outside the registration generation are skipped.
|
||||
Assert.Contains("attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget", generated, StringComparison.Ordinal);
|
||||
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssemblyWithoutExportsEmitsNoRegistry()
|
||||
{
|
||||
// Referencing the analyzer must not mint a colliding
|
||||
// SharpEmu.Generated.SysAbiExportRegistry type in export-free assemblies.
|
||||
const string noExports = """
|
||||
public static class PlainCode
|
||||
{
|
||||
public static int Nothing() => 0;
|
||||
}
|
||||
""";
|
||||
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(noExports));
|
||||
|
||||
Assert.Equal(string.Empty, generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidHandlersAreSkippedNotEmitted()
|
||||
{
|
||||
const string invalid = """
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace TestExports;
|
||||
|
||||
public static class BrokenExports
|
||||
{
|
||||
[SysAbiExport(ExportName = "sceKernelUsleep")]
|
||||
public static long WrongReturn(CpuContext ctx) => 0;
|
||||
|
||||
[SysAbiExport(ExportName = "sceKernelGettimeofday")]
|
||||
private static int Inaccessible(CpuContext ctx) => 0;
|
||||
}
|
||||
""";
|
||||
var (updated, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(invalid));
|
||||
|
||||
Assert.DoesNotContain("WrongReturn", generated, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Inaccessible", generated, StringComparison.Ordinal);
|
||||
RoslynTestHost.AssertCompiles(updated);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@
|
||||
// Synthetic-shader conformance dumper.
|
||||
//
|
||||
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
|
||||
// decode -> SPIR-V pipeline (SharpEmu.ShaderCompiler + SharpEmu.ShaderCompiler.Vulkan)
|
||||
// and writes the resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs
|
||||
// can then be checked with spirv-val / spirv-dis.
|
||||
// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
|
||||
// reflection so no emulator source changes are required) and writes the
|
||||
// resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be
|
||||
// checked with spirv-val / spirv-dis.
|
||||
//
|
||||
// Programs that contain buffer_store_dword automatically get a single
|
||||
// global-memory binding covering every store, which the emitter exposes as
|
||||
@@ -20,9 +21,9 @@
|
||||
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Reflection;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using SharpEmu.Libs.CxxAbi;
|
||||
|
||||
const ulong ProgramAddress = 0x100000;
|
||||
|
||||
@@ -137,6 +138,44 @@ const ulong ProgramAddress = 0x100000;
|
||||
]),
|
||||
];
|
||||
|
||||
var assembly = typeof(CxaGuardExports).Assembly;
|
||||
var shaderTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderTranslator")
|
||||
?? throw new InvalidOperationException("Gen5ShaderTranslator not found");
|
||||
var spirvTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5SpirvTranslator")
|
||||
?? throw new InvalidOperationException("Gen5SpirvTranslator not found");
|
||||
var describe = shaderTranslator.GetMethod(
|
||||
"Describe",
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Gen5ShaderTranslator.Describe not found");
|
||||
var tryDecode = shaderTranslator.GetMethod(
|
||||
"TryDecodeProgram",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Gen5ShaderTranslator.TryDecodeProgram not found");
|
||||
var stateType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderState")
|
||||
?? throw new InvalidOperationException("Gen5ShaderState not found");
|
||||
var evaluationType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderEvaluation")
|
||||
?? throw new InvalidOperationException("Gen5ShaderEvaluation not found");
|
||||
var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding")
|
||||
?? throw new InvalidOperationException("Gen5ImageBinding not found");
|
||||
var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
|
||||
?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
|
||||
var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding")
|
||||
?? throw new InvalidOperationException("Gen5PixelOutputBinding not found");
|
||||
var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind")
|
||||
?? throw new InvalidOperationException("Gen5PixelOutputKind not found");
|
||||
var tryCompile = spirvTranslator.GetMethod(
|
||||
"TryCompileVertexShader",
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
|
||||
var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.Single(method =>
|
||||
method.Name == "TryCompilePixelShader" &&
|
||||
method.GetParameters()[2].ParameterType.IsGenericType);
|
||||
var tryCompileCompute = spirvTranslator.GetMethod(
|
||||
"TryCompileComputeShader",
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileComputeShader not found");
|
||||
|
||||
var outputDirectory = args.Length > 0
|
||||
? args[0]
|
||||
: Path.Combine(AppContext.BaseDirectory, "spv");
|
||||
@@ -151,18 +190,19 @@ foreach (var (name, expectTranslate, words) in testPrograms)
|
||||
|
||||
Console.WriteLine(
|
||||
$"[{name}] decode: " +
|
||||
Gen5ShaderTranslator.Describe(ctx, ProgramAddress, ProgramAddress));
|
||||
(string)describe.Invoke(null, [ctx, ProgramAddress, ProgramAddress])!);
|
||||
|
||||
if (!Gen5ShaderTranslator.TryDecodeProgram(ctx, ProgramAddress, out var program, out var decodeError))
|
||||
object?[] decodeArgs = [ctx, ProgramAddress, null, null];
|
||||
if (!(bool)tryDecode.Invoke(null, decodeArgs)!)
|
||||
{
|
||||
if (expectTranslate)
|
||||
{
|
||||
failures++;
|
||||
Console.WriteLine($"[{name}] FAILED: decode error ({decodeError})");
|
||||
Console.WriteLine($"[{name}] FAILED: decode error ({decodeArgs[3]})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[{name}] decode failed as expected ({decodeError})");
|
||||
Console.WriteLine($"[{name}] decode failed as expected ({decodeArgs[3]})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -179,110 +219,167 @@ foreach (var (name, expectTranslate, words) in testPrograms)
|
||||
|
||||
// Buffer stores need a global-memory binding; the emitter resolves them by
|
||||
// instruction PC, so collect store PCs from the decoded program itself.
|
||||
var programObj = decodeArgs[2]!;
|
||||
var instructions = (System.Collections.IEnumerable)programObj
|
||||
.GetType().GetProperty("Instructions")!.GetValue(programObj)!;
|
||||
var storePcs = new List<uint>();
|
||||
foreach (var instruction in program!.Instructions)
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (instruction.Opcode.StartsWith("BufferStore", StringComparison.Ordinal))
|
||||
var op = (string)instruction.GetType().GetProperty("Opcode")!.GetValue(instruction)!;
|
||||
if (op.StartsWith("BufferStore", StringComparison.Ordinal))
|
||||
{
|
||||
storePcs.Add(instruction.Pc);
|
||||
storePcs.Add((uint)instruction.GetType().GetProperty("Pc")!.GetValue(instruction)!);
|
||||
}
|
||||
}
|
||||
|
||||
// The binding's scalar base (8 -> s[8:11]) must match the srsrc field of
|
||||
// the hand-assembled buffer_store words, and the 64-byte backing store
|
||||
// must cover every hand-assembled store offset.
|
||||
var globalBindings = storePcs.Count > 0
|
||||
? new[] { new Gen5GlobalMemoryBinding(8u, 0UL, storePcs, new byte[64]) }
|
||||
: Array.Empty<Gen5GlobalMemoryBinding>();
|
||||
var globalBindings = Array.CreateInstance(globalBindingType, storePcs.Count > 0 ? 1 : 0);
|
||||
if (storePcs.Count > 0)
|
||||
{
|
||||
globalBindings.SetValue(
|
||||
Activator.CreateInstance(
|
||||
globalBindingType,
|
||||
8u,
|
||||
0UL,
|
||||
(IReadOnlyList<uint>)storePcs,
|
||||
new byte[64]),
|
||||
0);
|
||||
}
|
||||
|
||||
var state = new Gen5ShaderState(program, new uint[16], Metadata: null);
|
||||
var evaluation = new Gen5ShaderEvaluation(
|
||||
var state = Activator.CreateInstance(
|
||||
stateType,
|
||||
programObj,
|
||||
new uint[16],
|
||||
null,
|
||||
null,
|
||||
0u)!;
|
||||
var evaluation = Activator.CreateInstance(
|
||||
evaluationType,
|
||||
new uint[256],
|
||||
new uint[256],
|
||||
new Dictionary<uint, IReadOnlyList<uint>>(),
|
||||
Array.Empty<Gen5ImageBinding>(),
|
||||
globalBindings);
|
||||
Array.CreateInstance(imageBindingType, 0),
|
||||
globalBindings,
|
||||
null,
|
||||
null,
|
||||
null)!;
|
||||
|
||||
if (Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var vertexShader, out var vertexError))
|
||||
var compileArgs = PadWithDefaults(tryCompile, [state, evaluation, null, null]);
|
||||
if ((bool)tryCompile.Invoke(null, BindingFlags.OptionalParamBinding, null, compileArgs, null)!)
|
||||
{
|
||||
var shader = compileArgs[2]!;
|
||||
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||
var path = Path.Combine(outputDirectory, $"{name}.spv");
|
||||
File.WriteAllBytes(path, vertexShader.Spirv);
|
||||
Console.WriteLine($"[{name}] emit: success, {vertexShader.Spirv.Length} bytes -> {path}");
|
||||
File.WriteAllBytes(path, spirv);
|
||||
Console.WriteLine($"[{name}] emit: success, {spirv.Length} bytes -> {path}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures++;
|
||||
Console.WriteLine($"[{name}] emit: FAILED ({vertexError})");
|
||||
Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
|
||||
}
|
||||
|
||||
if (Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, 1, 1, 1, out var computeShader, out var computeError))
|
||||
var computeArgs = PadWithDefaults(tryCompileCompute, [state, evaluation, 1u, 1u, 1u, null, null]);
|
||||
if ((bool)tryCompileCompute.Invoke(null, BindingFlags.OptionalParamBinding, null, computeArgs, null)!)
|
||||
{
|
||||
var shader = computeArgs[5]!;
|
||||
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||
var path = Path.Combine(outputDirectory, $"{name}-cs.spv");
|
||||
File.WriteAllBytes(path, computeShader.Spirv);
|
||||
Console.WriteLine($"[{name}] compute emit: success, {computeShader.Spirv.Length} bytes -> {path}");
|
||||
File.WriteAllBytes(path, spirv);
|
||||
Console.WriteLine($"[{name}] compute emit: success, {spirv.Length} bytes -> {path}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures++;
|
||||
Console.WriteLine($"[{name}] compute emit: FAILED ({computeError})");
|
||||
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
|
||||
}
|
||||
|
||||
if (name.StartsWith("mrt", StringComparison.Ordinal))
|
||||
{
|
||||
Gen5PixelOutputBinding[] pixelOutputs = name switch
|
||||
(uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch
|
||||
{
|
||||
"mrt" =>
|
||||
[
|
||||
new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(3, 1, Gen5PixelOutputKind.Uint),
|
||||
new Gen5PixelOutputBinding(6, 2, Gen5PixelOutputKind.Sint),
|
||||
],
|
||||
"mrt-float2" =>
|
||||
[
|
||||
new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(1, 1, Gen5PixelOutputKind.Float),
|
||||
],
|
||||
"mrt8" =>
|
||||
[
|
||||
new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(1, 1, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(2, 2, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(3, 3, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(4, 4, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(5, 5, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(6, 6, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(7, 7, Gen5PixelOutputKind.Float),
|
||||
],
|
||||
_ => [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
|
||||
"mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[]
|
||||
{
|
||||
(0, 0, "Float"),
|
||||
(3, 1, "Uint"),
|
||||
(6, 2, "Sint"),
|
||||
},
|
||||
"mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")],
|
||||
"mrt8" => Enumerable.Range(0, 8)
|
||||
.Select(index => ((uint)index, (uint)index, "Float"))
|
||||
.ToArray(),
|
||||
_ => [(0, 0, "Float")],
|
||||
};
|
||||
|
||||
if (Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, pixelOutputs, out var pixelShader, out var pixelError))
|
||||
var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length);
|
||||
for (var index = 0; index < outputSpecs.Length; index++)
|
||||
{
|
||||
var spec = outputSpecs[index];
|
||||
pixelOutputs.SetValue(
|
||||
Activator.CreateInstance(
|
||||
pixelOutputBindingType,
|
||||
spec.GuestSlot,
|
||||
spec.HostLocation,
|
||||
Enum.Parse(pixelOutputKindType, spec.Kind)),
|
||||
index);
|
||||
}
|
||||
|
||||
var pixelArgs = PadWithDefaults(
|
||||
tryCompilePixel,
|
||||
[state, evaluation, pixelOutputs, null, null]);
|
||||
if ((bool)tryCompilePixel.Invoke(
|
||||
null,
|
||||
BindingFlags.OptionalParamBinding,
|
||||
null,
|
||||
pixelArgs,
|
||||
null)!)
|
||||
{
|
||||
var shader = pixelArgs[3]!;
|
||||
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||
var path = Path.Combine(outputDirectory, $"{name}-ps.spv");
|
||||
File.WriteAllBytes(path, pixelShader.Spirv);
|
||||
Console.WriteLine($"[{name}] pixel emit: success, {pixelShader.Spirv.Length} bytes -> {path}");
|
||||
File.WriteAllBytes(path, spirv);
|
||||
Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures++;
|
||||
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelError})");
|
||||
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})");
|
||||
}
|
||||
|
||||
if (name == "mrt")
|
||||
{
|
||||
Gen5PixelOutputBinding[] invalidOutputs =
|
||||
[
|
||||
new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
|
||||
new Gen5PixelOutputBinding(3, 7, Gen5PixelOutputKind.Float),
|
||||
];
|
||||
if (Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, invalidOutputs, out _, out var invalidError))
|
||||
var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2);
|
||||
invalidOutputs.SetValue(
|
||||
Activator.CreateInstance(
|
||||
pixelOutputBindingType,
|
||||
0u,
|
||||
0u,
|
||||
Enum.Parse(pixelOutputKindType, "Float")),
|
||||
0);
|
||||
invalidOutputs.SetValue(
|
||||
Activator.CreateInstance(
|
||||
pixelOutputBindingType,
|
||||
3u,
|
||||
7u,
|
||||
Enum.Parse(pixelOutputKindType, "Float")),
|
||||
1);
|
||||
var invalidPixelArgs = PadWithDefaults(
|
||||
tryCompilePixel,
|
||||
[state, evaluation, invalidOutputs, null, null]);
|
||||
if ((bool)tryCompilePixel.Invoke(
|
||||
null,
|
||||
BindingFlags.OptionalParamBinding,
|
||||
null,
|
||||
invalidPixelArgs,
|
||||
null)!)
|
||||
{
|
||||
failures++;
|
||||
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidError})");
|
||||
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,6 +390,37 @@ Console.WriteLine(failures == 0
|
||||
: $"RESULT: {failures} unexpected outcome(s)");
|
||||
Environment.ExitCode = failures == 0 ? 0 : 1;
|
||||
|
||||
// Reflection Invoke does not apply C# default parameter values, so a newly
|
||||
// added optional parameter on a translator entry point would otherwise throw
|
||||
// TargetParameterCountException. Type.Missing + OptionalParamBinding lets the
|
||||
// runtime substitute the declared defaults; only a new *required* parameter
|
||||
// should force a tool update.
|
||||
static object?[] PadWithDefaults(MethodInfo method, object?[] arguments)
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
if (arguments.Length > parameters.Length)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{method.DeclaringType?.Name}.{method.Name} takes fewer parameters than the tool supplies");
|
||||
}
|
||||
|
||||
var padded = new object?[parameters.Length];
|
||||
arguments.CopyTo(padded, 0);
|
||||
for (var i = arguments.Length; i < padded.Length; i++)
|
||||
{
|
||||
if (!parameters[i].IsOptional)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{method.DeclaringType?.Name}.{method.Name} gained a required parameter " +
|
||||
$"'{parameters[i].Name}' — the tool needs updating");
|
||||
}
|
||||
|
||||
padded[i] = Type.Missing;
|
||||
}
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
internal sealed class FakeMemory : ICpuMemory
|
||||
{
|
||||
private readonly List<(ulong Base, byte[] Data)> _regions = [];
|
||||
|
||||
@@ -12,11 +12,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- References only the shader-compiler projects: the conformance tool is
|
||||
emulator-independent by design. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user