Compare commits

...

17 Commits

Author SHA1 Message Date
ParantezTech 940557ed4a [CI] fix asset 2026-07-16 01:03:43 +03:00
en-he f69fdd4027 Fix PNG chunk CRC validation (#241) 2026-07-16 00:46:43 +03:00
Cloudy 9eef39549a hle: implement offline NP reachability state (#238) 2026-07-16 00:35:18 +03:00
Nicola Pomarico 90651f26a9 Fix macOS exact-address mmap falling back to MAP_FIXED (#214)
The PS5 image loader requires guest images to land at fixed virtual
addresses (e.g. the 32 GiB main image base). On macOS the exact-address
mmap path only ever passed that address as a hint, never MAP_FIXED, to
avoid clobbering untracked host mappings (dyld, JIT heap, Rosetta).

On Apple Silicon under Rosetta 2 the kernel does not reliably honor
that hint, so the allocation failed deterministically before a single
guest instruction ran (reported in #194). Retry with true MAP_FIXED
as a fallback only when the hint-only attempt doesn't land at the
requested address, so the safer hint path is still tried first.
2026-07-16 00:28:39 +03:00
kostyaff b5930465f2 [AGC] Correct VReadlaneB32 decode and emission (#237)
VReadlaneB32 (VOP3 0x360) had two bugs:

1. Decode: VOP3 decode always set destinations = Vector(word & 0xFF).
   For VReadlaneB32, bits 0-7 are unused — the scalar destination is
   in bits 8-14. Now decodes as Scalar((word >> 8) & 0x7F).

2. Emission: VReadlaneB32 was in the VMovB32 fall-through group,
   just returning GetRawSource(instruction, 0) — reading the current
   lane's value. By ISA, sdst = vsrc0[lane(src1)], which requires
   reading a different lane's value. Now uses SPIR-V
   GroupNonUniformBroadcast(scope=Subgroup, value=src0, lane=src1)
   when subgroup operations are available, with a fallback to the
   current-lane simplification when not.

3. Routing: TryEmitVectorAlu called TryGetVectorDestination first,
   which checks for VectorRegister kind. With the scalar destination
   fix, VReadlaneB32 now routes to a new TryEmitReadlane handler
   before the vector destination check.

4. Subgroup capability: UsesSubgroupShuffle now includes VReadlaneB32
   so GroupNonUniform capability is enabled when needed.

Verified: dotnet build 0 errors/0 warnings, 26/26 tests pass,
ShaderDump all programs behaved as expected.
2026-07-16 00:27:26 +03:00
brbrhuehue-matrix 83303e953e Update Brazilian Portuguese translation (#233) 2026-07-16 00:26:08 +03:00
kostyaff 92497689ab [AGC] Fix VOP3 decode for V_READLANE_B32 and V_WRITELANE_B32 (#232)
PR #200 moved shader files from SharpEmu.Libs/Agc/ to new projects
SharpEmu.ShaderCompiler and SharpEmu.ShaderCompiler.Vulkan. This
re-ports the VOP3 decode fix from PR #226 to the new file paths.

Decode table (Gen5ShaderTranslator.cs):
- 0x360: VMadU32U16 -> VReadlaneB32 (per RDNA2 ISA)
- 0x361: VMulLoU32 -> VWritelaneB32 (0x361 was a duplicate of 0x169)
- 0x373: added VMadU32U16 at its correct opcode

Emission (Gen5SpirvTranslator.Alu.cs):
- VWritelaneB32: per-lane conditional write via IEqual+Select,
  stores with guardWithExec:false (writelane bypasses exec mask)
- VReadlaneB32: kept as GetRawSource(instruction, 0) simplification
  (correct emission with src1 lane select is a follow-up)

Verified: dotnet build 0 errors/0 warnings, 26/26 tests pass,
ShaderDump all programs behaved as expected.
2026-07-16 00:25:59 +03:00
Gutemberg Ribeiro de7973af55 [CI] Fix PR artifact-links comment for fork PRs (#227)
The workflow resolved the PR from workflow_run.pull_requests or the head
commit, both of which come back empty for PRs from forks — so it logged
"No open PR for this build" and never commented. Resolve the PR by its
head "owner:branch" (from workflow_run.head_repository/head_branch),
which works for forks, keeping the commit-association path as a fallback.
2026-07-16 00:25:21 +03:00
s1099 3da7ae7d70 Update repo URL (#224) 2026-07-16 00:24:14 +03:00
Jack Del Aguila 1be009ce40 [HLE] Fix POSIX condition variable semantics (#113) (#223)
* [HLE] Trigger AGC graphics events by filter instead of exact ident (#173)

The PM4 EVENT_WRITE packet carries a 6-bit hardware EVENT_TYPE, but the
guest registers AGC events via sceAgcDriverAddEqEvent with a full guest
eventId. These two values are not the same numbering scheme, so the exact
ident lookup in TriggerRegisteredEvents never matched and the AGC
interrupt thread hung forever.

Add TriggerRegisteredEventsByFilter, which wakes every graphics event
registration on every queue. This is a compatibility workaround for
issue #173 while the real PS5 mapping remains unknown.

Includes unit tests covering the mismatched ident/eventType case.

* [HLE] Fix POSIX condition variable semantics (#113)

Remove PendingSignals from PthreadCondState. POSIX condition signals are edges,
not semaphore credits - a signal with no waiter must have no effect. The previous
implementation persisted signals, causing lock inversions and predicate bypasses.

Changes:
- Remove PendingSignals property and TryConsumePendingSignal method
- Remove pending signal consumption logic from PthreadCondWaitCore
- Remove PendingSignals increment from PthreadCondSignalCore
- Add regression tests verifying POSIX-correct behavior

Fixes #113
2026-07-16 00:24:05 +03:00
999sian ad92ab30fd fix(gui): respect case-sensitive Linux paths (#218) 2026-07-16 00:17:31 +03:00
can 5f97d2edc4 Fix sceKernelGetTscFrequency disagreeing with the counter on non-Windows hosts (#213)
sceKernelReadTsc only returns the CPU's RDTSC when the host RDTSC reader is
available (currently 64-bit Windows); on Linux and macOS it falls back to the
QPC-based Stopwatch. ResolveKernelTscFrequency, however, still consulted the
CPUID-reported hardware TSC frequency in that case, so sceKernelGetTscFrequency
reported a multi-GHz rate while ReadTsc was ticking at the Stopwatch frequency.
A guest computing elapsed = readTscDelta / frequency then gets the wrong time
on those platforms.

Gate the calibrated/CPUID frequencies on RDTSC actually being available (the
calibration path was already self-gated; the CPUID path was not) and otherwise
report the Stopwatch frequency, keeping ReadTsc and GetTscFrequency consistent.

The selection logic is extracted into a pure, host-independent helper so both
branches can be unit tested, including a regression test asserting that a host
without RDTSC reports the Stopwatch frequency rather than the hardware TSC.
2026-07-16 00:11:46 +03:00
Eyoatam Wubeshet 5c8ace82c0 Add missing French translations for Environment and About sections (#206)
Fills in the untranslated keys under Options.Env.* and About.* so the
French locale matches en.json. Tried to keep the wording consistent with
the rest of the file.

Co-authored-by: Rick21-bit <Rick21-bit@users.noreply.github.com>
2026-07-16 00:09:16 +03:00
can 341c2a0cb6 Fix sceRtcConvertLocalTimeToUtc failing on non-UTC hosts (#210)
The guest local tick was decoded into a DateTimeKind.Utc DateTime and then
passed to TimeZoneInfo.ConvertTimeToUtc together with TimeZoneInfo.Local.
That overload throws ArgumentException when a Utc-kind value is paired with a
source zone other than UTC, so on any machine whose local zone is not UTC the
export caught the exception and always returned INVALID_ARGUMENT.

Re-tag the decoded value as DateTimeKind.Unspecified so it is interpreted as
local wall-clock time and converted correctly. The reverse direction
(sceRtcConvertUtcToLocalTime) was already correct because ConvertTimeFromUtc
accepts a Utc-kind input.

Add a RtcExports unit-test suite covering the tick/calendar conversions, DOS
time packing, Win32 file time, leap-year and validation error codes, and a
regression test that round-trips a UTC tick through local time and back.
2026-07-16 00:04:35 +03:00
Gutemberg Ribeiro 320dbcacba [SourceGenerators] Compile-time SysAbi export registry, analyzers, and build-generated aerolib.bin (#204)
* [SourceGenerators] Add the SysAbi export generator and analyzers (phase 0)

New SharpEmu.SourceGenerators Roslyn component, complete and tested but
consumed by nothing yet — the emulator projects adopt it in the
following commits.

Ps5Nid ports the PS NID derivation (base64 of the byte-reversed first
eight SHA1 bytes of name + fixed suffix) from
scripts/generate_aerolib_binary.py to C#, so what has always been a
manual, out-of-band computation becomes a compile-time capability.

SysAbiExportGenerator emits a per-assembly SysAbiExportRegistry whose
CreateExports(Generation) reproduces ModuleManager's reflection scan
exactly — same generation inheritance and filtering, same method-name
fallback, same libKernel default — with attribute-omitted NIDs derived
algorithmically (equivalent to the runtime catalog lookup, which is
built from the same computation). Parameterless handlers are adapted to
the SysAbiFunction shape; invalid declarations are skipped here because
the analyzer rejects them as build errors, so nothing drops silently.

SysAbiExportAnalyzer turns the runtime failure modes into diagnostics:
SHEM001 duplicate NID (across declared and derived forms), SHEM002
malformed NID, SHEM003 uncallable handler signature, SHEM004 NID
contradicting its export name (the class of drift previously fixed by
hand), SHEM005 unresolvable export, SHEM006 export name unknown to
ps5_names.txt when the catalog is wired as an AdditionalFile, SHEM007
handler not reachable by generated code.

The self-contained test suite drives both in-process against the real
SharpEmu.HLE metadata: known catalog NID pairs pin the algorithm, the
generated registry must itself compile, and each diagnostic has a
triggering fixture. Fittingly, the NID pinning test caught a wrong
pair in its own first draft — the exact mistake SHEM004 exists to stop.

* [SourceGenerators] Adopt the generated export registry in the emulator (phase 1)

SharpEmu.Libs consumes the generator and analyzers, with
scripts/ps5_names.txt wired as the AdditionalFile catalog. The runtime
now registers exports from the compile-time SysAbiExportRegistry
instead of the boot-time reflection scan; RegisterFromAssembly is
retained solely as the arbiter for a parity test that pins the two
tables identical — same NIDs, names, libraries, targets, and handler
methods — across Gen4, Gen5, and combined registration.

First contact between the analyzer and all 715 existing exports
surfaced real drift the old offline checker structurally missed
(scripts/check_sysabi_aerolib.py skipped any NID absent from
aerolib.bin): three exports whose friendly names collide with real
catalog symbols of different NIDs, now suppressed at-site with reasons
pending AGC API confirmation, alongside the established synthetic
Unknown* labels for uncatalogued NIDs, which prompted a rule
refinement — SHEM004 only hard-errors when the export name is a real
catalog symbol, since synthetic labels cannot be validated by hashing
and the NID is authoritative for them. The two allowlisted mismatches
in the python checker no longer trigger anything, and the checker is
deleted: the analyzer subsumes it with the semantic model instead of
regex, and validates every declared pair rather than only
catalog-known NIDs.

* [SourceGenerators] Generate aerolib.bin at build time from ps5_names.txt

The runtime NID -> name catalog is derived data and no longer lives in
the repository: a Framework-only MSBuild task (GenerateAerolibBinaryTask,
sharing the same Ps5Nid implementation the analyzers use) builds it
into the intermediate directory from scripts/ps5_names.txt — now the
single source of truth — and SharpEmu.HLE embeds it from there. The
output is byte-identical to the previously committed binary, verified
with cmp against git history; a new test pins that the embedded catalog
loads and resolves a known symbol both directions.

scripts/generate_aerolib_binary.py is deleted (its algorithm lives in
Ps5Nid, its invocation in the build); the REUSE annotation for the
binary goes with it. MSBuild's Inputs/Outputs check means the ~154k NID
hashes only recompute when the names file actually changes. The task
implements ITask against Microsoft.Build.Framework directly, keeping
the vulnerable-flagged Utilities.Core package out and the analyzer
project's file-IO ban suppressed only inside the task itself.

* [SourceGenerators] Emit typed-signature register thunks (phase 2)

[SysAbiExport] handlers can now be written with real signatures — a
CpuContext followed by up to six int/uint/long/ulong parameters — and
the generator emits the SysV unmarshalling thunk, mapping parameters
positionally to RDI/RSI/RDX/RCX/R8/R9 with the same unchecked-cast
idiom hand-written handlers use. SHEM003 accepts the new shape and
rejects register overflow and non-register-representable types. Both
shapes coexist, so migration is per-handler; sceKernelPollSema,
sceKernelSignalSema, and sceKernelCancelSema migrate as the
demonstration (the last showing raw ulong guest-address passthrough).

The reflection scan cannot represent typed handlers, so it retires
here: RegisterFromAssembly, its signature validation, and
ResolveExportInfo are deleted, and the parity test that pinned the
generated registry to the scan is replaced by content-invariant tests
(duplicate-free, full 715-export surface, catalog identity). Deleting
the scan surfaced a phase-1 latent regression — the pre-JIT warm sweep
enumerated only reflection-scanned assemblies, so the generated
registration path warmed nothing and re-exposed the guest-thread
fail-fast risk; the warm set is now derived from the registered
handler delegates themselves.

* [SourceGenerators] Marshal guest strings declaratively with [GuestCString] (phase 3)

A string parameter on a typed [SysAbiExport] handler, annotated
[GuestCString(maxLength)], now makes the generated thunk read the
null-terminated UTF-8 string from the argument register's guest
address before the handler runs, returning
ORBIS_GEN2_ERROR_MEMORY_FAULT to the guest when the read fails —
the exact prologue nearly every string-taking handler writes by hand.
The attribute lives in SharpEmu.HLE next to SysAbiExportAttribute;
SHEM008 rejects misuse (non-string parameter, non-positive MaxLength)
while a bare string parameter stays a SHEM003 signature error.

_open, open, and sceKernelOpen migrate as the demonstration; they were
chosen because their hand-written prologue faulted on a null pointer
the same way the thunk does (handlers that return INVALID_ARGUMENT for
null pointers, like sceKernelCreateSema, keep the raw shape so guest-
visible semantics stay untouched).

* [SourceGenerators] Apply review findings across the branch

Behavior: the open/_open/sceKernelOpen [GuestCString] demo migration is
reverted — the local compat reader falls back to host memory for paths
in loader-mapped regions that ctx.Memory cannot see, so the generated
thunk would have turned recoverable reads into MEMORY_FAULT. The
marshalling infrastructure stays, proven by generator/analyzer tests;
production migration waits for a handler whose semantics the thunk
reproduces exactly. A comment on the handler records why.

Build robustness: the aerolib target is 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 the task/names
paths are centralized in properties. The generator now emits no
registry for export-free assemblies, so referencing the analyzer can
never mint a colliding SharpEmu.Generated type.

Cleanup and perf: the pragma-suppression sites left mis-indented by the
phase-1 relocation are reformatted and the restores moved after the
method body; the dead ExportsForTesting hook and its InternalsVisibleTo
are deleted; the aerolib task reuses one SHA1 instance across ~150k
names; the analyzer caches the parsed catalog per file snapshot instead
of re-parsing 150k lines every compilation start, shares the attribute
name constant with the generator, and computes the catalog-membership
check once.

* [CI] Run the test suites in the build workflow

The workflow compiled the test projects (they are in SharpEmu.slnx) but
never executed them. A solution-level dotnet test now runs between
build and publish, so any test failure fails the build — including the
AerolibCatalogTests/SysAbiRegistryTests that guard the build-generated
aerolib.bin and the generated export registry. Generation failures of
aerolib.bin itself already fail the build step: the MSBuild task logs
an error event and returns false, and a missing task assembly or
missing embedded output are hard MSBuild errors. The NuGet cache key
now also tracks the test projects' lock files.

* [SourceGenerators] Address review feedback

Multi-diagnostic analyzer tests no longer assume a stable diagnostic
order (analyzer execution is concurrent), and the aerolib task logs
the full exception instead of only its message so build failures keep
the type and stack trace.

* [SourceGenerators] Address second review round

Symbol-name comparisons in the shape rules and analyzer now pin an
explicit SymbolDisplayFormat.FullyQualifiedFormat instead of relying on
the display-format default, and the aerolib task fails loudly on a
symbol name that would overflow the format's ushort length prefix
instead of silently truncating it, with null-safe output-directory
handling made explicit.

* [SourceGenerators] Embed aerolib.bin via a target so design-time builds never reference it

The static EmbeddedResource item referenced the generated file even in
design-time builds, where the generation target is skipped — on a fresh
clone the IDE would try to embed a file that never existed. The item is
now created inside an EmbedAerolibBinary target gated on
DesignTimeBuild, separate from the generation target so an up-to-date
skip of GenerateAerolibBinary cannot drop the item with the rest of its
body, and hooked before AssignTargetPaths since dynamic resource items
added later miss the resource pipeline.

Verified fresh build, incremental rebuild (embedded catalog test both
times), and a simulated design-time compile with no artifacts present.

* [SourceGenerators] Regenerate test lock file after rebase onto main

Rebase fallout: main's package graph shifted under #200, so the
SourceGenerators.Tests lock file is re-evaluated to keep --locked-mode
restore green at the branch tip.

* [Build] Drop NuGet lock files; rely on central package management

Central package management was already in effect (ManagePackageVersionsCentrally
with all versions in Directory.Packages.props and no inline PackageReference
versions), so the per-project packages.lock.json files and the lock-mode
workflow only added maintenance overhead. This removes all eleven lock files,
drops RestorePackagesWithLockFile so restore no longer regenerates them, and
takes --locked-mode off the CI restore steps (re-keying the NuGet cache on the
central props files). Package versions remain centrally pinned in
Directory.Packages.props.
2026-07-16 00:00:32 +03:00
Gutemberg Ribeiro 30fdd8d6ed [Gpu] Backend-neutral shader compiler and guest-GPU renderer seam (#200)
* [ShaderCompiler] Extract the backend-neutral shader compiler project

Move the Gen5 (gfx10) microcode decoder, the scalar evaluator, the
shader IR, and the metadata reader out of SharpEmu.Libs/Agc into a new
SharpEmu.ShaderCompiler project — the half of shader compilation every
codegen backend (SPIR-V today; MSL and DXIL later) consumes. Types go
public: they are the contract now. Nothing in the project may depend on
a host graphics API; the SPIR-V-specific artifact types
(Gen5SpirvShader, Gen5SpirvStage) stay beside the emitter in Libs.

Three couplings surfaced by the move, each resolved at the right depth:
GuestDrawKind was defined inside VulkanVideoPresenter despite being a
guest-domain, decoder-produced concept — it moves to the shared project;
the evaluator's one HLE dependency (the tracked-libc-heap read
fallback) becomes an injectable hook that a Libs module initializer
installs before any caller can reach the evaluator; and the inline-
constant table is promoted to a shared Gen5InlineConstants so backends
cannot drift on constant semantics (the SPIR-V translator now delegates
to it).

The ShaderDump tool drops its reflection over the moved types in favor
of direct typed calls; only the SPIR-V emitter, still internal to Libs
until it moves to its own backend project, is reached via reflection.
Verified by a clean solution build, the existing test suite, and a full
ShaderDump conformance run.

* [ShaderCompiler] Move the SPIR-V emitter into SharpEmu.ShaderCompiler.Vulkan

Gen5SpirvTranslator (with its ALU partial), SpirvModuleBuilder,
SpirvFixedShaders, and the Gen5SpirvShader/Gen5SpirvStage artifact types
move whole from SharpEmu.Libs/Agc into the first per-backend codegen
project. Notably it needs no Vulkan bindings reference: emitters
produce bytes from the shared IR; renderers own graphics APIs. Types go
public as the backend's contract; AgcExports and the presenter consume
them exactly as before.

The ShaderDump tool drops its last reflection: with both halves of the
pipeline public it drives decode and all three emit entry points with
direct typed calls, retiring the PadWithDefaults invoke shim — and it
no longer references SharpEmu.Libs at all, making the conformance tool
emulator-independent by design. Verified by a clean solution build, the
test suite, a full ShaderDump conformance run, and a locked-mode
restore under the pinned SDK.

* [Gpu] Extract the guest-GPU backend seam (IGuestGpuBackend)

The AGC/VideoOut/SystemService export layers now reach the renderer
through IGuestGpuBackend via GuestGpu.Current (mirroring HostPlatform),
instead of calling VulkanVideoPresenter statics. The Vulkan backend is
a thin adapter over the existing presenter, so the extraction stays
mechanical; only the adapter and the presenter itself reference the
presenter now.

The types crossing the seam move to Gpu/GuestGpuTypes.cs and drop their
Vulkan prefixes, which an audit showed were misnomers: every field is a
neutral primitive or a raw guest value (guest addresses, format and
number-type codes, CB_BLEND register bitfields, verbatim sampler
descriptor dwords). The one genuine Vulkan value in the old surface —
the Silk.NET Format inside VulkanRenderTargetFormat, which callers
never read — stops crossing: TryDecodeRenderTargetFormat is replaced at
the seam by TryGetRenderTargetOutputKind, which surfaces only the
Gen5PixelOutputKind callers actually consume, keeping native formats a
backend-internal concern. ToVulkanSampler in AgcExports is renamed
ToGuestSampler to match what it always produced.

Seam rules are documented on the interface: no host-API value crosses,
and submission stays coarse-grained with synchronization internal to
backends. Interim exception, resolved next: shader parameters are still
SPIR-V blobs.

* [Gpu] Move shader compilation behind the guest-GPU backend

The seam's interim exception is gone: AgcExports no longer calls
Gen5SpirvTranslator or handles SPIR-V bytes. IGuestGpuBackend gains the
three TryCompile entry points, which take the backend-neutral
(Gen5ShaderState, Gen5ShaderEvaluation) contract plus the flat
per-role resource-slot bases a multi-stage draw needs, and return
opaque IGuestCompiledShader handles that only the producing backend can
submit — the Vulkan backend wraps its SPIR-V in
VulkanCompiledGuestShader and rejects foreign handles loudly. Draw and
dispatch submissions take handles instead of byte arrays; the shader
caches in AgcExports store handles.

IGuestCompiledShader.Payload exposes the backend-defined compiled bytes
for exactly two callers: the diagnostics dump and the size trace —
documented as never-interpret. The unused _pixelSpirvCache is deleted.
With this, a Metal or DX12 backend plugs in by implementing
IGuestGpuBackend with its own codegen; nothing in the export layers
knows which shader format exists.

Verified by a clean solution build, the test suite, and a full
ShaderDump conformance run under the pinned SDK.

* [Gpu] Fix rename collateral from the seam extraction

Address review findings: a doc comment picked up the mechanical
VulkanVideoPresenter -> GuestGpu.Current rewrite and ended up naming
members that do not exist on the interface, and CreateVulkanIndexBuffer
kept its Vulkan prefix while every sibling factory was de-Vulkanized —
it produces the neutral GuestIndexBuffer, so it is CreateGuestIndexBuffer.

* [Gpu] Label diagnostics dumps with the backend's payload extension

Address the review's altitude finding on DumpSpirv: the dump helper's
IR-disassembly half is backend-neutral and stays put, but writing the
opaque payload to a hardcoded .spv interpreted bytes the seam says
never to interpret. IGuestCompiledShader now declares its payload's
file extension, and the renamed DumpCompiledShader takes the handle and
writes honestly-labeled dumps whichever backend produced them.

* [Gpu] Make the shader-cache hit path allocation-free and lock-free

Every translated draw built its cache key with a LINQ Select feeding
string.Join plus one interpolated string per render target — steady
per-draw allocation whether or not the shaders were already cached. The
output layout is now packed exactly into a ulong (guest slot in 6 bits
+ output kind in 2 bits per target, host locations being the byte
positions, target count in the key beside it), and the
Gen5PixelOutputBinding array is only materialized on a cache miss,
where compilation dwarfs it.

The graphics/compute shader caches switch from Dictionary guarded by
_submitTraceGate to ConcurrentDictionary, making the per-draw and
per-dispatch hit paths lock-free and decoupling them from the tracing
gate they coincidentally shared. And the seam-shaped render-target list
is built once when a translated draw is created instead of a
Select/ToArray per submission of a cached draw.

* [Gpu] Replace LINQ with explicit loops in code this branch introduced

Project rule going forward: no LINQ — it allocates enumerators,
closures, and delegates, and this codebase is GC-pause-sensitive. The
pixel-output and guest-render-target array builds and the ShaderDump
store-PC collection become plain loops; pre-existing LINQ elsewhere is
left for changes that already touch those lines.

* [ShaderCompiler] Suppress CA2255 on the evaluator hook installer

The analyzer coverage that arrived with the rebase flags
ModuleInitializer in library code; 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 — so
suppress with that justification rather than weaken the guarantee to a
static constructor's lazier timing.

* [Gpu] Resolve rebase artifacts onto main

Dedupe the System.Collections.Concurrent using in AgcExports that the
rebase merge duplicated (main and this branch each added it), and
regenerate the lock files for the new shader-compiler projects and
SharpEmu.Libs against main's current package graph so --locked-mode
restore matches at the branch tip.

* [CI] Comment per-platform build artifact links on PRs

Adds a workflow_run workflow that, after "Build and Release" finishes a
pull-request build, posts (and keeps updated in place) a single PR
comment linking the Windows, Linux, and macOS artifacts from that run.

It runs via workflow_run rather than in the build workflow because PRs
from forks build with a read-only token that cannot comment; the
follow-on run executes in the base-repo context with write access and
without checking out fork code. GitHub only triggers workflow_run from
the default branch, so this takes effect once merged to main.
2026-07-15 11:11:24 -06:00
Berk c69ac6ddab [CI] Update for cross-platform support (#209)
* [CI] Update for cross-platform support

* [CI] fix check
2026-07-15 16:07:36 +03:00
86 changed files with 4098 additions and 2514 deletions
+120
View File
@@ -0,0 +1,120 @@
# 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 });
}
+57 -68
View File
@@ -34,31 +34,31 @@ jobs:
name: Init
runs-on: ubuntu-latest
outputs:
archive-name: ${{ steps.vars.outputs.archive-name }}
artifact-name: ${{ steps.vars.outputs.artifact-name }}
release-name: ${{ steps.vars.outputs.release-name }}
release-tag: ${{ steps.vars.outputs.release-tag }}
safe-ref: ${{ steps.vars.outputs.safe-ref }}
short-sha: ${{ steps.vars.outputs.short-sha }}
version: ${{ steps.vars.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Compute workflow variables
id: vars
shell: bash
run: |
short_sha="${GITHUB_SHA::7}"
safe_ref="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9._-]#-#g')"
archive_name="sharpemu-win64-${short_sha}.zip"
artifact_name="sharpemu-win64-${short_sha}"
release_tag="win64-${safe_ref}-${short_sha}"
release_name="SharpEmu win64 ${short_sha}"
version="$(python3 -c 'import sys, xml.etree.ElementTree as ET; version = ET.parse("Directory.Build.props").getroot().findtext(".//SharpEmuVersion"); sys.exit("Directory.Build.props is missing SharpEmuVersion") if not version or not version.strip() else print(version.strip())')"
release_tag="v${version}"
release_name="SharpEmu v${version}"
{
echo "short-sha=${short_sha}"
echo "safe-ref=${safe_ref}"
echo "archive-name=${archive_name}"
echo "artifact-name=${artifact_name}"
echo "release-tag=${release_tag}"
echo "release-name=${release_name}"
echo "version=${version}"
} >> "$GITHUB_OUTPUT"
reuse:
@@ -94,14 +94,20 @@ jobs:
cache: true
cache-dependency-path: |
Directory.Packages.props
src/**/packages.lock.json
Directory.Build.props
- name: Restore solution
run: dotnet restore SharpEmu.slnx --locked-mode
run: dotnet restore SharpEmu.slnx
- 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
@@ -112,7 +118,8 @@ jobs:
run: |
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
$archivePath = Join-Path $env:RELEASE_DIR "${{ needs.init.outputs.archive-name }}"
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
if (Test-Path $archivePath) {
Remove-Item $archivePath -Force
}
@@ -122,8 +129,8 @@ jobs:
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: ${{ needs.init.outputs.artifact-name }}
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
if-no-files-found: error
build-posix:
@@ -156,14 +163,19 @@ jobs:
cache: true
cache-dependency-path: |
Directory.Packages.props
src/**/packages.lock.json
Directory.Build.props
- name: Restore solution
run: dotnet restore SharpEmu.slnx --locked-mode
run: dotnet restore SharpEmu.slnx
- 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"
@@ -175,14 +187,14 @@ jobs:
run: |
mkdir -p "$RELEASE_DIR"
# tar keeps the executable bit, which zip would drop.
tar -czf "$RELEASE_DIR/sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz" \
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
-C "$PUBLISH_DIR" .
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
if-no-files-found: error
release:
@@ -190,78 +202,55 @@ jobs:
needs:
- init
- build
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: ${{ needs.init.outputs.artifact-name }}
path: release
- name: Create or update release
shell: bash
env:
ARCHIVE_NAME: ${{ needs.init.outputs.archive-name }}
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
run: |
asset_path="release/${ARCHIVE_NAME}"
notes="Automated Windows build for commit ${GITHUB_SHA}."
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" "${asset_path}" --clobber
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
else
gh release create "${RELEASE_TAG}" "${asset_path}" \
--title "${RELEASE_NAME}" \
--notes "${notes}" \
--target "${GITHUB_SHA}"
fi
release-posix:
name: Publish GitHub Release (${{ matrix.rid }})
needs:
- init
- build-posix
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
fail-fast: false
matrix:
rid: [linux-x64, osx-x64]
steps:
- name: Download build artifact
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
path: release
- name: Create or update release
shell: bash
env:
ARCHIVE_NAME: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: SharpEmu ${{ matrix.rid }} ${{ needs.init.outputs.short-sha }}
RELEASE_TAG: ${{ matrix.rid }}-${{ needs.init.outputs.safe-ref }}-${{ needs.init.outputs.short-sha }}
RID: ${{ matrix.rid }}
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
VERSION: ${{ needs.init.outputs.version }}
run: |
asset_path="release/${ARCHIVE_NAME}"
notes="Automated ${RID} build for commit ${GITHUB_SHA}."
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release assets found." >&2
exit 1
fi
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" "${asset_path}" --clobber
gh release upload "${RELEASE_TAG}" "${assets[@]}" --clobber
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
else
gh release create "${RELEASE_TAG}" "${asset_path}" \
gh release create "${RELEASE_TAG}" "${assets[@]}" \
--title "${RELEASE_NAME}" \
--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
+2 -1
View File
@@ -9,7 +9,8 @@ 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>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
+3
View File
@@ -12,6 +12,9 @@ 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" />
+6 -6
View File
@@ -91,18 +91,18 @@ release includes the MoltenVK Vulkan implementation.
## Games Tested
* **Demon's Souls Remake**
* [Demon's Souls [PPSA01341]](https://github.com/par274/sharpemu/issues/2)
* [Demon's Souls [PPSA01341]](https://github.com/sharpemu/sharpemu/issues/2)
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this.
![DeS videoOut submit first frame](./.github/images/des-videoout-shaders.jpg)
* **Poppy Playtime Chapter 1**
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/par274/sharpemu/issues/3)
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/sharpemu/sharpemu/issues/3)
* **SILENT HILL: The Short Message**
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/par274/sharpemu/issues/4)
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/sharpemu/sharpemu/issues/4)
* **Dreaming Sarah**
* [Dreaming Sarah [PPSA02929]](https://github.com/par274/sharpemu/issues/9)
* [Dreaming Sarah [PPSA02929]](https://github.com/sharpemu/sharpemu/issues/9)
* Real texture rendering for this game;
![Splash texture](./.github/images/dreaming-sarah.jpg)
@@ -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/par274/sharpemu.git`
2. Clone the repository: `git clone https://github.com/sharpemu/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/par274/sharpemu/blob/main/LICENSE)
- [**GPL-2.0 license**](https://github.com/sharpemu/sharpemu/blob/main/LICENSE)
## Contributing
-2
View File
@@ -5,9 +5,7 @@ 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/**",
+4
View File
@@ -11,8 +11,12 @@ 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>
-172
View File
@@ -1,172 +0,0 @@
# 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())
-54
View File
@@ -1,54 +0,0 @@
# 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()
-1
View File
@@ -25,7 +25,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
-572
View File
@@ -1,572 +0,0 @@
{
"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=="
}
}
}
}
+3 -1
View File
@@ -84,7 +84,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
};
var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
// 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.Freeze();
// Resolve the host platform once at the composition root; on unsupported
-155
View File
@@ -1,155 +0,0 @@
{
"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"
}
}
}
}
}
+13 -5
View File
@@ -28,9 +28,9 @@
"Options.General": "Opções Gerais",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"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.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.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": "Recortar",
"Console.Split": "Dividir",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Console do SharpEmu",
@@ -134,5 +134,13 @@
"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"
"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!"
}
+18 -1
View File
@@ -26,6 +26,15 @@
"Library.Loading": "Chargement de la bibliothèque…",
"Options.General": "Général",
"Options.Env.Tab": "Environnement",
"Options.Section.Environment": "VARIABLES DENVIRONNEMENT",
"Options.Env.Desc": "Paramètres passés à l’émulateur comme variables denvironnement 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 linitialisation é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 linstallation 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",
@@ -125,5 +134,13 @@
"Dialog.PsExecutables": "Exécutables PlayStation",
"Dialog.SaveLogFile": "Choisir lemplacement du fichier journal",
"Dialog.PlainTextFiles": "Fichiers texte brut",
"Dialog.LogFiles": "Fichiers journaux"
"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 laide et suivez le développement.",
"About.GithubButton": "Contribuer sur GitHub !",
"About.DiscordButton": "Rejoindre notre Discord !"
}
+15 -9
View File
@@ -34,6 +34,12 @@ 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();
@@ -166,7 +172,7 @@ public partial class MainWindow : Window
{
Process.Start(new ProcessStartInfo
{
FileName = "https://github.com/par274/sharpemu",
FileName = "https://github.com/sharpemu/sharpemu",
UseShellExecute = true
});
};
@@ -727,7 +733,7 @@ public partial class MainWindow : Window
}
var changed = false;
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
if (!_settings.GameFolders.Contains(path, FilePathComparer))
{
_settings.GameFolders.Add(path);
changed = true;
@@ -737,7 +743,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, StringComparison.OrdinalIgnoreCase)) > 0;
excluded.StartsWith(prefix, FilePathComparison)) > 0;
if (changed)
{
@@ -750,7 +756,7 @@ public partial class MainWindow : Window
private async Task RescanLibraryAsync()
{
var folders = _settings.GameFolders.ToArray();
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
var excluded = new HashSet<string>(_settings.ExcludedGames, FilePathComparer);
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
EmptyState.IsVisible = false;
LoadingState.IsVisible = true;
@@ -868,7 +874,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>(StringComparer.OrdinalIgnoreCase);
var seen = new HashSet<string>(FilePathComparer);
var enumeration = new EnumerationOptions
{
IgnoreInaccessible = true,
@@ -1132,13 +1138,13 @@ public partial class MainWindow : Window
return;
}
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
if (!_settings.ExcludedGames.Contains(game.Path, FilePathComparer))
{
_settings.ExcludedGames.Add(game.Path);
_settings.Save();
}
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, FilePathComparison));
GameList.SelectedItem = null;
RefreshVisibleGames();
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
@@ -1162,7 +1168,7 @@ public partial class MainWindow : Window
}
if (selectedPath is not null &&
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, FilePathComparison))
is { } reselected)
{
GameList.SelectedItem = reselected;
@@ -1487,7 +1493,7 @@ public partial class MainWindow : Window
_isRunning = true;
_runningGameName = displayName;
_runningGameTitleId = _allGames
.FirstOrDefault(game => game.Path.Equals(ebootPath, StringComparison.OrdinalIgnoreCase))?
.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?
.TitleId;
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
StatusDot.Fill = SuccessLineBrush;
-1
View File
@@ -9,7 +9,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
the executable is started without arguments. -->
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
<!-- Required by the source-generated LibraryImport stubs in the linked
controller readers below. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-189
View File
@@ -1,189 +0,0 @@
{
"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.
+20
View File
@@ -0,0 +1,20 @@
// 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,6 +271,26 @@ 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()}");
+2 -3
View File
@@ -1,13 +1,12 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
namespace SharpEmu.HLE;
public interface IModuleManager
{
int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null);
/// <summary>Registers pre-built exports (the compile-time generated registry).</summary>
int RegisterExports(IReadOnlyList<ExportedFunction> exports);
void Freeze();
+16 -145
View File
@@ -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 Assembly, Generation Generation)> _scannedAssemblies = new();
private readonly HashSet<Assembly> _warmupAssemblies = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
public int RegisterExports(IReadOnlyList<ExportedFunction> exports)
{
ArgumentNullException.ThrowIfNull(assembly);
ArgumentNullException.ThrowIfNull(exports);
lock (_registrationGate)
{
@@ -27,48 +27,21 @@ public sealed class ModuleManager : IModuleManager
throw new InvalidOperationException("Module registration is frozen.");
}
// Deduplicated: one assembly is reached through many types.
if (!_scannedAssemblies.Add((assembly, generation)))
{
return 0;
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
foreach (var type in assembly.GetTypes())
foreach (var export in exports)
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
if (!_dispatchTable.TryAdd(export.Nid, export.Function))
{
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++;
Console.Error.WriteLine($"[HLE] Duplicate NID '{export.Nid}' ({export.Name}) — already registered, skipping.");
continue;
}
_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++;
}
return registeredCount;
@@ -92,7 +65,8 @@ public sealed class ModuleManager : IModuleManager
Assembly[] assemblies;
lock (_registrationGate)
{
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
assemblies = new Assembly[_warmupAssemblies.Count];
_warmupAssemblies.CopyTo(assemblies);
}
assemblies = WithGuestReachableDependencies(assemblies);
@@ -328,107 +302,4 @@ 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);
}
+46 -1
View File
@@ -21,6 +21,51 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
<!-- 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" />
</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>
-10
View File
@@ -1,10 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+165 -136
View File
@@ -1,11 +1,13 @@
// 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;
@@ -157,13 +159,14 @@ 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();
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<
// 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<
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
byte[]> _computeSpirvCache = new();
IGuestCompiledShader> _computeShaderCache = new();
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
private static readonly bool _traceAgc = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
@@ -334,17 +337,20 @@ public static class AgcExports
ulong ExportShaderAddress,
ulong PixelShaderAddress,
uint PrimitiveType,
byte[] VertexSpirv,
byte[] PixelSpirv,
IGuestCompiledShader VertexShader,
IGuestCompiledShader PixelShader,
uint AttributeCount,
uint VertexCount,
uint InstanceCount,
VulkanGuestIndexBuffer? IndexBuffer,
GuestIndexBuffer? IndexBuffer,
IReadOnlyList<TranslatedImageBinding> Textures,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
IReadOnlyList<Gen5VertexInputBinding> VertexInputs,
IReadOnlyList<RenderTargetDescriptor> RenderTargets,
VulkanGuestRenderState RenderState);
// 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);
private sealed record TranslatedImageBinding(
TextureDescriptor Descriptor,
@@ -423,6 +429,8 @@ 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",
@@ -440,6 +448,7 @@ 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",
@@ -619,6 +628,8 @@ 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",
@@ -676,7 +687,10 @@ 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",
@@ -720,6 +734,7 @@ public static class AgcExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
#pragma warning restore SHEM004
[SysAbiExport(
Nid = "LtTouSCZjHM",
@@ -2540,6 +2555,8 @@ 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",
@@ -2554,6 +2571,7 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
#pragma warning restore SHEM006
[SysAbiExport(
Nid = "h9z6+0hEydk",
@@ -2567,6 +2585,8 @@ 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",
@@ -2587,6 +2607,7 @@ 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
@@ -2720,8 +2741,12 @@ public static class AgcExports
ctx.TryReadUInt32(currentAddress + sizeof(uint), out var eventTypeRaw))
{
var eventType = eventTypeRaw & 0x3Fu;
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents(
eventType,
// 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(
KernelEventQueueCompatExports.KernelEventFilterGraphics,
eventType);
if (tracePackets)
@@ -2942,7 +2967,7 @@ public static class AgcExports
handle,
displayBufferIndex,
out var cachedDisplayBuffer) &&
VulkanVideoPresenter.TrySubmitGuestImage(
GuestGpu.Current.TrySubmitGuestImage(
cachedDisplayBuffer.Address,
cachedDisplayBuffer.Width,
cachedDisplayBuffer.Height,
@@ -2966,11 +2991,11 @@ public static class AgcExports
displayBufferIndex,
translatedDisplayBuffer,
"draw-fallback");
var textures = CreateVulkanGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
var textures = CreateGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
var globalMemoryBuffers =
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
VulkanVideoPresenter.SubmitTranslatedDraw(
translatedDraw.PixelSpirv,
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
GuestGpu.Current.SubmitTranslatedDraw(
translatedDraw.PixelShader,
textures,
globalMemoryBuffers,
translatedDisplayBuffer.Width,
@@ -2978,7 +3003,7 @@ public static class AgcExports
translatedDraw.AttributeCount);
TraceAgcShader(
$"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " +
$"spirv={translatedDraw.PixelSpirv.Length} textures={textures.Count} " +
$"spirv={translatedDraw.PixelShader.Payload.Length} textures={textures.Count} " +
$"global_buffers={globalMemoryBuffers.Count} " +
$"fallback={fallbackTextureCount} {translatedDisplayBuffer.Width}x{translatedDisplayBuffer.Height}");
@@ -3013,7 +3038,7 @@ public static class AgcExports
displayBufferIndex,
out var displayBuffer))
{
VulkanVideoPresenter.SubmitGuestDraw(
GuestGpu.Current.SubmitGuestDraw(
state.GuestDrawKind,
displayBuffer.Width,
displayBuffer.Height);
@@ -3409,29 +3434,23 @@ public static class AgcExports
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
if (firstTarget.Address != 0)
{
var textures = CreateVulkanGuestDrawTextures(
var textures = CreateGuestDrawTextures(
ctx,
translatedDraw.Textures,
out _);
var globalMemoryBuffers =
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
var vertexBuffers =
CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs);
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
TraceRectListVertices(translatedDraw, vertexBuffers);
TraceGrassDrawVertices(translatedDraw, textures, vertexBuffers);
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
translatedDraw.PixelSpirv,
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
translatedDraw.PixelShader,
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
translatedDraw.RenderTargets.Select(target =>
new VulkanGuestRenderTarget(
target.Address,
target.Width,
target.Height,
target.Format,
target.NumberType)).ToArray(),
translatedDraw.VertexSpirv,
translatedDraw.GuestTargets,
translatedDraw.VertexShader,
translatedDraw.VertexCount,
translatedDraw.InstanceCount,
translatedDraw.PrimitiveType,
@@ -3445,14 +3464,14 @@ public static class AgcExports
.FirstOrDefault(binding => binding.IsStorage);
if (storageTarget is not null)
{
var textures = CreateVulkanGuestDrawTextures(
var textures = CreateGuestDrawTextures(
ctx,
translatedDraw.Textures,
out _);
var globalMemoryBuffers =
CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
translatedDraw.PixelSpirv,
CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
GuestGpu.Current.SubmitStorageTranslatedDraw(
translatedDraw.PixelShader,
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
@@ -3561,14 +3580,14 @@ public static class AgcExports
.Where(target => HasPixelColorExport(pixelState, target.Slot))
.OrderBy(target => target.Slot)
.ToArray();
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
var renderTargetOutputKinds = new Gen5PixelOutputKind[renderTargets.Length];
for (var index = 0; index < renderTargets.Length; index++)
{
var target = renderTargets[index];
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
if (!GuestGpu.Current.TryGetRenderTargetOutputKind(
target.Format,
target.NumberType,
out renderTargetFormats[index]))
out renderTargetOutputKinds[index]))
{
error =
$"unsupported color target format={target.Format} number_type={target.NumberType}";
@@ -3576,16 +3595,17 @@ public static class AgcExports
}
}
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}"));
// 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 attributeCount = GetInterpolatedAttributeCount(pixelState);
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
@@ -3595,19 +3615,25 @@ public static class AgcExports
pixelShaderAddress,
pixelStateFingerprint,
outputLayout,
(uint)renderTargets.Length,
attributeCount);
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
exportEvaluation.GlobalMemoryBindings.Count;
(byte[] Vertex, byte[] Pixel) compiled;
lock (_submitTraceGate)
{
_graphicsSpirvCache.TryGetValue(shaderKey, out compiled);
}
_graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
if (compiled.Vertex is null || compiled.Pixel is null)
{
if (!Gen5SpirvTranslator.TryCompilePixelShader(
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(
pixelState,
pixelEvaluation,
pixelOutputs,
@@ -3617,7 +3643,7 @@ public static class AgcExports
totalGlobalBufferCount: totalGlobalBuffers + 2,
imageBindingBase: 0,
scalarRegisterBufferIndex: totalGlobalBuffers) ||
!Gen5SpirvTranslator.TryCompileVertexShader(
!GuestGpu.Current.TryCompileVertexShader(
exportState,
exportEvaluation,
out var vertexShader,
@@ -3630,23 +3656,20 @@ public static class AgcExports
return false;
}
compiled = (vertexShader.Spirv, pixelShader.Spirv);
DumpSpirv(
compiled = (vertexShader!, pixelShader!);
DumpCompiledShader(
"vs",
exportShaderAddress,
exportStateFingerprint,
compiled.Vertex,
exportState.Program);
DumpSpirv(
DumpCompiledShader(
"ps",
pixelShaderAddress,
pixelStateFingerprint,
compiled.Pixel,
pixelState.Program);
lock (_submitTraceGate)
{
_graphicsSpirvCache.TryAdd(shaderKey, compiled);
}
_graphicsShaderCache.TryAdd(shaderKey, compiled);
}
var imageBindings = pixelEvaluation.ImageBindings
@@ -3683,6 +3706,17 @@ 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,
@@ -3692,11 +3726,12 @@ public static class AgcExports
attributeCount,
vertexCount,
state.InstanceCount,
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
textures,
globalMemoryBindings,
vertexInputs,
renderTargets,
guestTargets,
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
textures,
@@ -3717,8 +3752,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 VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
private static GuestRenderState ApplyTransparentPremultipliedFillClear(
GuestRenderState renderState,
IReadOnlyList<TranslatedImageBinding> textures,
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
IReadOnlyList<uint> pixelUserData)
@@ -3748,7 +3783,7 @@ public static class AgcExports
};
}
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
blend is
{
Enable: true,
@@ -3757,7 +3792,7 @@ public static class AgcExports
ColorFunc: 0,
};
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
private static GuestIndexBuffer? CreateGuestIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
uint indexCount)
@@ -3775,7 +3810,7 @@ public static class AgcExports
var address = state.IndexBufferAddress + byteOffset;
return (ctx.Memory.TryRead(address, data) ||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, data))
? new VulkanGuestIndexBuffer(data, is32Bit)
? new GuestIndexBuffer(data, is32Bit)
: null;
}
@@ -3943,19 +3978,19 @@ public static class AgcExports
return targets;
}
private static VulkanGuestRenderState CreateRenderState(
private static GuestRenderState CreateRenderState(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> targets,
Gen5ShaderState pixelState)
{
if (targets.Count == 0)
{
return VulkanGuestRenderState.Default;
return GuestRenderState.Default;
}
var target = targets[0];
var scissor = DecodeScissor(registers, target.Width, target.Height);
return new VulkanGuestRenderState(
return new GuestRenderState(
targets.Select(target =>
{
var blend = DecodeBlendState(registers, target.Slot);
@@ -3968,7 +4003,7 @@ public static class AgcExports
DecodeViewport(registers, target.Width, target.Height, scissor));
}
private static VulkanGuestBlendState DecodeBlendState(
private static GuestBlendState DecodeBlendState(
IReadOnlyDictionary<uint, uint> registers,
uint slot)
{
@@ -3979,7 +4014,7 @@ public static class AgcExports
}
registers.TryGetValue(CbBlend0Control + slot, out var control);
return new VulkanGuestBlendState(
return new GuestBlendState(
((control >> 30) & 1u) != 0,
control & 0x1Fu,
(control >> 8) & 0x1Fu,
@@ -3991,14 +4026,14 @@ public static class AgcExports
writeMask);
}
private static VulkanGuestRect? DecodeScissor(
private static GuestRect? DecodeScissor(
IReadOnlyDictionary<uint, uint> registers,
uint targetWidth,
uint targetHeight)
{
if (targetWidth == 0 || targetHeight == 0)
{
return new VulkanGuestRect(0, 0, 0, 0);
return new GuestRect(0, 0, 0, 0);
}
var left = 0;
@@ -4063,22 +4098,22 @@ public static class AgcExports
return null;
}
return new VulkanGuestRect(
return new GuestRect(
left,
top,
checked((uint)(right - left)),
checked((uint)(bottom - top)));
}
private static VulkanGuestViewport? DecodeViewport(
private static GuestViewport? DecodeViewport(
IReadOnlyDictionary<uint, uint> registers,
uint targetWidth,
uint targetHeight,
VulkanGuestRect? scissor)
GuestRect? scissor)
{
if (targetWidth == 0 || targetHeight == 0)
{
return new VulkanGuestViewport(0, 0, 0, 0, 0, 1);
return new GuestViewport(0, 0, 0, 0, 0, 1);
}
var minDepth = 0f;
@@ -4104,7 +4139,7 @@ public static class AgcExports
xScale > 0f &&
yScale != 0f)
{
return new VulkanGuestViewport(
return new GuestViewport(
xOffset - xScale,
yOffset - yScale,
xScale * 2f,
@@ -4117,10 +4152,10 @@ public static class AgcExports
{
return minDepth == 0f && maxDepth == 1f
? null
: new VulkanGuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
: new GuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
}
return new VulkanGuestViewport(
return new GuestViewport(
rect.X,
rect.Y,
rect.Width,
@@ -4297,7 +4332,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.PixelSpirv.Length} " +
$"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelShader.Payload.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} " +
@@ -4307,16 +4342,16 @@ public static class AgcExports
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
}
private static IReadOnlyList<VulkanGuestDrawTexture> CreateVulkanGuestDrawTextures(
private static IReadOnlyList<GuestDrawTexture> CreateGuestDrawTextures(
CpuContext ctx,
IReadOnlyList<TranslatedImageBinding> bindings,
out int fallbackTextureCount)
{
var textures = new List<VulkanGuestDrawTexture>(bindings.Count);
var textures = new List<GuestDrawTexture>(bindings.Count);
fallbackTextureCount = 0;
foreach (var binding in bindings)
{
if (TryCreateVulkanGuestDrawTexture(
if (TryCreateGuestDrawTexture(
ctx,
binding.Descriptor,
binding.IsStorage,
@@ -4335,13 +4370,13 @@ public static class AgcExports
return textures;
}
private static IReadOnlyList<VulkanGuestMemoryBuffer> CreateVulkanGuestMemoryBuffers(
private static IReadOnlyList<GuestMemoryBuffer> CreateGuestMemoryBuffers(
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
{
var buffers = new VulkanGuestMemoryBuffer[bindings.Count];
var buffers = new GuestMemoryBuffer[bindings.Count];
for (var index = 0; index < bindings.Count; index++)
{
buffers[index] = new VulkanGuestMemoryBuffer(
buffers[index] = new GuestMemoryBuffer(
bindings[index].BaseAddress,
bindings[index].Data);
}
@@ -4349,14 +4384,14 @@ public static class AgcExports
return buffers;
}
private static IReadOnlyList<VulkanGuestVertexBuffer> CreateVulkanGuestVertexBuffers(
private static IReadOnlyList<GuestVertexBuffer> CreateGuestVertexBuffers(
IReadOnlyList<Gen5VertexInputBinding> bindings)
{
var buffers = new VulkanGuestVertexBuffer[bindings.Count];
var buffers = new GuestVertexBuffer[bindings.Count];
for (var index = 0; index < bindings.Count; index++)
{
var binding = bindings[index];
buffers[index] = new VulkanGuestVertexBuffer(
buffers[index] = new GuestVertexBuffer(
binding.Location,
binding.ComponentCount,
binding.DataFormat,
@@ -4370,13 +4405,13 @@ public static class AgcExports
return buffers;
}
private static bool TryCreateVulkanGuestDrawTexture(
private static bool TryCreateGuestDrawTexture(
CpuContext ctx,
TextureDescriptor descriptor,
bool isStorage,
uint mipLevel,
IReadOnlyList<uint> samplerDescriptor,
out VulkanGuestDrawTexture texture)
out GuestDrawTexture texture)
{
texture = default!;
if (descriptor.Type != Gen5TextureType2D ||
@@ -4412,12 +4447,12 @@ public static class AgcExports
if (!isStorage &&
descriptor.Address != 0 &&
VulkanVideoPresenter.IsGpuGuestImageAvailable(
GuestGpu.Current.IsGpuGuestImageAvailable(
descriptor.Address,
descriptor.Format,
descriptor.NumberType))
{
texture = new VulkanGuestDrawTexture(
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -4431,7 +4466,7 @@ public static class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor));
Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -4451,7 +4486,7 @@ public static class AgcExports
}
}
texture = new VulkanGuestDrawTexture(
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -4465,7 +4500,7 @@ public static class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor));
Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -4506,7 +4541,7 @@ public static class AgcExports
DumpTextureSourceIfRequested(descriptor, sourceWidth, source);
var rgba = source;
texture = new VulkanGuestDrawTexture(
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -4520,7 +4555,7 @@ public static class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor));
Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -4553,8 +4588,8 @@ public static class AgcExports
private static void TraceGrassDrawVertices(
TranslatedGuestDraw draw,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestVertexBuffer> vertexBuffers)
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
{
if (_grassTraceCount >= 6 ||
!textures.Any(texture => texture.Width == 288 && texture.Height == 160) ||
@@ -4593,7 +4628,7 @@ public static class AgcExports
private static void TraceRectListVertices(
TranslatedGuestDraw draw,
IReadOnlyList<VulkanGuestVertexBuffer> vertexBuffers)
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
{
if (draw.PrimitiveType != 0x11 ||
draw.IndexBuffer is not null ||
@@ -4676,7 +4711,7 @@ public static class AgcExports
}
}
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
private static GuestDrawTexture CreateFallbackGuestDrawTexture(
bool isStorage,
uint format,
uint numberType)
@@ -4724,9 +4759,9 @@ public static class AgcExports
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
}
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
private static GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
descriptor.Count >= 4
? new VulkanGuestSampler(
? new GuestSampler(
descriptor[0],
descriptor[1],
descriptor[2],
@@ -4924,47 +4959,39 @@ public static class AgcExports
localSizeX,
localSizeY,
localSizeZ);
byte[] computeSpirv;
lock (_submitTraceGate)
{
_computeSpirvCache.TryGetValue(shaderKey, out computeSpirv!);
}
_computeShaderCache.TryGetValue(shaderKey, out var computeShader);
if (computeSpirv is null &&
Gen5SpirvTranslator.TryCompileComputeShader(
if (computeShader is null &&
GuestGpu.Current.TryCompileComputeShader(
shaderState,
evaluation,
localSizeX,
localSizeY,
localSizeZ,
out var compiledCompute,
out computeShader,
out computeError))
{
computeSpirv = compiledCompute.Spirv;
DumpSpirv(
DumpCompiledShader(
"cs",
shaderAddress,
shaderKey.Item2,
computeSpirv,
computeShader!,
shaderState.Program);
}
if (computeSpirv is not null)
if (computeShader is not null)
{
lock (_submitTraceGate)
{
_computeSpirvCache.TryAdd(shaderKey, computeSpirv);
}
_computeShaderCache.TryAdd(shaderKey, computeShader);
var textures = CreateVulkanGuestDrawTextures(
var textures = CreateGuestDrawTextures(
ctx,
translatedBindings,
out _);
var globalMemoryBuffers =
CreateVulkanGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
VulkanVideoPresenter.SubmitComputeDispatch(
CreateGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
GuestGpu.Current.SubmitComputeDispatch(
shaderAddress,
computeSpirv,
computeShader,
textures,
globalMemoryBuffers,
dispatch.GroupCountX,
@@ -5135,7 +5162,7 @@ public static class AgcExports
}
}
else if (source is { } cachedSourceTexture &&
VulkanVideoPresenter.TrySubmitGuestImageBlit(
GuestGpu.Current.TrySubmitGuestImageBlit(
cachedSourceTexture.Address,
cachedSourceTexture.Width,
cachedSourceTexture.Height,
@@ -5489,7 +5516,7 @@ public static class AgcExports
$"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}");
}
if (Gen5SpirvTranslator.TryCompilePixelShader(
if (GuestGpu.Current.TryCompilePixelShader(
pixelState,
evaluation,
[new(0, 0, Gen5PixelOutputKind.Float)],
@@ -5498,7 +5525,7 @@ public static class AgcExports
{
TraceAgcShader(
$"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " +
$"bytes={compiledPixel.Spirv.Length} bindings={evaluation.ImageBindings.Count} " +
$"bytes={compiledPixel!.Payload.Length} bindings={evaluation.ImageBindings.Count} " +
$"global_buffers={evaluation.GlobalMemoryBindings.Count}");
}
else
@@ -6388,14 +6415,14 @@ public static class AgcExports
$"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " +
$"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}";
private static void DumpSpirv(
private static void DumpCompiledShader(
string stage,
ulong shaderAddress,
ulong stateFingerprint,
byte[] spirv,
IGuestCompiledShader shader,
Gen5ShaderProgram program)
{
if (spirv.Length == 0 ||
if (shader.Payload.Length == 0 ||
!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"),
"1",
@@ -6407,7 +6434,9 @@ 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}.spv"), spirv);
File.WriteAllBytes(
Path.Combine(directory, $"{name}.{shader.PayloadFileExtension}"),
shader.Payload);
var lines = new List<string>(program.Instructions.Count + 2)
{
@@ -0,0 +1,30 @@
// 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;
}
}
@@ -48,19 +48,25 @@ 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)
{
+18
View File
@@ -0,0 +1,18 @@
// 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;
}
+116
View File
@@ -0,0 +1,116 @@
// 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);
@@ -0,0 +1,19 @@
// 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; }
}
+140
View File
@@ -0,0 +1,140 @@
// 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);
}
@@ -0,0 +1,12 @@
// 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";
}
@@ -0,0 +1,251 @@
// 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,6 +589,66 @@ 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,6 +1816,8 @@ 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,20 +99,6 @@ 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);
@@ -1375,9 +1361,8 @@ public static class KernelPthreadCompatExports
{
state.Waiters++;
var observedEpoch = state.SignalEpoch;
var consumedPendingSignal = state.TryConsumePendingSignal();
TracePthreadCond(
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
"wait-enter",
condAddress,
mutexAddress,
state,
@@ -1392,25 +1377,6 @@ 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(
@@ -1542,10 +1508,6 @@ 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,41 +1717,59 @@ 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)
{
TraceKernelTscFrequency("env", overrideHz);
return overrideHz;
return (overrideHz, "env");
}
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
if (rdtscAvailable)
{
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
return calibratedHz;
if (tryCalibrate(out ulong calibratedHz) && calibratedHz >= minSane)
{
return (calibratedHz, "calibrated-rdtsc");
}
if (tryResolveCpuid(out ulong cpuidHz) && cpuidHz >= minSane)
{
return (cpuidHz, "cpuid");
}
}
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
TraceKernelTscFrequency("cpuid", cpuidHz);
return cpuidHz;
}
var hostQpc = Stopwatch.Frequency > 0
? unchecked((ulong)Stopwatch.Frequency)
var hostQpc = stopwatchFrequency > 0
? unchecked((ulong)stopwatchFrequency)
: DefaultKernelTscFrequency;
if (hostQpc >= minSane)
{
TraceKernelTscFrequency("qpc", hostQpc);
return hostQpc;
}
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
return DefaultKernelTscFrequency;
return hostQpc >= minSane ? (hostQpc, "qpc") : (DefaultKernelTscFrequency, "default");
}
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
@@ -259,11 +259,8 @@ public static class KernelSemaphoreCompatExports
ExportName = "sceKernelPollSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelPollSema(CpuContext ctx)
public static int KernelPollSema(CpuContext ctx, uint handle, int needCount)
{
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);
@@ -299,11 +296,8 @@ public static class KernelSemaphoreCompatExports
ExportName = "sceKernelSignalSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelSignalSema(CpuContext ctx)
public static int KernelSignalSema(CpuContext ctx, uint handle, int signalCount)
{
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);
@@ -341,12 +335,8 @@ public static class KernelSemaphoreCompatExports
ExportName = "sceKernelCancelSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCancelSema(CpuContext ctx)
public static int KernelCancelSema(CpuContext ctx, uint handle, int setCount, ulong waitingThreadsAddress)
{
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 -2
View File
@@ -10,6 +10,7 @@ public static class NpManagerExports
{
private const int NpTitleIdSize = 16;
private const int NpTitleSecretSize = 128;
private const int NpReachabilityStateUnavailable = 0;
[SysAbiExport(
Nid = "3Zl8BePTh9Y",
@@ -75,8 +76,15 @@ public static class NpManagerExports
LibraryName = "libSceNpManager")]
public static int NpGetNpReachabilityState(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return ctx.TryWriteInt32(stateAddress, NpReachabilityStateUnavailable)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
+6
View File
@@ -84,6 +84,12 @@ 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
{
+11
View File
@@ -6,6 +6,17 @@ 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,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary;
@@ -93,7 +94,7 @@ public static class SystemServiceExports
LibraryName = "libSceSystemService")]
public static int SystemServiceHideSplashScreen(CpuContext ctx)
{
VulkanVideoPresenter.HideSplashScreen();
GuestGpu.Current.HideSplashScreen();
return ctx.SetReturn(0);
}
@@ -161,6 +161,8 @@ 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",
@@ -184,6 +186,7 @@ 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,6 +8,9 @@ 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,
@@ -90,6 +93,13 @@ 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)
@@ -186,6 +196,41 @@ 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,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Audio;
using SharpEmu.Libs.Kernel;
using SharpEmu.Logging;
@@ -812,7 +813,7 @@ public static class VideoOutExports
bgraFrame[offset + 3] = rgbaFrame[offset + 3];
}
VulkanVideoPresenter.Submit(bgraFrame, width, height);
GuestGpu.Current.Submit(bgraFrame, width, height);
}
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
@@ -1203,7 +1204,7 @@ public static class VideoOutExports
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
{
guestImageAddress = displayBuffer.Address;
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
guestImageSubmitted = GuestGpu.Current.TrySubmitGuestImage(
displayBuffer.Address,
displayBuffer.Width,
displayBuffer.Height,
@@ -1334,14 +1335,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}");
}
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
GuestGpu.Current.EnsureStarted(attribute.Width, attribute.Height);
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
if (guestFormat != 0)
{
foreach (var address in addresses)
{
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
GuestGpu.Current.RegisterKnownDisplayBuffer(address, guestFormat);
}
}
@@ -1573,7 +1574,7 @@ public static class VideoOutExports
: 0u;
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
// the backend keys its guest-image registry on (see VulkanVideoPresenter.
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
NormalizePixelFormat(pixelFormat) switch
@@ -7,6 +7,9 @@ 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;
@@ -22,111 +25,6 @@ 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)
@@ -137,26 +35,26 @@ internal readonly record struct VulkanRenderTargetFormat(
internal sealed record VulkanTranslatedGuestDraw(
byte[] VertexSpirv,
byte[] PixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> Textures,
IReadOnlyList<VulkanGuestMemoryBuffer> GlobalMemoryBuffers,
IReadOnlyList<VulkanGuestVertexBuffer> VertexBuffers,
IReadOnlyList<GuestDrawTexture> Textures,
IReadOnlyList<GuestMemoryBuffer> GlobalMemoryBuffers,
IReadOnlyList<GuestVertexBuffer> VertexBuffers,
uint AttributeCount,
uint VertexCount,
uint InstanceCount,
uint PrimitiveType,
VulkanGuestIndexBuffer? IndexBuffer,
VulkanGuestRenderState RenderState);
GuestIndexBuffer? IndexBuffer,
GuestRenderState RenderState);
internal sealed record VulkanOffscreenGuestDraw(
VulkanTranslatedGuestDraw Draw,
IReadOnlyList<VulkanGuestRenderTarget> Targets,
IReadOnlyList<GuestRenderTarget> Targets,
bool PublishTarget);
internal sealed record VulkanComputeGuestDispatch(
ulong ShaderAddress,
byte[] ComputeSpirv,
IReadOnlyList<VulkanGuestDrawTexture> Textures,
IReadOnlyList<VulkanGuestMemoryBuffer> GlobalMemoryBuffers,
IReadOnlyList<GuestDrawTexture> Textures,
IReadOnlyList<GuestMemoryBuffer> GlobalMemoryBuffers,
uint GroupCountX,
uint GroupCountY,
uint GroupCountZ);
@@ -407,8 +305,8 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint width,
uint height,
uint attributeCount,
@@ -416,9 +314,9 @@ internal static unsafe class VulkanVideoPresenter
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 || width == 0 || height == 0)
{
@@ -456,7 +354,7 @@ internal static unsafe class VulkanVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
renderState ?? VulkanGuestRenderState.Default),
renderState ?? GuestRenderState.Default),
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
System.Threading.Monitor.PulseAll(_gate);
@@ -473,17 +371,17 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
VulkanGuestRenderTarget target,
GuestRenderTarget target,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null)
{
SubmitOffscreenTranslatedDraw(
pixelSpirv,
@@ -502,17 +400,17 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
IReadOnlyList<VulkanGuestRenderTarget> targets,
IReadOnlyList<GuestRenderTarget> targets,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 ||
targets.Count == 0 ||
@@ -541,7 +439,7 @@ internal static unsafe class VulkanVideoPresenter
$"{firstTarget.Width}x{firstTarget.Height} textures={textures.Count}");
}
var effectiveRenderState = renderState ?? VulkanGuestRenderState.Default;
var effectiveRenderState = renderState ?? GuestRenderState.Default;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{
effectiveRenderState = effectiveRenderState with
@@ -589,8 +487,8 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitStorageTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
uint width,
uint height)
@@ -623,8 +521,8 @@ internal static unsafe class VulkanVideoPresenter
1,
4,
null,
VulkanGuestRenderState.Default),
[new VulkanGuestRenderTarget(
GuestRenderState.Default),
[new GuestRenderTarget(
Address: 0,
width,
height,
@@ -637,8 +535,8 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitComputeDispatch(
ulong shaderAddress,
byte[] computeSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ)
@@ -818,7 +716,7 @@ internal static unsafe class VulkanVideoPresenter
SubmitOffscreenTranslatedDraw(
fragmentSpirv,
[
new VulkanGuestDrawTexture(
new GuestDrawTexture(
sourceAddress,
sourceWidth,
sourceHeight,
@@ -830,7 +728,7 @@ internal static unsafe class VulkanVideoPresenter
],
[],
attributeCount: 1,
new VulkanGuestRenderTarget(
new GuestRenderTarget(
destinationAddress,
destinationWidth,
destinationHeight,
@@ -1363,7 +1261,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<VulkanGuestSampler, Sampler> _samplers = new();
private readonly Dictionary<GuestSampler, Sampler> _samplers = new();
private readonly Dictionary<byte[], string> _shaderDigests =
new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<DescriptorLayoutKey, DescriptorLayoutBundle>
@@ -1418,9 +1316,9 @@ internal static unsafe class VulkanVideoPresenter
public uint VertexCount = 3;
public uint InstanceCount = 1;
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
public VulkanGuestBlendState[] Blends = [VulkanGuestBlendState.Default];
public VulkanGuestRect? Scissor;
public VulkanGuestViewport? Viewport;
public GuestBlendState[] Blends = [GuestBlendState.Default];
public GuestRect? Scissor;
public GuestViewport? Viewport;
public RenderPass TransientRenderPass;
public Framebuffer TransientFramebuffer;
}
@@ -1440,7 +1338,7 @@ internal static unsafe class VulkanVideoPresenter
public bool NeedsUpload;
public bool OwnsStorage;
public bool IsStorage;
public VulkanGuestSampler SamplerState;
public GuestSampler SamplerState;
public Sampler Sampler;
public GuestImageResource? GuestImage;
public ulong CpuContentFingerprint;
@@ -1740,11 +1638,11 @@ internal static unsafe class VulkanVideoPresenter
$"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}";
}
private static string GuestImageDebugName(VulkanGuestRenderTarget target, Format format) =>
private static string GuestImageDebugName(GuestRenderTarget target, Format format) =>
$"SharpEmu guest 0x{target.Address:X16} {target.Width}x{target.Height} " +
$"fmt{target.Format}/{format}";
private static string TextureDebugName(VulkanGuestDrawTexture texture, Format format) =>
private static string TextureDebugName(GuestDrawTexture texture, Format format) =>
$"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " +
$"fmt{texture.Format}/{format}";
@@ -3621,7 +3519,7 @@ internal static unsafe class VulkanVideoPresenter
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TextureResource ResolveTextureResource(VulkanGuestDrawTexture texture)
private TextureResource ResolveTextureResource(GuestDrawTexture texture)
{
if (texture.IsStorage)
{
@@ -3695,7 +3593,7 @@ internal static unsafe class VulkanVideoPresenter
}
private bool TryCreateCpuTextureRefreshResource(
VulkanGuestDrawTexture texture,
GuestDrawTexture texture,
GuestImageResource guestImage,
ImageView view,
out TextureResource resource)
@@ -3760,7 +3658,7 @@ internal static unsafe class VulkanVideoPresenter
}
private static bool IsCompatibleGuestImageAlias(
VulkanGuestDrawTexture texture,
GuestDrawTexture texture,
GuestImageResource guestImage)
{
if (guestImage.Width == texture.Width &&
@@ -3781,7 +3679,7 @@ internal static unsafe class VulkanVideoPresenter
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture)
private TextureResource ResolveStorageImageResource(GuestDrawTexture texture)
{
if (texture.Address == 0)
{
@@ -3858,7 +3756,7 @@ internal static unsafe class VulkanVideoPresenter
return resource;
}
private TextureResource CreateStorageScratchResource(VulkanGuestDrawTexture texture)
private TextureResource CreateStorageScratchResource(GuestDrawTexture texture)
{
var width = Math.Max(texture.Width, 1);
var height = Math.Max(texture.Height, 1);
@@ -3948,7 +3846,7 @@ internal static unsafe class VulkanVideoPresenter
};
}
private GuestImageResource ResolveStorageGuestImage(VulkanGuestDrawTexture texture)
private GuestImageResource ResolveStorageGuestImage(GuestDrawTexture texture)
{
if (texture.Address == 0)
{
@@ -3957,7 +3855,7 @@ internal static unsafe class VulkanVideoPresenter
var format = GetTextureFormat(texture.Format, texture.NumberType);
var guestImage = GetOrCreateGuestImage(
new VulkanGuestRenderTarget(
new GuestRenderTarget(
texture.Address,
texture.Width,
texture.Height,
@@ -3974,7 +3872,7 @@ internal static unsafe class VulkanVideoPresenter
return guestImage;
}
private TextureResource CreateTextureResource(VulkanGuestDrawTexture texture)
private TextureResource CreateTextureResource(GuestDrawTexture texture)
{
var width = Math.Max(texture.Width, 1);
var height = Math.Max(texture.Height, 1);
@@ -4172,7 +4070,7 @@ internal static unsafe class VulkanVideoPresenter
}
private void DumpTextureUpload(
VulkanGuestDrawTexture texture,
GuestDrawTexture texture,
byte[] pixels,
uint rowLength,
uint width,
@@ -4264,7 +4162,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(VulkanGuestSampler sampler)
private Sampler CreateSampler(GuestSampler sampler)
{
if (_samplers.TryGetValue(sampler, out var cachedSampler))
{
@@ -4342,7 +4240,7 @@ internal static unsafe class VulkanVideoPresenter
}
private GlobalBufferResource CreateGlobalBufferResource(
VulkanGuestMemoryBuffer guestBuffer)
GuestMemoryBuffer guestBuffer)
{
var buffer = CreateHostBuffer(
guestBuffer.Data,
@@ -4371,7 +4269,7 @@ internal static unsafe class VulkanVideoPresenter
}
private VertexBufferResource CreateVertexBufferResource(
VulkanGuestVertexBuffer guestBuffer)
GuestVertexBuffer guestBuffer)
{
var buffer = CreateHostBuffer(
guestBuffer.Data,
@@ -4594,7 +4492,7 @@ internal static unsafe class VulkanVideoPresenter
private static uint GetDrawVertexCount(
uint primitiveType,
uint vertexCount,
VulkanGuestIndexBuffer? indexBuffer)
GuestIndexBuffer? indexBuffer)
{
if (primitiveType == GuestPrimitiveRectList && indexBuffer is null)
{
@@ -4640,41 +4538,41 @@ internal static unsafe class VulkanVideoPresenter
_ => BlendOp.Add,
};
private static uint DecodeSamplerClampX(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerClampX(GuestSampler sampler) =>
sampler.Word0 & 0x7u;
private static uint DecodeSamplerClampY(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerClampY(GuestSampler sampler) =>
(sampler.Word0 >> 3) & 0x7u;
private static uint DecodeSamplerClampZ(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerClampZ(GuestSampler sampler) =>
(sampler.Word0 >> 6) & 0x7u;
private static uint DecodeSamplerDepthCompare(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerDepthCompare(GuestSampler sampler) =>
(sampler.Word0 >> 12) & 0x7u;
private static float DecodeSamplerMinLod(VulkanGuestSampler sampler) =>
private static float DecodeSamplerMinLod(GuestSampler sampler) =>
(sampler.Word1 & 0xFFFu) / 256.0f;
private static float DecodeSamplerMaxLod(VulkanGuestSampler sampler) =>
private static float DecodeSamplerMaxLod(GuestSampler sampler) =>
((sampler.Word1 >> 12) & 0xFFFu) / 256.0f;
private static float DecodeSamplerLodBias(VulkanGuestSampler sampler)
private static float DecodeSamplerLodBias(GuestSampler sampler)
{
var raw = sampler.Word2 & 0x3FFFu;
var signed = (short)((raw ^ 0x2000u) - 0x2000u);
return signed / 256.0f;
}
private static uint DecodeSamplerMagFilter(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerMagFilter(GuestSampler sampler) =>
(sampler.Word2 >> 20) & 0x3u;
private static uint DecodeSamplerMinFilter(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerMinFilter(GuestSampler sampler) =>
(sampler.Word2 >> 22) & 0x3u;
private static uint DecodeSamplerMipFilter(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerMipFilter(GuestSampler sampler) =>
(sampler.Word2 >> 26) & 0x3u;
private static uint DecodeSamplerBorderColor(VulkanGuestSampler sampler) =>
private static uint DecodeSamplerBorderColor(GuestSampler sampler) =>
(sampler.Word3 >> 30) & 0x3u;
private static SamplerAddressMode ToVkSamplerAddressMode(uint mode) =>
@@ -4741,11 +4639,11 @@ internal static unsafe class VulkanVideoPresenter
return flags;
}
private static VulkanGuestRect ClampScissor(VulkanGuestRect? scissor, Extent2D extent)
private static GuestRect ClampScissor(GuestRect? scissor, Extent2D extent)
{
if (scissor is not { } rect)
{
return new VulkanGuestRect(0, 0, extent.Width, extent.Height);
return new GuestRect(0, 0, extent.Width, extent.Height);
}
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
@@ -4758,7 +4656,7 @@ internal static unsafe class VulkanVideoPresenter
rect.Y + checked((int)rect.Height),
top,
checked((int)extent.Height));
return new VulkanGuestRect(
return new GuestRect(
left,
top,
checked((uint)(right - left)),
@@ -4773,7 +4671,7 @@ internal static unsafe class VulkanVideoPresenter
? viewportEpsilon
: 0f;
private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent)
private static Viewport ClampViewport(GuestViewport? viewport, Extent2D extent)
{
if (viewport is not { } rect)
{
@@ -5476,7 +5374,7 @@ internal static unsafe class VulkanVideoPresenter
[MethodImpl(MethodImplOptions.NoInlining)]
private GuestImageResource GetOrCreateGuestImage(
VulkanGuestRenderTarget target,
GuestRenderTarget target,
Format format)
{
var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels);
@@ -5999,15 +5897,15 @@ internal static unsafe class VulkanVideoPresenter
var gpuInFlight = _pendingGuestSubmissions.Count +
(_presentationInFlight ? 1 : 0);
var readCount = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
var readBytes = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
var readHits = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
var readPvmBytes = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
var readLibcBytes = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
ref Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
var readsPerSecond =
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
var readMbPerSecond =
-138
View File
@@ -1,138 +0,0 @@
{
"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"
}
}
}
}
+4 -4
View File
@@ -12,8 +12,8 @@ namespace SharpEmu.Logging;
/// </summary>
public static class BuildInfo
{
private const string ProjectUrl = "https://github.com/par274/sharpemu";
private const string CanonicalRepository = "par274/sharpemu";
private const string ProjectUrl = "https://github.com/sharpemu/sharpemu";
private const string CanonicalRepository = "sharpemu/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/par274/sharpemu
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/sharpemu/sharpemu
///
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
/// Built from branch "main" of "sharpemu/sharpemu" by GitHub Actions workflow run https://github.com/sharpemu/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.
-6
View File
@@ -1,6 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
}
}
@@ -0,0 +1,23 @@
// 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,9 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
using SharpEmu.ShaderCompiler;
internal static partial class Gen5SpirvTranslator
namespace SharpEmu.ShaderCompiler.Vulkan;
public static partial class Gen5SpirvTranslator
{
private sealed partial class CompilationContext
{
@@ -22,6 +24,11 @@ internal 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";
@@ -32,10 +39,36 @@ internal 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
@@ -2336,6 +2369,42 @@ internal 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)
@@ -2491,47 +2560,7 @@ internal static partial class Gen5SpirvTranslator
resultSignChanged);
}
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;
}
private static bool TryDecodeInlineConstant(uint encoded, out uint value) =>
Gen5InlineConstants.TryDecode(encoded, out value);
}
}
@@ -1,9 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
using SharpEmu.ShaderCompiler;
internal static partial class Gen5SpirvTranslator
namespace SharpEmu.ShaderCompiler.Vulkan;
public static partial class Gen5SpirvTranslator
{
private const uint ScalarRegisterCount = 256;
private const uint VectorRegisterCount = 512;
@@ -2505,7 +2507,7 @@ internal static partial class Gen5SpirvTranslator
private bool UsesSubgroupShuffle() =>
_state.Program.Instructions.Any(instruction =>
instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32");
instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32" or "VReadlaneB32");
private bool UsesWaveControl() =>
_state.Program.Instructions.Any(instruction =>
@@ -0,0 +1,19 @@
<!--
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,9 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
namespace SharpEmu.ShaderCompiler.Vulkan;
internal static class SpirvFixedShaders
public static class SpirvFixedShaders
{
public static byte[] CreateFullscreenVertex(uint attributeCount)
{
@@ -4,9 +4,9 @@
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.Agc;
namespace SharpEmu.ShaderCompiler.Vulkan;
internal enum SpirvOp : ushort
public enum SpirvOp : ushort
{
Nop = 0,
Name = 5,
@@ -167,7 +167,7 @@ internal enum SpirvOp : ushort
GroupNonUniformShuffleDown = 348,
}
internal enum SpirvCapability : uint
public enum SpirvCapability : uint
{
Shader = 1,
Float16 = 9,
@@ -186,7 +186,7 @@ internal enum SpirvCapability : uint
RuntimeDescriptorArray = 5302,
}
internal enum SpirvStorageClass : uint
public enum SpirvStorageClass : uint
{
UniformConstant = 0,
Input = 1,
@@ -200,21 +200,21 @@ internal enum SpirvStorageClass : uint
StorageBuffer = 12,
}
internal enum SpirvExecutionModel : uint
public enum SpirvExecutionModel : uint
{
Vertex = 0,
Fragment = 4,
GLCompute = 5,
}
internal enum SpirvExecutionMode : uint
public enum SpirvExecutionMode : uint
{
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
}
internal enum SpirvDecoration : uint
public enum SpirvDecoration : uint
{
Block = 2,
ArrayStride = 6,
@@ -227,7 +227,7 @@ internal enum SpirvDecoration : uint
Offset = 35,
}
internal enum SpirvBuiltIn : uint
public enum SpirvBuiltIn : uint
{
Position = 0,
VertexIndex = 42,
@@ -241,7 +241,7 @@ internal enum SpirvBuiltIn : uint
SubgroupLocalInvocationId = 41,
}
internal enum SpirvImageDim : uint
public enum SpirvImageDim : uint
{
Dim1D = 0,
Dim2D = 1,
@@ -250,7 +250,7 @@ internal enum SpirvImageDim : uint
Buffer = 5,
}
internal enum SpirvImageFormat : uint
public enum SpirvImageFormat : uint
{
Unknown = 0,
Rgba32f = 1,
@@ -294,7 +294,7 @@ internal enum SpirvImageFormat : uint
R8ui = 39,
}
internal sealed class SpirvModuleBuilder
public sealed class SpirvModuleBuilder
{
private const uint Magic = 0x07230203;
private const uint Version15 = 0x00010500;
@@ -0,0 +1,54 @@
// 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,9 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
namespace SharpEmu.ShaderCompiler;
internal enum Gen5ShaderEncoding
public enum Gen5ShaderEncoding
{
Sop1,
Sop2,
@@ -26,7 +26,7 @@ internal enum Gen5ShaderEncoding
Exp,
}
internal enum Gen5OperandKind
public enum Gen5OperandKind
{
ScalarRegister,
VectorRegister,
@@ -34,7 +34,7 @@ internal enum Gen5OperandKind
LiteralConstant,
}
internal enum Gen5ShaderResourceKind
public enum Gen5ShaderResourceKind
{
ReadOnlyTexture,
ReadWriteTexture,
@@ -42,45 +42,31 @@ internal enum Gen5ShaderResourceKind
ConstantBuffer,
}
internal enum Gen5PixelOutputKind
public enum Gen5PixelOutputKind
{
Float,
Uint,
Sint,
}
internal readonly record struct Gen5PixelOutputBinding(
public readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
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(
public readonly record struct Gen5ShaderResourceMapping(
Gen5ShaderResourceKind Kind,
uint Slot,
uint OffsetDwords,
bool SizeFlag);
internal sealed record Gen5ShaderMetadata(
public sealed record Gen5ShaderMetadata(
uint ExtendedUserDataSizeDwords,
uint ShaderResourceTableSizeDwords,
IReadOnlyDictionary<uint, uint> DirectResources,
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
internal readonly record struct Gen5ComputeSystemRegisters(
public readonly record struct Gen5ComputeSystemRegisters(
uint? WorkGroupXRegister,
uint? WorkGroupYRegister,
uint? WorkGroupZRegister,
@@ -133,14 +119,14 @@ internal readonly record struct Gen5ComputeSystemRegisters(
}
}
internal sealed record Gen5ShaderState(
public sealed record Gen5ShaderState(
Gen5ShaderProgram Program,
IReadOnlyList<uint> UserData,
Gen5ShaderMetadata? Metadata,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
uint UserDataScalarRegisterBase = 0);
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
{
public static Gen5Operand Scalar(uint index) =>
new(Gen5OperandKind.ScalarRegister, index);
@@ -177,9 +163,9 @@ internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
};
}
internal abstract record Gen5InstructionControl;
public abstract record Gen5InstructionControl;
internal sealed record Gen5ImageControl(
public sealed record Gen5ImageControl(
uint Dmask,
uint VectorAddress,
IReadOnlyList<uint> AddressRegisters,
@@ -197,7 +183,7 @@ internal sealed record Gen5ImageControl(
: VectorAddress + (uint)component;
}
internal sealed record Gen5GlobalMemoryControl(
public sealed record Gen5GlobalMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
@@ -206,7 +192,7 @@ internal sealed record Gen5GlobalMemoryControl(
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5BufferMemoryControl(
public sealed record Gen5BufferMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
@@ -217,25 +203,25 @@ internal sealed record Gen5BufferMemoryControl(
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5ExportControl(
public sealed record Gen5ExportControl(
uint Target,
uint EnableMask,
bool Compressed,
bool Done,
bool ValidMask) : Gen5InstructionControl;
internal sealed record Gen5InterpolationControl(
public sealed record Gen5InterpolationControl(
uint Attribute,
uint Channel) : Gen5InstructionControl;
internal sealed record Gen5Vop3Control(
public sealed record Gen5Vop3Control(
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
internal sealed record Gen5SdwaControl(
public sealed record Gen5SdwaControl(
uint DestinationSelect,
uint Source0Select,
uint Source1Select,
@@ -244,7 +230,7 @@ internal sealed record Gen5SdwaControl(
uint OutputModifier,
bool Clamp) : Gen5InstructionControl;
internal sealed record Gen5DppControl(
public sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
bool BoundControl,
@@ -253,17 +239,17 @@ internal sealed record Gen5DppControl(
uint BankMask,
uint RowMask) : Gen5InstructionControl;
internal sealed record Gen5ScalarMemoryControl(
public sealed record Gen5ScalarMemoryControl(
uint DestinationCount,
int ImmediateOffsetBytes,
uint? DynamicOffsetRegister) : Gen5InstructionControl;
internal sealed record Gen5DataShareControl(
public sealed record Gen5DataShareControl(
uint Offset0,
uint Offset1,
bool Gds) : Gen5InstructionControl;
internal sealed record Gen5ImageBinding(
public sealed record Gen5ImageBinding(
uint Pc,
string Opcode,
Gen5ImageControl Control,
@@ -271,13 +257,13 @@ internal sealed record Gen5ImageBinding(
IReadOnlyList<uint> SamplerDescriptor,
uint? MipLevel);
internal sealed record Gen5GlobalMemoryBinding(
public sealed record Gen5GlobalMemoryBinding(
uint ScalarAddress,
ulong BaseAddress,
IReadOnlyList<uint> InstructionPcs,
byte[] Data);
internal sealed record Gen5VertexInputBinding(
public sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
@@ -288,7 +274,7 @@ internal sealed record Gen5VertexInputBinding(
uint OffsetBytes,
byte[] Data);
internal sealed record Gen5ShaderEvaluation(
public sealed record Gen5ShaderEvaluation(
IReadOnlyList<uint> InitialScalarRegisters,
IReadOnlyList<uint> ScalarRegisters,
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
@@ -298,7 +284,7 @@ internal sealed record Gen5ShaderEvaluation(
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
internal sealed record Gen5ShaderInstruction(
public sealed record Gen5ShaderInstruction(
uint Pc,
Gen5ShaderEncoding Encoding,
string Opcode,
@@ -307,7 +293,7 @@ internal sealed record Gen5ShaderInstruction(
IReadOnlyList<Gen5Operand> Destinations,
Gen5InstructionControl? Control);
internal sealed record Gen5ShaderProgram(
public sealed record Gen5ShaderProgram(
ulong Address,
IReadOnlyList<Gen5ShaderInstruction> Instructions)
{
@@ -3,9 +3,9 @@
using SharpEmu.HLE;
namespace SharpEmu.Libs.Agc;
namespace SharpEmu.ShaderCompiler;
internal static class Gen5ShaderMetadataReader
public static class Gen5ShaderMetadataReader
{
private const ulong ShaderUserDataOffset = 0x08;
private const int ResourceClassCount = 4;
@@ -2,13 +2,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Numerics;
namespace SharpEmu.Libs.Agc;
namespace SharpEmu.ShaderCompiler;
internal static class Gen5ShaderScalarEvaluator
public 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;
@@ -20,12 +28,12 @@ internal static class Gen5ShaderScalarEvaluator
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
: 1 * 1024 * 1024;
internal static long GlobalMemoryReadCount;
internal static long GlobalMemoryReadBytes;
internal static long GlobalMemoryReadCacheHits;
internal static long GlobalMemoryReadPvmBytes;
internal static long GlobalMemoryReadLibcBytes;
internal static long GlobalMemoryReadReuses;
public static long GlobalMemoryReadCount;
public static long GlobalMemoryReadBytes;
public static long GlobalMemoryReadCacheHits;
public static long GlobalMemoryReadPvmBytes;
public static long GlobalMemoryReadLibcBytes;
public static long GlobalMemoryReadReuses;
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
private static readonly object _crossFrameReadGate = new();
@@ -573,12 +581,12 @@ internal static class Gen5ShaderScalarEvaluator
[ThreadStatic]
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
internal static void BeginGlobalMemoryReadScope()
public static void BeginGlobalMemoryReadScope()
{
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
}
internal static void EndGlobalMemoryReadScope()
public static void EndGlobalMemoryReadScope()
{
_globalMemoryReadCache = null;
}
@@ -647,7 +655,7 @@ internal static class Gen5ShaderScalarEvaluator
data = GC.AllocateUninitializedArray<byte>(candidateSize);
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
if (readFromPvm ||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
FallbackMemoryReader?.Invoke(baseAddress, data) == true)
{
Interlocked.Increment(ref GlobalMemoryReadCount);
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
@@ -2,14 +2,13 @@
// 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.Libs.Agc;
namespace SharpEmu.ShaderCompiler;
internal static class Gen5ShaderTranslator
public static class Gen5ShaderTranslator
{
private const int MaxInstructions = 4096;
private const int MinimumUserDataDwords = 16;
@@ -276,7 +275,9 @@ internal static class Gen5ShaderTranslator
return true;
}
private static bool TryDecodeProgram(
// Public contract entry: emitter test suites and tools drive the decoder directly
// from raw instruction words.
public static bool TryDecodeProgram(
CpuContext ctx,
ulong address,
out Gen5ShaderProgram program,
@@ -910,9 +911,10 @@ internal static class Gen5ShaderTranslator
0x16A => "VMulHiU32",
0x16B => "VMulLoI32",
0x16C => "VMulHiI32",
0x360 => "VMadU32U16",
0x361 => "VMulLoU32",
0x360 => "VReadlaneB32",
0x361 => "VWritelaneB32",
0x362 => "VLdexpF32",
0x373 => "VMadU32U16",
0x346 => "VLshlAddU32",
0x347 => "VAddLshlU32",
0x36D => "VAdd3U32",
@@ -1220,7 +1222,7 @@ internal static class Gen5ShaderTranslator
private static bool IsMimgInstruction(string name) =>
name.StartsWith("Image", StringComparison.Ordinal);
internal static bool IsStorageImageOperation(string name) =>
public static bool IsStorageImageOperation(string name) =>
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
@@ -1465,6 +1467,12 @@ internal 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,
@@ -0,0 +1,15 @@
// 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,
}
@@ -0,0 +1,20 @@
<!--
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>
@@ -0,0 +1,99 @@
// 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
}
+75
View File
@@ -0,0 +1,75 @@
// 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;
}
}
@@ -0,0 +1,29 @@
<!--
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>
@@ -0,0 +1,80 @@
// 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);
}
@@ -0,0 +1,202 @@
// 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));
}
}
}
@@ -0,0 +1,273 @@
// 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("\"", "\\\"") + "\"";
}
@@ -0,0 +1,229 @@
// 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);
}
}
@@ -0,0 +1,25 @@
// 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);
}
}
@@ -0,0 +1,136 @@
// 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.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
return manager;
}
@@ -0,0 +1,130 @@
// 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);
}
}
@@ -0,0 +1,82 @@
// 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)!);
}
}
}
@@ -0,0 +1,331 @@
// 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]));
}
}
@@ -0,0 +1,52 @@
// 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);
}
}
@@ -0,0 +1,52 @@
// 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);
}
}
}
@@ -1,255 +0,0 @@
{
"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"
}
}
}
}
}
@@ -0,0 +1,34 @@
// 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));
}
@@ -0,0 +1,107 @@
// 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);
}
@@ -0,0 +1,28 @@
<!--
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>
@@ -0,0 +1,240 @@
// 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");
}
}
@@ -0,0 +1,165 @@
// 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);
}
}
+62 -190
View File
@@ -4,10 +4,9 @@
// Synthetic-shader conformance dumper.
//
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
// 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.
// 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.
//
// Programs that contain buffer_store_dword automatically get a single
// global-memory binding covering every store, which the emitter exposes as
@@ -21,9 +20,9 @@
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
using System.Buffers.Binary;
using System.Reflection;
using SharpEmu.HLE;
using SharpEmu.Libs.CxxAbi;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
const ulong ProgramAddress = 0x100000;
@@ -138,44 +137,6 @@ 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");
@@ -190,19 +151,18 @@ foreach (var (name, expectTranslate, words) in testPrograms)
Console.WriteLine(
$"[{name}] decode: " +
(string)describe.Invoke(null, [ctx, ProgramAddress, ProgramAddress])!);
Gen5ShaderTranslator.Describe(ctx, ProgramAddress, ProgramAddress));
object?[] decodeArgs = [ctx, ProgramAddress, null, null];
if (!(bool)tryDecode.Invoke(null, decodeArgs)!)
if (!Gen5ShaderTranslator.TryDecodeProgram(ctx, ProgramAddress, out var program, out var decodeError))
{
if (expectTranslate)
{
failures++;
Console.WriteLine($"[{name}] FAILED: decode error ({decodeArgs[3]})");
Console.WriteLine($"[{name}] FAILED: decode error ({decodeError})");
}
else
{
Console.WriteLine($"[{name}] decode failed as expected ({decodeArgs[3]})");
Console.WriteLine($"[{name}] decode failed as expected ({decodeError})");
}
continue;
@@ -219,167 +179,110 @@ 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 instructions)
foreach (var instruction in program!.Instructions)
{
var op = (string)instruction.GetType().GetProperty("Opcode")!.GetValue(instruction)!;
if (op.StartsWith("BufferStore", StringComparison.Ordinal))
if (instruction.Opcode.StartsWith("BufferStore", StringComparison.Ordinal))
{
storePcs.Add((uint)instruction.GetType().GetProperty("Pc")!.GetValue(instruction)!);
storePcs.Add(instruction.Pc);
}
}
// 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 = 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 globalBindings = storePcs.Count > 0
? new[] { new Gen5GlobalMemoryBinding(8u, 0UL, storePcs, new byte[64]) }
: Array.Empty<Gen5GlobalMemoryBinding>();
var state = Activator.CreateInstance(
stateType,
programObj,
new uint[16],
null,
null,
0u)!;
var evaluation = Activator.CreateInstance(
evaluationType,
var state = new Gen5ShaderState(program, new uint[16], Metadata: null);
var evaluation = new Gen5ShaderEvaluation(
new uint[256],
new uint[256],
new Dictionary<uint, IReadOnlyList<uint>>(),
Array.CreateInstance(imageBindingType, 0),
globalBindings,
null,
null,
null)!;
Array.Empty<Gen5ImageBinding>(),
globalBindings);
var compileArgs = PadWithDefaults(tryCompile, [state, evaluation, null, null]);
if ((bool)tryCompile.Invoke(null, BindingFlags.OptionalParamBinding, null, compileArgs, null)!)
if (Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var vertexShader, out var vertexError))
{
var shader = compileArgs[2]!;
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}.spv");
File.WriteAllBytes(path, spirv);
Console.WriteLine($"[{name}] emit: success, {spirv.Length} bytes -> {path}");
File.WriteAllBytes(path, vertexShader.Spirv);
Console.WriteLine($"[{name}] emit: success, {vertexShader.Spirv.Length} bytes -> {path}");
}
else
{
failures++;
Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
Console.WriteLine($"[{name}] emit: FAILED ({vertexError})");
}
var computeArgs = PadWithDefaults(tryCompileCompute, [state, evaluation, 1u, 1u, 1u, null, null]);
if ((bool)tryCompileCompute.Invoke(null, BindingFlags.OptionalParamBinding, null, computeArgs, null)!)
if (Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, 1, 1, 1, out var computeShader, out var computeError))
{
var shader = computeArgs[5]!;
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}-cs.spv");
File.WriteAllBytes(path, spirv);
Console.WriteLine($"[{name}] compute emit: success, {spirv.Length} bytes -> {path}");
File.WriteAllBytes(path, computeShader.Spirv);
Console.WriteLine($"[{name}] compute emit: success, {computeShader.Spirv.Length} bytes -> {path}");
}
else
{
failures++;
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
Console.WriteLine($"[{name}] compute emit: FAILED ({computeError})");
}
if (name.StartsWith("mrt", StringComparison.Ordinal))
{
(uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch
Gen5PixelOutputBinding[] pixelOutputs = name switch
{
"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")],
"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)],
};
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)!)
if (Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, pixelOutputs, out var pixelShader, out var pixelError))
{
var shader = pixelArgs[3]!;
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}-ps.spv");
File.WriteAllBytes(path, spirv);
Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}");
File.WriteAllBytes(path, pixelShader.Spirv);
Console.WriteLine($"[{name}] pixel emit: success, {pixelShader.Spirv.Length} bytes -> {path}");
}
else
{
failures++;
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})");
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelError})");
}
if (name == "mrt")
{
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)!)
Gen5PixelOutputBinding[] invalidOutputs =
[
new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
new Gen5PixelOutputBinding(3, 7, Gen5PixelOutputKind.Float),
];
if (Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, invalidOutputs, out _, out var invalidError))
{
failures++;
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
}
else
{
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})");
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidError})");
}
}
}
@@ -390,37 +293,6 @@ 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,8 +12,11 @@ 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.Libs\SharpEmu.Libs.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
</ItemGroup>
</Project>