mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 12:48:39 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 487bda6a32 | |||
| 09812600a0 | |||
| 3005babab8 | |||
| 09bd4f028b | |||
| 2bda253927 | |||
| 71e5912c75 |
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Aerolib Catalog
|
||||
|
||||
```bash
|
||||
# NID to export name
|
||||
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk
|
||||
|
||||
# Export name to NID
|
||||
python scripts/aerolib_catalog.py lookup sceKernelWaitSema
|
||||
|
||||
# Search export names
|
||||
python scripts/aerolib_catalog.py search VideoOut --limit 20
|
||||
|
||||
# Export all NID/name pairs to artifacts/aerolib.txt
|
||||
python scripts/aerolib_catalog.py export
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
NID_SUFFIX = bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")
|
||||
NID_PATTERN = re.compile(r"^[A-Za-z0-9+-]{11}$")
|
||||
DEFAULT_NAMES_FILE = Path(__file__).resolve().with_name("ps5_names.txt")
|
||||
DEFAULT_EXPORT_FILE = Path(__file__).resolve().parents[1] / "artifacts" / "aerolib.txt"
|
||||
|
||||
|
||||
def compute_nid(export_name: str) -> str:
|
||||
digest = hashlib.sha1(export_name.encode("utf-8") + NID_SUFFIX).digest()
|
||||
encoded = base64.b64encode(digest[:8][::-1]).decode("ascii")
|
||||
return encoded.rstrip("=").replace("/", "-")
|
||||
|
||||
|
||||
def read_names(path: Path) -> list[str]:
|
||||
try:
|
||||
return [
|
||||
line.strip()
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
except OSError as error:
|
||||
raise SystemExit(f"Unable to read catalog '{path}': {error}") from error
|
||||
|
||||
|
||||
def write_pair(nid: str, export_name: str) -> None:
|
||||
print(f"{nid}\t{export_name}")
|
||||
|
||||
|
||||
def lookup(args: argparse.Namespace) -> int:
|
||||
value = args.value.strip()
|
||||
if NID_PATTERN.fullmatch(value):
|
||||
for export_name in read_names(args.names):
|
||||
if compute_nid(export_name) == value:
|
||||
write_pair(value, export_name)
|
||||
return 0
|
||||
|
||||
print(f"NID not found in catalog: {value}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
names = set(read_names(args.names))
|
||||
write_pair(compute_nid(value), value)
|
||||
if value not in names:
|
||||
print("Warning: export name is not present in the catalog.", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def search(args: argparse.Namespace) -> int:
|
||||
names = read_names(args.names)
|
||||
if args.regex:
|
||||
try:
|
||||
pattern = re.compile(args.query, 0 if args.case_sensitive else re.IGNORECASE)
|
||||
except re.error as error:
|
||||
print(f"Invalid regular expression: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
matches = (name for name in names if pattern.search(name))
|
||||
elif args.case_sensitive:
|
||||
matches = (name for name in names if args.query in name)
|
||||
else:
|
||||
query = args.query.casefold()
|
||||
matches = (name for name in names if query in name.casefold())
|
||||
|
||||
count = 0
|
||||
for export_name in matches:
|
||||
write_pair(compute_nid(export_name), export_name)
|
||||
count += 1
|
||||
if args.limit and count >= args.limit:
|
||||
break
|
||||
|
||||
if count == 0:
|
||||
print(f"No catalog names matched: {args.query}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def export_catalog(args: argparse.Namespace) -> int:
|
||||
pairs = [(compute_nid(name), name) for name in read_names(args.names)]
|
||||
if args.sort == "nid":
|
||||
pairs.sort(key=lambda pair: (pair[0], pair[1]))
|
||||
elif args.sort == "name":
|
||||
pairs.sort(key=lambda pair: pair[1])
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as output:
|
||||
output.write("# NID\tExportName\n")
|
||||
for nid, export_name in pairs:
|
||||
output.write(f"{nid}\t{export_name}\n")
|
||||
except OSError as error:
|
||||
print(f"Unable to write catalog '{args.output}': {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Wrote {len(pairs)} entries to {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Inspect the SharpEmu PS5 export-name/NID catalog.",
|
||||
epilog=(
|
||||
"Examples:\n"
|
||||
" python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk\n"
|
||||
" python scripts/aerolib_catalog.py lookup sceKernelWaitSema\n"
|
||||
" python scripts/aerolib_catalog.py search VideoOut --limit 20\n"
|
||||
" python scripts/aerolib_catalog.py export"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--names",
|
||||
type=Path,
|
||||
default=DEFAULT_NAMES_FILE,
|
||||
help=f"source name list (default: {DEFAULT_NAMES_FILE})",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
lookup_parser = subparsers.add_parser(
|
||||
"lookup", help="resolve a NID or calculate the NID for an export name"
|
||||
)
|
||||
lookup_parser.add_argument("value", help="11-character NID or exact export name")
|
||||
lookup_parser.set_defaults(handler=lookup)
|
||||
|
||||
search_parser = subparsers.add_parser(
|
||||
"search", help="find export names and print matching NID/name pairs"
|
||||
)
|
||||
search_parser.add_argument("query", help="name substring or regular expression")
|
||||
search_parser.add_argument(
|
||||
"--limit", type=int, default=50, help="maximum matches; 0 means unlimited"
|
||||
)
|
||||
search_parser.add_argument(
|
||||
"--case-sensitive", action="store_true", help="match case exactly"
|
||||
)
|
||||
search_parser.add_argument(
|
||||
"--regex", action="store_true", help="treat the query as a regular expression"
|
||||
)
|
||||
search_parser.set_defaults(handler=search)
|
||||
|
||||
export_parser = subparsers.add_parser(
|
||||
"export", help="write every NID/name pair to a tab-separated text file"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"output",
|
||||
type=Path,
|
||||
nargs="?",
|
||||
default=DEFAULT_EXPORT_FILE,
|
||||
help=f"output file (default: {DEFAULT_EXPORT_FILE})",
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--sort",
|
||||
choices=("source", "nid", "name"),
|
||||
default="nid",
|
||||
help="output ordering (default: nid)",
|
||||
)
|
||||
export_parser.set_defaults(handler=export_catalog)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
return args.handler(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
// libKernel's address-wait primitives (sceKernelSyncOnAddress*) are the PS5's
|
||||
// futex-style wait/wake: a thread parks on a guest address until another thread
|
||||
// wakes that address. Guest runtimes (seen driving Juicy Realm, PPSA19268)
|
||||
// build their own spinlocks/queues on top of it and call the wait in a hot
|
||||
// loop; left unimplemented, every wait returns immediately and the runtime
|
||||
// busy-spins forever (millions of calls, no forward progress).
|
||||
//
|
||||
// This implements wait/wake over the existing cooperative-block scheduler,
|
||||
// keyed on the address. The real primitive takes a compare value so the wait
|
||||
// only sleeps while the address still holds the expected value; that exact
|
||||
// value is not recovered here, so each wait is given a bounded deadline and
|
||||
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
|
||||
// self-heals when the deadline expires and the guest re-checks its own
|
||||
// condition, which futex callers already tolerate. A matching wake releases
|
||||
// waiters immediately through the same key.
|
||||
public static class KernelSyncOnAddressCompatExports
|
||||
{
|
||||
// Safety-net poll interval. Real releases come from the wake side (generation
|
||||
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
|
||||
// raced/missed its wake stays parked before the guest re-evaluates. Kept
|
||||
// large: a short interval turns every parked waiter into a hot re-poll that
|
||||
// steals scheduler bandwidth from the threads that actually make progress
|
||||
// (including the ones that would issue the wake), so it must be a rare last
|
||||
// resort, not a spin substitute.
|
||||
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
// Per-address host gate for the non-cooperative (host main thread) fallback,
|
||||
// which cannot use the guest-thread scheduler's block mechanism.
|
||||
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
|
||||
|
||||
// Per-address wake generation. A wait captures the current generation and
|
||||
// its wake predicate stays unsatisfied (keeps the thread parked) until a
|
||||
// wake bumps it. This is what actually holds the thread blocked: a bare
|
||||
// "always satisfied" predicate is treated as an immediate late-arrival by
|
||||
// the dispatcher's race guard and never yields, leaving the guest to
|
||||
// busy-spin. The generation also closes the register-vs-park race for free:
|
||||
// a wake landing in that window bumps the generation, so the predicate is
|
||||
// already satisfied and the guest correctly resumes at once.
|
||||
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
|
||||
|
||||
private static long CurrentGeneration(ulong address) =>
|
||||
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
|
||||
|
||||
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Hc4CaR6JBL0",
|
||||
ExportName = "sceKernelSyncOnAddressWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int SyncOnAddressWait(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
if (address == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var observedGeneration = CurrentGeneration(address);
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
|
||||
|
||||
// Cooperative path: stay parked until a wake bumps this address's
|
||||
// generation (or the deadline expires as a self-heal). The guest
|
||||
// re-evaluates its own condition after resuming.
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelSyncOnAddressWait",
|
||||
WakeKey(address),
|
||||
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
|
||||
deadline))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Non-cooperative caller (host main thread): bounded host wait so a
|
||||
// missed wake self-heals instead of hanging.
|
||||
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
|
||||
lock (gate)
|
||||
{
|
||||
if (CurrentGeneration(address) == observedGeneration)
|
||||
{
|
||||
Monitor.Wait(gate, WaitSelfHealTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "q2y-wDIVWZA",
|
||||
ExportName = "sceKernelSyncOnAddressWake",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int SyncOnAddressWake(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
if (address == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// rsi carries the number of waiters to release (1 = wake-one, a large
|
||||
// value = wake-all); default to all if it looks unset.
|
||||
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
|
||||
|
||||
// Bump the generation first so a wait that has registered but not yet
|
||||
// parked sees the change and resumes instead of missing this wake.
|
||||
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
|
||||
|
||||
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
|
||||
|
||||
if (_hostAddressGates.TryGetValue(address, out var gate))
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -953,22 +953,13 @@ public static partial class Gen5SpirvTranslator
|
||||
case "VPkMulF16":
|
||||
case "VPkMinF16":
|
||||
case "VPkMaxF16":
|
||||
case "VPkFmaF16":
|
||||
if (!TryEmitPackedF16(instruction, out result, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case "VPkFmaF16":
|
||||
// Deliberately loud: a fused f16 FMA rounds the product+add once,
|
||||
// whereas doing the multiply-add in f32 and rounding to f16 at the
|
||||
// end double-rounds. Concrete miss: fma(0x4100, 0x7522, 0x04EA) is
|
||||
// 0x7A6B fused but 0x7A6A via f32. Exact emulation (round-to-odd
|
||||
// f32 product then RNE pack) is a planned follow-up slice.
|
||||
error =
|
||||
$"unsupported vop3p opcode {instruction.Opcode} " +
|
||||
"(fused f16 FMA requires single-rounding; deferred to a later slice)";
|
||||
return false;
|
||||
default:
|
||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||
return false;
|
||||
@@ -1008,8 +999,9 @@ public static partial class Gen5SpirvTranslator
|
||||
// even. For add and mul this is bit-exact to a true f16 op (the f32 result
|
||||
// rounds losslessly to f16 by the double-rounding theorem; a f16 product even
|
||||
// fits in f32 exactly). min/max carry no rounding, so they are exact once the
|
||||
// conversions are. v_pk_fma_f16 is intentionally not routed here because a
|
||||
// fused f16 FMA cannot be reproduced by an f32 multiply-add plus a pack.
|
||||
// conversions are. v_pk_fma_f16 cannot be reproduced by a plain f32
|
||||
// multiply-add plus a pack (that double-rounds), so it goes through the
|
||||
// round-to-odd sequence in EmitPackedF16FusedMultiplyAdd instead.
|
||||
private bool TryEmitPackedF16(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out uint result,
|
||||
@@ -1029,7 +1021,8 @@ public static partial class Gen5SpirvTranslator
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < 2; index++)
|
||||
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
|
||||
for (var index = 0; index < sourceCount; index++)
|
||||
{
|
||||
var source = instruction.Sources[index];
|
||||
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
|
||||
@@ -1054,6 +1047,12 @@ public static partial class Gen5SpirvTranslator
|
||||
{
|
||||
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
||||
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
|
||||
if (instruction.Opcode == "VPkFmaF16")
|
||||
{
|
||||
var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
|
||||
return EmitFloatToHalf(EmitPackedF16FusedMultiplyAdd(left, right, addend));
|
||||
}
|
||||
|
||||
var value = instruction.Opcode switch
|
||||
{
|
||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||
@@ -1065,6 +1064,75 @@ public static partial class Gen5SpirvTranslator
|
||||
return EmitFloatToHalf(Bitcast(_uintType, value));
|
||||
}
|
||||
|
||||
// Fused f16 multiply-add with a single rounding, emulated in f32 without the
|
||||
// Float16 capability. The f32 product of two widened f16 values is exact
|
||||
// (11-bit significands, and the exponent stays inside the f32 normal range:
|
||||
// any non-zero product magnitude is in [2^-48, 2^33]), so only the addition
|
||||
// rounds. An f32 add then an f16 pack would round twice; instead the add is
|
||||
// corrected to round-to-odd, which a following round-to-nearest-even pack
|
||||
// turns into the exactly-once-rounded fused result (innocuous double rounding
|
||||
// holds because f32 carries 24 significand bits >= 11 + 2).
|
||||
//
|
||||
// sum = RN(product + addend); Knuth's 2Sum recovers the exact residual
|
||||
// (product + addend) - sum from four more RN ops. 2Sum is exact for any two
|
||||
// finite f32 inputs; no intermediate here can overflow (|product| < 2^33,
|
||||
// |addend| < 2^16) and none can enter the f32 subnormal range (every finite
|
||||
// value in play is a multiple of 2^-48 by construction), so implementation
|
||||
// f32 denorm-flush modes never see a denormal. If the residual says the sum
|
||||
// was inexact and the sum's significand is even, step one ulp towards the
|
||||
// true value: consecutive floats have consecutive sign-magnitude encodings,
|
||||
// so that neighbour is the enclosing float with the odd significand.
|
||||
//
|
||||
// Inf/NaN inputs make the residual NaN (e.g. sum - addend = Inf - Inf); the
|
||||
// ordered compare below is then false and the IEEE sum passes through
|
||||
// unchanged. A residual of zero also covers the exact-sum case, where the
|
||||
// parity fix must not fire. Returns the round-to-odd f32 bit pattern.
|
||||
private uint EmitPackedF16FusedMultiplyAdd(uint left, uint right, uint addend)
|
||||
{
|
||||
var product = EmitPreciseFloat(SpirvOp.FMul, left, right);
|
||||
var sum = EmitPreciseFloat(SpirvOp.FAdd, product, addend);
|
||||
|
||||
var productPart = EmitPreciseFloat(SpirvOp.FSub, sum, addend);
|
||||
var addendPart = EmitPreciseFloat(SpirvOp.FSub, sum, productPart);
|
||||
var productError = EmitPreciseFloat(SpirvOp.FSub, product, productPart);
|
||||
var addendError = EmitPreciseFloat(SpirvOp.FSub, addend, addendPart);
|
||||
var residual = EmitPreciseFloat(SpirvOp.FAdd, productError, addendError);
|
||||
|
||||
var sumBits = Bitcast(_uintType, sum);
|
||||
var residualBits = Bitcast(_uintType, residual);
|
||||
var inexact = _module.AddInstruction(
|
||||
SpirvOp.FOrdNotEqual, _boolType, residual, Float(0));
|
||||
var evenSignificand = Equal(BitwiseAnd(sumBits, UInt(1)), 0);
|
||||
var adjust = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd, _boolType, inexact, evenSignificand);
|
||||
|
||||
// Residual sign relative to the sum picks the step direction: same sign
|
||||
// means the true value lies away from zero (encoding + 1), opposite sign
|
||||
// means towards zero (encoding - 1). The sum cannot be zero here (any
|
||||
// inexact sum has magnitude >= 2^-48) and cannot be the largest finite
|
||||
// value (its significand is odd), so the step never crosses zero or Inf.
|
||||
var towardZero = IsNotZero(
|
||||
BitwiseAnd(BitwiseXor(sumBits, residualBits), UInt(0x8000_0000)));
|
||||
var stepped = SelectU(
|
||||
towardZero,
|
||||
ISubU(sumBits, UInt(1)),
|
||||
IAdd(sumBits, UInt(1)));
|
||||
return SelectU(adjust, stepped, sumBits);
|
||||
}
|
||||
|
||||
// A float op the driver must evaluate exactly as written. The 2Sum
|
||||
// residual above is error-free only op by op; without NoContraction
|
||||
// driver compilers fold the sequence (e.g. contract product+sum into an
|
||||
// f32 fma and simplify the rebuilt terms), collapsing the residual to
|
||||
// zero. Observed on AMD RDNA3 Windows: the pinned midpoint case decays
|
||||
// to the double-rounded result unless every op in the chain is marked.
|
||||
private uint EmitPreciseFloat(SpirvOp operation, uint left, uint right)
|
||||
{
|
||||
var value = _module.AddInstruction(operation, _floatType, left, right);
|
||||
_module.AddDecoration(value, SpirvDecoration.NoContraction);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Reads source `index`, selects the half feeding this lane (op_sel / op_sel_hi),
|
||||
// widens it exactly to f32 and applies the lane's negate modifier (neg_lo / neg_hi).
|
||||
private uint EmitPackedF16Operand(
|
||||
|
||||
@@ -238,6 +238,7 @@ public enum SpirvDecoration : uint
|
||||
Binding = 33,
|
||||
DescriptorSet = 34,
|
||||
Offset = 35,
|
||||
NoContraction = 42,
|
||||
}
|
||||
|
||||
public enum SpirvBuiltIn : uint
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.LibcInternal;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.LibcInternal;
|
||||
|
||||
public sealed class LibcInternalExportsTests
|
||||
{
|
||||
private const ulong Base = 0x3_0000_0000;
|
||||
private const ulong InfoAddress = Base + 0x100;
|
||||
private const ulong ExpectedInfoSize = 32;
|
||||
|
||||
[Fact]
|
||||
public void HeapGetTraceInfo_NullPointer_ReturnsInvalidArgument()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
context[CpuRegister.Rdi] = 0;
|
||||
|
||||
var result = LibcInternalExports.LibcHeapGetTraceInfo(context);
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
result);
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeapGetTraceInfo_WrongSize_ReturnsInvalidArgument()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
Span<byte> sizeBytes = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
sizeBytes,
|
||||
ExpectedInfoSize - 1);
|
||||
|
||||
Assert.True(memory.TryWrite(InfoAddress, sizeBytes));
|
||||
|
||||
context[CpuRegister.Rdi] = InfoAddress;
|
||||
|
||||
var result = LibcInternalExports.LibcHeapGetTraceInfo(context);
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
result);
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeapGetTraceInfo_ValidBuffer_WritesStablePointers()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
Span<byte> sizeBytes = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
sizeBytes,
|
||||
ExpectedInfoSize);
|
||||
|
||||
Assert.True(memory.TryWrite(InfoAddress, sizeBytes));
|
||||
|
||||
context[CpuRegister.Rdi] = InfoAddress;
|
||||
|
||||
var firstResult =
|
||||
LibcInternalExports.LibcHeapGetTraceInfo(context);
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
firstResult);
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
|
||||
Assert.True(
|
||||
context.TryReadUInt64(
|
||||
InfoAddress + 16,
|
||||
out var firstMaskAddress));
|
||||
|
||||
Assert.True(
|
||||
context.TryReadUInt64(
|
||||
InfoAddress + 24,
|
||||
out var firstTableAddress));
|
||||
|
||||
Assert.NotEqual(0UL, firstMaskAddress);
|
||||
Assert.Equal(firstMaskAddress + 8UL, firstTableAddress);
|
||||
|
||||
var secondResult =
|
||||
LibcInternalExports.LibcHeapGetTraceInfo(context);
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
secondResult);
|
||||
|
||||
Assert.True(
|
||||
context.TryReadUInt64(
|
||||
InfoAddress + 16,
|
||||
out var secondMaskAddress));
|
||||
|
||||
Assert.True(
|
||||
context.TryReadUInt64(
|
||||
InfoAddress + 24,
|
||||
out var secondTableAddress));
|
||||
|
||||
Assert.Equal(firstMaskAddress, secondMaskAddress);
|
||||
Assert.Equal(firstTableAddress, secondTableAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeapGetTraceInfo_TruncatedOutput_ReturnsMemoryFault()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, 31);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
Span<byte> sizeBytes = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
sizeBytes,
|
||||
ExpectedInfoSize);
|
||||
|
||||
Assert.True(memory.TryWrite(Base, sizeBytes));
|
||||
|
||||
context[CpuRegister.Rdi] = Base;
|
||||
|
||||
var result = LibcInternalExports.LibcHeapGetTraceInfo(context);
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
result);
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@
|
||||
// [2] v_mul_lo_i32 -> low 32 bits of the same product
|
||||
// [3] store attempted with EXEC=0 -> must NOT land (sentinel remains)
|
||||
// [4] store after EXEC restored -> 1.5f (0x3FC00000)
|
||||
// [5] v_pk_fma_f16 fma(2.5h, 21024h, 7.496e-5h) -> 0x7A6B packed; the exact
|
||||
// sum sits just above an f16 midpoint, so a double-rounded f32
|
||||
// multiply-add would give 0x7A6A instead
|
||||
// [6] the same fma with the addend negated -> 0x7A6A packed (just below the
|
||||
// same midpoint), pinning the opposite rounding direction
|
||||
// Every other word of the buffer must still hold the sentinel afterwards.
|
||||
//
|
||||
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
||||
@@ -35,6 +40,12 @@ var expectedHi = (uint)(product >> 32);
|
||||
var expectedLo = (uint)product;
|
||||
var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
|
||||
|
||||
// v_pk_fma_f16 of (0x4100, 0x7522, 0x04EA) per lane: the exact product
|
||||
// 2.5 * 21024 = 52560 is an f16 tie (between 0x7A6A and 0x7A6B), so the tiny
|
||||
// addend decides the rounding direction under a single fused rounding.
|
||||
const uint ExpectedPkFma = 0x7A6B_7A6B;
|
||||
const uint ExpectedPkFmaNeg = 0x7A6A_7A6A;
|
||||
|
||||
unsafe
|
||||
{
|
||||
var spvPath = args.Length > 0
|
||||
@@ -352,6 +363,8 @@ unsafe
|
||||
("v_mul_lo_i32 lo(0x7FFFFFFF*0x10003)", words[2], expectedLo),
|
||||
("exec=0 store suppressed (offset 12 sentinel)", words[3], Sentinel),
|
||||
("store after exec restore (offset 16)", words[4], expectedRestored),
|
||||
("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma),
|
||||
("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg),
|
||||
};
|
||||
var failures = 0;
|
||||
foreach (var (name, actual, expected) in results)
|
||||
|
||||
@@ -42,6 +42,23 @@ const ulong ProgramAddress = 0x100000;
|
||||
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
||||
0xBF810000, // s_endpgm
|
||||
]),
|
||||
// Packed f16 (VOP3P) arithmetic, including the fused multiply-add. The
|
||||
// constants pin the double-rounding regression from the VOP3P first slice:
|
||||
// fma(0x4100, 0x7522, 0x04EA) must round once to 0x7A6B (an f32
|
||||
// multiply-add then pack yields 0x7A6A). The last fma exercises the src2
|
||||
// neg_lo/neg_hi modifier path.
|
||||
("pk-f16", true, [
|
||||
0x7E0002FF, 0x41004100, // v_mov_b32 v0, 0x41004100 (2.5 packed)
|
||||
0x7E0202FF, 0x75227522, // v_mov_b32 v1, 0x75227522 (21024 packed)
|
||||
0x7E0402FF, 0x04EA04EA, // v_mov_b32 v2, 0x04EA04EA (~7.496e-5 packed)
|
||||
0xCC0E4003, 0x1C0A0300, // v_pk_fma_f16 v3, v0, v1, v2
|
||||
0xCC0F4004, 0x18020500, // v_pk_add_f16 v4, v0, v2
|
||||
0xCC104005, 0x18020300, // v_pk_mul_f16 v5, v0, v1
|
||||
0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1
|
||||
0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1
|
||||
0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2
|
||||
0xBF810000, // s_endpgm
|
||||
]),
|
||||
("mrt", true, [
|
||||
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
||||
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
||||
@@ -115,7 +132,10 @@ const ulong ProgramAddress = 0x100000;
|
||||
// Executable end-to-end test: compute with real ALU instructions, then
|
||||
// buffer_store_dword results to guestBuffers[0] at offsets 0/4/8, prove
|
||||
// that a store with EXEC=0 does not land (offset 12 stays sentinel), and
|
||||
// that stores work again after EXEC is restored (offset 16).
|
||||
// that stores work again after EXEC is restored (offset 16). Offsets 20/24
|
||||
// hold the packed fused f16 FMA and its negated-addend twin, whose exact
|
||||
// results (0x7A6B7A6B / 0x7A6A7A6A) straddle an f16 midpoint and therefore
|
||||
// catch any double-rounding regression on real hardware.
|
||||
("exec", true, [
|
||||
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
|
||||
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
|
||||
@@ -133,6 +153,13 @@ const ulong ProgramAddress = 0x100000;
|
||||
0xE070000C, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:12 (masked, must not land)
|
||||
0xBEFE03C1, // s_mov_b32 exec_lo, -1 -> lane active again
|
||||
0xE0700010, 0x80020000, // buffer_store_dword v0, off, s[8:11], 0 offset:16
|
||||
0x7E0E02FF, 0x41004100, // v_mov_b32 v7, 0x41004100 (2.5 packed)
|
||||
0x7E1002FF, 0x75227522, // v_mov_b32 v8, 0x75227522 (21024 packed)
|
||||
0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed)
|
||||
0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9
|
||||
0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9
|
||||
0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20
|
||||
0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24
|
||||
0xBF810000, // s_endpgm
|
||||
]),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user