Compare commits
80 Commits
xnetcat/gp
...
1016_part3
| Author | SHA1 | Date |
|---|---|---|
|
|
0666071f79 | |
|
|
ee9660d81d | |
|
|
28b2f5709d | |
|
|
f7e8f54dbc | |
|
|
afd0dffcf6 | |
|
|
cde859c5e5 | |
|
|
757567bd15 | |
|
|
62bbf51bbc | |
|
|
c1a0da742f | |
|
|
ad0fd8be7b | |
|
|
6f2937936a | |
|
|
13b77eedcb | |
|
|
a7bfef97ac | |
|
|
10254a3c7e | |
|
|
3d65921eb7 | |
|
|
cd77a68c87 | |
|
|
a662295409 | |
|
|
4d8f5a75d5 | |
|
|
d3deab28aa | |
|
|
8c0ca1a2a7 | |
|
|
9d530f63f9 | |
|
|
f945215935 | |
|
|
c37f76f12d | |
|
|
73666f529e | |
|
|
0e55fa924c | |
|
|
d3f584a2f1 | |
|
|
dd6a936a12 | |
|
|
c0028d304b | |
|
|
ef10b4a1d8 | |
|
|
7be8a4b389 | |
|
|
1984901a31 | |
|
|
7b30868b03 | |
|
|
dc3b1d305d | |
|
|
7866bd4217 | |
|
|
30429400ed | |
|
|
78792d7a5a | |
|
|
7c08757173 | |
|
|
2a2ea5a35a | |
|
|
34e8900035 | |
|
|
923f75d340 | |
|
|
946bd0a777 | |
|
|
d31138fcca | |
|
|
077c9a940f | |
|
|
ea1dcbcea1 | |
|
|
53f233da9b | |
|
|
b368247d64 | |
|
|
c566dba5a1 | |
|
|
8d884f2692 | |
|
|
679ea0f355 | |
|
|
124dfa8afd | |
|
|
1b66707daf | |
|
|
619ac2b367 | |
|
|
a1c0d7d36d | |
|
|
6cc753e0fc | |
|
|
beedb746d9 | |
|
|
82c9731f74 | |
|
|
d708227541 | |
|
|
b02c06181a | |
|
|
e1edda7772 | |
|
|
775141d58c | |
|
|
38f7196470 | |
|
|
8acc081205 | |
|
|
98f223ce69 | |
|
|
8b631d3b03 | |
|
|
a9356eb8d2 | |
|
|
a73e424a27 | |
|
|
a0fd9dcb83 | |
|
|
4e6d1e0d9a | |
|
|
d809f84491 | |
|
|
da0953845b | |
|
|
db32c9d882 | |
|
|
30321142cb | |
|
|
1e784a035f | |
|
|
7ab6de1b93 | |
|
|
70dcbe4e05 | |
|
|
0aa611e317 | |
|
|
435bbd4844 | |
|
|
3e2ad63303 | |
|
|
ac8608449f | |
|
|
2df6f5b0a0 |
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 538 KiB |
10
README.md
10
README.md
|
|
@ -58,17 +58,17 @@ Our goal is **not** to emulate PS4 games, as there is already an excellent emula
|
|||
|
||||
## Games Tested
|
||||
|
||||
| Demons Souls Remake | Astro Bot |
|
||||
| Demon's Souls Remake | ASTRO BOT |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| Hades | Dead Cells |
|
||||
| ASTRO's PLAYROOM | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| PAC-MAN World Re-PAC | Tomb Raider V Remastered |
|
||||
| God of War: Sons of Sparta | Tomb Raider V Remastered |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Status
|
||||
|
||||
|
|
|
|||
|
|
@ -1925,6 +1925,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
return HasUsableLleLibcExport("tcVi5SivF7Q", "sprintf");
|
||||
}
|
||||
if (IsLibcFileObjectExport(exportName))
|
||||
{
|
||||
return HasUsableLleLibcExport(ComputePsNid(exportName), exportName);
|
||||
}
|
||||
if (string.Equals(value, "0", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
|
|
@ -3978,6 +3982,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
public bool TryGetGuestThreadStackBounds(
|
||||
ulong threadHandle,
|
||||
out ulong stackBase,
|
||||
out ulong stackSize)
|
||||
{
|
||||
stackBase = 0;
|
||||
stackSize = 0;
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
if (!_guestThreads.TryGetValue(threadHandle, out var thread) ||
|
||||
thread.StackBase == 0 ||
|
||||
thread.StackSize == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stackBase = thread.StackBase;
|
||||
stackSize = thread.StackSize;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context)
|
||||
{
|
||||
if (threadHandle == 0)
|
||||
|
|
@ -5344,7 +5375,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteGuestExceptionContext(
|
||||
internal static bool TryWriteGuestExceptionContext(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
GuestCpuContinuation continuation,
|
||||
|
|
@ -5355,11 +5386,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
|
||||
|
||||
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
|
||||
// Orbis ucontext_t has a 0x10-byte signal mask and 0x30 bytes of
|
||||
// private fields before its amd64 mcontext. These offsets match the
|
||||
// platform ABI used by libScePs5Util and Unity's Boehm GC. Supplying a
|
||||
// bare mcontext here makes the collector miss live register roots.
|
||||
const int mcontext = 0x40;
|
||||
const int mcontext = 0;
|
||||
Write64(mcontext + 0x08, hasContinuation ? continuation.Rdi : context[CpuRegister.Rdi]);
|
||||
Write64(mcontext + 0x10, hasContinuation ? continuation.Rsi : context[CpuRegister.Rsi]);
|
||||
Write64(mcontext + 0x18, hasContinuation ? continuation.Rdx : context[CpuRegister.Rdx]);
|
||||
|
|
@ -5378,9 +5405,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
var rip = hasContinuation ? continuation.Rip : context.Rip;
|
||||
var rsp = hasContinuation ? continuation.Rsp : context[CpuRegister.Rsp];
|
||||
Write64(mcontext + 0xA0, rip);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : context.Rflags);
|
||||
Write64(mcontext + 0xB8, rsp);
|
||||
Write64(mcontext + 0xC8, 0x480); // sizeof(Orbis mcontext_t)
|
||||
Write64(mcontext + 0xF8, rsp);
|
||||
Write64(mcontext + 0x440, hasContinuation ? continuation.FsBase : context.FsBase);
|
||||
Write64(mcontext + 0x448, hasContinuation ? continuation.GsBase : context.GsBase);
|
||||
return context.Memory.TryWrite(address, bytes);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,44 @@ internal static class GuestRedZonePatcher
|
|||
private const ulong PageSize = 0x1000;
|
||||
private const ulong AllocationAlignment = 0x10000;
|
||||
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
|
||||
private const int MaximumJumpTableEntries = 4096;
|
||||
private const int MaximumJumpTableSetupInstructions = 6;
|
||||
private const ulong LargeExecutableBytes = 32UL << 20;
|
||||
private static readonly TimeSpan ProgressInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
private sealed class Progress(string hostName, string phase, int total)
|
||||
{
|
||||
private readonly long _started = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
private long _lastReport = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
|
||||
public void Report(int done)
|
||||
{
|
||||
var now = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (System.Diagnostics.Stopwatch.GetElapsedTime(_lastReport, now) < ProgressInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastReport = now;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] {hostName} red-zone {phase}: {done}/{total} ({(total == 0 ? 100 : done * 100L / total)}%) " +
|
||||
$"after {System.Diagnostics.Stopwatch.GetElapsedTime(_started, now).TotalSeconds:F0} s");
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ExecutableBytes(IReadOnlyList<ProgramHeader> programHeaders)
|
||||
{
|
||||
var bytes = 0UL;
|
||||
foreach (var header in programHeaders)
|
||||
{
|
||||
if (header.HeaderType == ProgramHeaderType.Load && (header.Flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
bytes += header.FileSize;
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal enum SpanRefusal
|
||||
{
|
||||
|
|
@ -27,6 +65,7 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetAfter,
|
||||
StackAfter,
|
||||
TooShort,
|
||||
IndirectBranch,
|
||||
}
|
||||
|
||||
public static PatchResult Patch(
|
||||
|
|
@ -62,8 +101,19 @@ internal static class GuestRedZonePatcher
|
|||
return default;
|
||||
}
|
||||
|
||||
var executableBytes = ExecutableBytes(programHeaders);
|
||||
var large = executableBytes >= LargeExecutableBytes;
|
||||
if (large)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] {hostName} red-zone patch: scanning {functionStarts.Length} functions in {executableBytes >> 20} MB of code; " +
|
||||
"a large executable takes a while before the game window opens.");
|
||||
}
|
||||
|
||||
var sites = CollectPatchSites(
|
||||
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
|
||||
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan,
|
||||
large ? new Progress(hostName, "scan", functionStarts.Length) : null);
|
||||
sites.Sort(static (left, right) => left.Address.CompareTo(right.Address));
|
||||
if (sites.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -94,8 +144,10 @@ internal static class GuestRedZonePatcher
|
|||
var patched = 0;
|
||||
var failed = 0;
|
||||
var previousEnd = 0UL;
|
||||
var patchProgress = large ? new Progress(hostName, "patch", sites.Count) : null;
|
||||
foreach (var site in sites)
|
||||
{
|
||||
patchProgress?.Report(patched + failed);
|
||||
// Sites are produced in increasing address order. Patching two that
|
||||
// overlap would write one jump over another and send the trampoline
|
||||
// into whatever follows, so refuse rather than corrupt guest code.
|
||||
|
|
@ -128,6 +180,7 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetRefusals = scan.BranchTargetRefusals,
|
||||
StackRefusals = scan.StackRefusals,
|
||||
TooShortRefusals = scan.TooShortRefusals,
|
||||
IndirectBranchRefusals = scan.IndirectBranchRefusals,
|
||||
TrampolineBytes = trampolineCursor - trampolineBase,
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -135,7 +188,8 @@ internal static class GuestRedZonePatcher
|
|||
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
|
||||
$"unrelocatable={result.UnrelocatableSites} " +
|
||||
$"(control_flow={result.ControlFlowRefusals} branch_target_after={result.BranchTargetRefusals} " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals} " +
|
||||
$"indirect_branch={result.IndirectBranchRefusals}) " +
|
||||
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
|
||||
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
|
||||
return result;
|
||||
|
|
@ -149,15 +203,17 @@ internal static class GuestRedZonePatcher
|
|||
bool protectRedZone,
|
||||
bool splitVectorStores,
|
||||
bool rewriteSha,
|
||||
out PatchResult result)
|
||||
out PatchResult result,
|
||||
Progress? progress = null)
|
||||
{
|
||||
var sites = new List<PatchSite>();
|
||||
var scannedFunctions = 0;
|
||||
var functionCount = 0;
|
||||
var redZoneFunctionCount = 0;
|
||||
var instructionCount = 0;
|
||||
var vectorStoreCount = 0;
|
||||
var unrelocatableSites = 0;
|
||||
var refusalCounts = new int[5];
|
||||
var refusalCounts = new int[6];
|
||||
var shaInstructionCount = 0;
|
||||
|
||||
foreach (var header in programHeaders)
|
||||
|
|
@ -190,6 +246,7 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
for (var functionIndex = 0; functionIndex < segmentFunctions.Length; functionIndex++)
|
||||
{
|
||||
progress?.Report(scannedFunctions++);
|
||||
var functionStart = segmentFunctions[functionIndex];
|
||||
var functionEnd = functionIndex + 1 < segmentFunctions.Length
|
||||
? segmentFunctions[functionIndex + 1]
|
||||
|
|
@ -222,6 +279,14 @@ internal static class GuestRedZonePatcher
|
|||
redZoneFunctionCount++;
|
||||
}
|
||||
var branchTargets = CollectBranchTargets(decoded);
|
||||
var canBorrowFollowingInstructions = TryCollectJumpTableTargets(
|
||||
memory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
decoded,
|
||||
functionStart,
|
||||
functionEnd,
|
||||
branchTargets);
|
||||
|
||||
var lastSiteEnd = 0UL;
|
||||
|
||||
|
|
@ -236,27 +301,32 @@ internal static class GuestRedZonePatcher
|
|||
continue;
|
||||
}
|
||||
|
||||
// A backward span reaches instructions that an earlier site
|
||||
// may already have replaced with its own jump. Two jumps
|
||||
// written over each other corrupt control flow, so a span is
|
||||
// only usable when it starts at or after the end of the last
|
||||
// one. Forward spans cannot overlap by construction; the
|
||||
// check costs nothing and documents the invariant.
|
||||
var backward = false;
|
||||
if (!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span, out var refusal))
|
||||
var span = default(PatchSite);
|
||||
var refusal = SpanRefusal.TooShort;
|
||||
var forward = TryBuildPatchSpan(
|
||||
decoded,
|
||||
instructionIndex,
|
||||
branchTargets,
|
||||
canBorrowFollowingInstructions,
|
||||
out span,
|
||||
out refusal);
|
||||
var backward = !forward &&
|
||||
canBorrowFollowingInstructions &&
|
||||
TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
if (!forward && !backward)
|
||||
{
|
||||
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
}
|
||||
|
||||
if ((!backward && refusal != SpanRefusal.None) || span.Address < lastSiteEnd)
|
||||
{
|
||||
// Refused spans never enter the site list, so they are
|
||||
// invisible in FailedSites. Count them separately.
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (span.Address < lastSiteEnd)
|
||||
{
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)SpanRefusal.TooShort]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
sites.Add(span);
|
||||
lastSiteEnd = span.Address + (ulong)span.ByteLength;
|
||||
if (splitVectorStores)
|
||||
|
|
@ -268,8 +338,6 @@ internal static class GuestRedZonePatcher
|
|||
shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
|
||||
}
|
||||
|
||||
// A backward span already ends at the current instruction, so
|
||||
// only a forward one consumes the instructions that follow.
|
||||
if (!backward)
|
||||
{
|
||||
instructionIndex += span.Instructions.Count - 1;
|
||||
|
|
@ -290,6 +358,7 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetRefusals = refusalCounts[(int)SpanRefusal.BranchTargetAfter],
|
||||
StackRefusals = refusalCounts[(int)SpanRefusal.StackAfter],
|
||||
TooShortRefusals = refusalCounts[(int)SpanRefusal.TooShort],
|
||||
IndirectBranchRefusals = refusalCounts[(int)SpanRefusal.IndirectBranch],
|
||||
ShaInstructionCount = shaInstructionCount,
|
||||
};
|
||||
return sites;
|
||||
|
|
@ -347,6 +416,321 @@ internal static class GuestRedZonePatcher
|
|||
return targets;
|
||||
}
|
||||
|
||||
private static bool TryCollectJumpTableTargets(
|
||||
IVirtualMemory memory,
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
ulong imageBase,
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
ulong functionStart,
|
||||
ulong functionEnd,
|
||||
HashSet<ulong> branchTargets)
|
||||
{
|
||||
List<ulong>? resolvedTargets = null;
|
||||
HashSet<ulong>? instructionBoundaries = null;
|
||||
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
|
||||
{
|
||||
if (decoded[instructionIndex].Instruction.FlowControl != FlowControl.IndirectBranch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
instructionBoundaries ??= decoded
|
||||
.Select(static entry => entry.Instruction.IP)
|
||||
.ToHashSet();
|
||||
resolvedTargets ??= new List<ulong>();
|
||||
if (!TryResolveJumpTable(
|
||||
memory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
decoded,
|
||||
instructionIndex,
|
||||
functionStart,
|
||||
functionEnd,
|
||||
instructionBoundaries,
|
||||
branchTargets,
|
||||
resolvedTargets))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedTargets is not null)
|
||||
{
|
||||
branchTargets.UnionWith(resolvedTargets);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveJumpTable(
|
||||
IVirtualMemory memory,
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
ulong imageBase,
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int jumpIndex,
|
||||
ulong functionStart,
|
||||
ulong functionEnd,
|
||||
IReadOnlySet<ulong> instructionBoundaries,
|
||||
IReadOnlySet<ulong> knownBranchTargets,
|
||||
List<ulong> targets)
|
||||
{
|
||||
if (jumpIndex < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var jump = decoded[jumpIndex].Instruction;
|
||||
var add = decoded[jumpIndex - 1].Instruction;
|
||||
var load = decoded[jumpIndex - 2].Instruction;
|
||||
if (jump.OpCount != 1 || jump.GetOpKind(0) != OpKind.Register ||
|
||||
add.Mnemonic != Mnemonic.Add || add.OpCount != 2 ||
|
||||
add.GetOpKind(0) != OpKind.Register || add.GetOpKind(1) != OpKind.Register ||
|
||||
load.Mnemonic != Mnemonic.Movsxd || load.OpCount != 2 ||
|
||||
load.GetOpKind(0) != OpKind.Register || load.GetOpKind(1) != OpKind.Memory ||
|
||||
load.MemorySize.GetSize() != sizeof(int) || load.MemoryIndexScale != sizeof(int) ||
|
||||
load.MemoryBase == Register.None || load.MemoryIndex == Register.None ||
|
||||
load.MemoryDisplacement64 != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetRegister = jump.GetOpRegister(0).GetFullRegister();
|
||||
var tableRegister = add.GetOpRegister(1).GetFullRegister();
|
||||
var selectorRegister = load.MemoryIndex.GetFullRegister();
|
||||
if (targetRegister != add.GetOpRegister(0).GetFullRegister() ||
|
||||
targetRegister != load.GetOpRegister(0).GetFullRegister() ||
|
||||
tableRegister != load.MemoryBase.GetFullRegister() ||
|
||||
targetRegister.GetSize() != sizeof(ulong) ||
|
||||
tableRegister.GetSize() != sizeof(ulong) ||
|
||||
selectorRegister.GetSize() != sizeof(ulong) ||
|
||||
tableRegister == selectorRegister)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var branchIndex = -1;
|
||||
for (var index = jumpIndex - 3;
|
||||
index >= 1 && index >= jumpIndex - MaximumJumpTableSetupInstructions;
|
||||
index--)
|
||||
{
|
||||
var candidate = decoded[index].Instruction;
|
||||
if (candidate.Mnemonic == Mnemonic.Ja)
|
||||
{
|
||||
branchIndex = index;
|
||||
break;
|
||||
}
|
||||
|
||||
if (candidate.Mnemonic != Mnemonic.Lea)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (branchIndex < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = branchIndex + 1; index <= jumpIndex; index++)
|
||||
{
|
||||
if (knownBranchTargets.Contains(decoded[index].Instruction.IP))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var boundsBranch = decoded[branchIndex].Instruction;
|
||||
var boundsCompare = decoded[branchIndex - 1].Instruction;
|
||||
if (boundsBranch.FlowControl != FlowControl.ConditionalBranch ||
|
||||
!TryGetNearBranchTarget(boundsBranch, out var defaultTarget) ||
|
||||
defaultTarget < functionStart || defaultTarget >= functionEnd ||
|
||||
boundsCompare.Mnemonic != Mnemonic.Cmp || boundsCompare.OpCount != 2 ||
|
||||
boundsCompare.GetOpKind(0) != OpKind.Register ||
|
||||
boundsCompare.GetOpRegister(0).GetFullRegister() != selectorRegister ||
|
||||
boundsCompare.GetOpRegister(0).GetSize() != sizeof(int) ||
|
||||
!TryGetNonNegativeImmediate(boundsCompare, 1, out var maximumIndex) ||
|
||||
maximumIndex >= MaximumJumpTableEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tableAddress = 0UL;
|
||||
var tableDefinitionFound = false;
|
||||
for (var index = branchIndex + 1; index <= jumpIndex - 3; index++)
|
||||
{
|
||||
var candidate = decoded[index].Instruction;
|
||||
if (candidate.OpCount < 1 || candidate.GetOpKind(0) != OpKind.Register)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var writtenRegister = candidate.GetOpRegister(0).GetFullRegister();
|
||||
if (writtenRegister == selectorRegister)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (writtenRegister != tableRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tableDefinitionFound || candidate.OpCount != 2 ||
|
||||
candidate.GetOpKind(1) != OpKind.Memory || !candidate.IsIPRelativeMemoryOperand)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
tableAddress = candidate.IPRelativeMemoryAddress;
|
||||
tableDefinitionFound = true;
|
||||
}
|
||||
|
||||
if (!tableDefinitionFound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entryCount = checked((int)maximumIndex + 1);
|
||||
var tableBytes = checked(entryCount * sizeof(int));
|
||||
if (!IsRangeInReadableLoad(programHeaders, imageBase, tableAddress, (ulong)tableBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entries = GC.AllocateUninitializedArray<byte>(tableBytes);
|
||||
if (!memory.TryRead(tableAddress, entries))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstTarget = targets.Count;
|
||||
for (var index = 0; index < entryCount; index++)
|
||||
{
|
||||
var relative = BinaryPrimitives.ReadInt32LittleEndian(entries.AsSpan(index * sizeof(int)));
|
||||
if (!TryAddSignedOffset(tableAddress, relative, out var target) ||
|
||||
target < functionStart || target >= functionEnd ||
|
||||
!instructionBoundaries.Contains(target))
|
||||
{
|
||||
targets.RemoveRange(firstTarget, targets.Count - firstTarget);
|
||||
return false;
|
||||
}
|
||||
|
||||
targets.Add(target);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsRangeInReadableLoad(
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
ulong imageBase,
|
||||
ulong address,
|
||||
ulong length)
|
||||
{
|
||||
foreach (var header in programHeaders)
|
||||
{
|
||||
if (header.HeaderType != ProgramHeaderType.Load ||
|
||||
(header.Flags & ProgramHeaderFlags.Read) == 0 ||
|
||||
header.FileSize == 0 ||
|
||||
header.VirtualAddress > ulong.MaxValue - imageBase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = imageBase + header.VirtualAddress;
|
||||
if (address < start || address - start > header.FileSize ||
|
||||
length > header.FileSize - (address - start))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAddSignedOffset(ulong address, int offset, out ulong result)
|
||||
{
|
||||
if (offset >= 0)
|
||||
{
|
||||
var positive = (ulong)offset;
|
||||
if (positive <= ulong.MaxValue - address)
|
||||
{
|
||||
result = address + positive;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var negative = (ulong)-(long)offset;
|
||||
if (negative <= address)
|
||||
{
|
||||
result = address - negative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetNearBranchTarget(in Instruction instruction, out ulong target)
|
||||
{
|
||||
for (var operand = 0; operand < instruction.OpCount; operand++)
|
||||
{
|
||||
switch (instruction.GetOpKind(operand))
|
||||
{
|
||||
case OpKind.NearBranch16:
|
||||
target = instruction.NearBranch16;
|
||||
return true;
|
||||
case OpKind.NearBranch32:
|
||||
target = instruction.NearBranch32;
|
||||
return true;
|
||||
case OpKind.NearBranch64:
|
||||
target = instruction.NearBranch64;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetNonNegativeImmediate(in Instruction instruction, int operand, out ulong value)
|
||||
{
|
||||
switch (instruction.GetOpKind(operand))
|
||||
{
|
||||
case OpKind.Immediate8:
|
||||
value = instruction.Immediate8;
|
||||
return true;
|
||||
case OpKind.Immediate16:
|
||||
value = instruction.Immediate16;
|
||||
return true;
|
||||
case OpKind.Immediate32:
|
||||
value = instruction.Immediate32;
|
||||
return true;
|
||||
case OpKind.Immediate64:
|
||||
value = instruction.Immediate64;
|
||||
return true;
|
||||
case OpKind.Immediate8to16 when instruction.Immediate8to16 >= 0:
|
||||
value = (ulong)instruction.Immediate8to16;
|
||||
return true;
|
||||
case OpKind.Immediate8to32 when instruction.Immediate8to32 >= 0:
|
||||
value = (ulong)instruction.Immediate8to32;
|
||||
return true;
|
||||
case OpKind.Immediate8to64 when instruction.Immediate8to64 >= 0:
|
||||
value = (ulong)instruction.Immediate8to64;
|
||||
return true;
|
||||
case OpKind.Immediate32to64 when instruction.Immediate32to64 >= 0:
|
||||
value = (ulong)instruction.Immediate32to64;
|
||||
return true;
|
||||
default:
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the enclosing-span search over a raw instruction range, so the
|
||||
/// behaviour can be pinned on the byte sequences that occur in real guest
|
||||
|
|
@ -376,7 +760,7 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
|
||||
if (siteIndex < 0 ||
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), true, out var site, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -417,7 +801,10 @@ internal static class GuestRedZonePatcher
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
var hasIndirectBranch = decoded.Any(static entry =>
|
||||
entry.Instruction.FlowControl == FlowControl.IndirectBranch);
|
||||
if (hasIndirectBranch ||
|
||||
!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -551,12 +938,13 @@ internal static class GuestRedZonePatcher
|
|||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site) =>
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, true, out site, out _);
|
||||
|
||||
private static bool TryBuildPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
bool canBorrowFollowingInstructions,
|
||||
out PatchSite site,
|
||||
out SpanRefusal refusal)
|
||||
{
|
||||
|
|
@ -568,6 +956,7 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
var instruction = decoded[index].Instruction;
|
||||
if (index != startIndex &&
|
||||
canBorrowFollowingInstructions &&
|
||||
!branchTargets.Contains(instruction.IP) &&
|
||||
IsRelocatableDirectBranch(instruction))
|
||||
{
|
||||
|
|
@ -578,7 +967,11 @@ internal static class GuestRedZonePatcher
|
|||
break;
|
||||
}
|
||||
|
||||
if (instruction.FlowControl != FlowControl.Next)
|
||||
if (index != startIndex && !canBorrowFollowingInstructions)
|
||||
{
|
||||
refusal = SpanRefusal.IndirectBranch;
|
||||
}
|
||||
else if (instruction.FlowControl != FlowControl.Next)
|
||||
{
|
||||
refusal = SpanRefusal.ControlFlow;
|
||||
}
|
||||
|
|
@ -1089,6 +1482,8 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
public int TooShortRefusals { get; init; }
|
||||
|
||||
public int IndirectBranchRefusals { get; init; }
|
||||
|
||||
public ulong TrampolineBytes { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
FiberExports.ResetRuntimeState();
|
||||
Http2Exports.ResetRuntimeState();
|
||||
NpAuthExports.ResetRuntimeState();
|
||||
NpUniversalDataSystemExports.ResetRuntimeState();
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,21 @@ public interface IGuestThreadScheduler
|
|||
/// </summary>
|
||||
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the exact mapped guest stack owned by a scheduler thread. The
|
||||
/// default keeps alternate/test schedulers source-compatible when they do
|
||||
/// not own guest stack mappings.
|
||||
/// </summary>
|
||||
bool TryGetGuestThreadStackBounds(
|
||||
ulong threadHandle,
|
||||
out ulong stackBase,
|
||||
out ulong stackSize)
|
||||
{
|
||||
stackBase = 0;
|
||||
stackSize = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||
|
||||
bool TryJoinThread(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public static class HleDataSymbols
|
|||
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
||||
private static readonly nint _progNameBufferAddress = Allocate(ProgNameMaxBytes + 1);
|
||||
private static readonly nint _progNamePointerAddress = Allocate(nint.Size);
|
||||
private static readonly nint _argumentVectorAddress = Allocate(nint.Size * 2);
|
||||
private static readonly nint _libcNeedFlagAddress = Allocate(sizeof(uint));
|
||||
private static readonly nint _libcInternalNeedFlagAddress = Allocate(sizeof(uint));
|
||||
|
||||
|
|
@ -55,6 +56,12 @@ public static class HleDataSymbols
|
|||
yield return LibcInternalNeedFlagNid;
|
||||
}
|
||||
|
||||
public static int ProcessArgumentCount =>
|
||||
_progNameBufferAddress != 0 && _argumentVectorAddress != 0 ? 1 : 0;
|
||||
|
||||
public static ulong ProcessArgumentVectorAddress =>
|
||||
unchecked((ulong)_argumentVectorAddress);
|
||||
|
||||
public static void ConfigureProcessImageName(string? processImageName)
|
||||
{
|
||||
var effectiveName = string.IsNullOrWhiteSpace(processImageName)
|
||||
|
|
@ -65,7 +72,9 @@ public static class HleDataSymbols
|
|||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
|
||||
if (_progNameBufferAddress == 0 ||
|
||||
_progNamePointerAddress == 0 ||
|
||||
_argumentVectorAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -77,6 +86,8 @@ public static class HleDataSymbols
|
|||
|
||||
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
|
||||
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
|
||||
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
|
||||
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ namespace SharpEmu.Libs.AppContent;
|
|||
public static class AppContentExports
|
||||
{
|
||||
private const ulong FallbackAvailableSpaceKb = 1024UL * 1024UL; // 1 GiB
|
||||
private const int AppContentErrorParameter = unchecked((int)0x80D90002);
|
||||
private const int AppContentErrorDrmNoEntitlement = unchecked((int)0x80D90007);
|
||||
private const int MountPointSize = 16;
|
||||
private const ulong BootParamAttrOffset = 4;
|
||||
private const string Temp0MountPoint = "/temp0";
|
||||
private const uint AppParamSkuFlag = 0;
|
||||
|
|
@ -99,6 +102,29 @@ public static class AppContentExports
|
|||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VANhIWcqYak",
|
||||
ExportName = "sceAppContentAddcontMount",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAppContent")]
|
||||
public static int AppContentAddcontMount(CpuContext ctx)
|
||||
{
|
||||
var entitlementLabelAddress = ctx[CpuRegister.Rsi];
|
||||
var mountPointAddress = ctx[CpuRegister.Rdx];
|
||||
if (entitlementLabelAddress == 0 || mountPointAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(AppContentErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> emptyMountPoint = stackalloc byte[MountPointSize];
|
||||
if (!ctx.Memory.TryWrite(mountPointAddress, emptyMountPoint))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(AppContentErrorDrmNoEntitlement);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "buYbeLOGWmA",
|
||||
ExportName = "sceAppContentTemporaryDataMount2",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ public static class AudioOut2Exports
|
|||
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
|
||||
private const int AudioOut2ContextParamSize = 0x40;
|
||||
private const int AudioOut2ContextMemorySize = 0x4000;
|
||||
private const int AudioOut2ContextMemoryAlignment = 0x100;
|
||||
// Exact object body size. Do not page-align to 64K — the RAGE Main Thread
|
||||
// stack-allocates this and a 64K VLA is what planted 0x10000 on the canary.
|
||||
private const int SpeakerArrayHeaderSize = 0x40;
|
||||
|
|
@ -36,6 +35,17 @@ public static class AudioOut2Exports
|
|||
private const int SpeakerArrayDivisorFieldOffset = 0x34;
|
||||
private const int SpeakerArrayResultFieldOffset = 0x3C;
|
||||
private const uint SpeakerArrayDefaultDivisor = 1;
|
||||
private const int SpeakerArrayCoefficientBytes = 0x400;
|
||||
|
||||
private static readonly string _stackOutBufferModes =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_AUDIO_OUT2_STACK_WRITES") ?? "1";
|
||||
|
||||
private static bool AllowStackOut(string which) =>
|
||||
string.Equals(_stackOutBufferModes, "1", StringComparison.Ordinal) ||
|
||||
_stackOutBufferModes
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Contains(which, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// SceAudioOut2SpeakerArrayParam: Position* (0x00), uint uiNumSpeakers (0x08),
|
||||
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
|
||||
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
|
||||
|
|
@ -60,6 +70,7 @@ public static class AudioOut2Exports
|
|||
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
||||
private const int AttributeEntrySize = 0x18;
|
||||
private const uint PortAttributeIdPcm = 0;
|
||||
private const ushort AudioOut2PortTypeVibration = 6;
|
||||
private const uint PortAttributeIdGain = 1;
|
||||
private const uint PortAttributeIdPassthrough = 5;
|
||||
private const uint PortAttributeIdAmbisonics = 8;
|
||||
|
|
@ -88,6 +99,11 @@ public static class AudioOut2Exports
|
|||
private static long _pushTraceCount;
|
||||
private static long _submitTraceCount;
|
||||
private static long _submitSkipTraceCount;
|
||||
private static long _bedWriteTraceCount;
|
||||
private static long _portCreateTraceCount;
|
||||
private static long _portDestroyTraceCount;
|
||||
private static long _portStateTraceCount;
|
||||
private static long _attributeCallTraceCount;
|
||||
private static long _attributePcmTraceCount;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
|
||||
|
|
@ -212,7 +228,7 @@ public static class AudioOut2Exports
|
|||
public uint Ambisonics = AmbisonicsNone;
|
||||
public uint Passthrough;
|
||||
|
||||
public int PcmPending;
|
||||
public byte[]? PcmData;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +242,7 @@ public static class AudioOut2Exports
|
|||
private static string SecondaryBackendName = "none";
|
||||
private static ulong PrimaryContextHandle;
|
||||
private static readonly object HostSubmitGate = new();
|
||||
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "g2tViFIohHE",
|
||||
|
|
@ -252,6 +269,16 @@ public static class AudioOut2Exports
|
|||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v8iOE+j8a5o",
|
||||
ExportName = "sceAudioOut2MasteringSetParam",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2MasteringSetParam(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
// 3D-audio object latency hint; the host mixer has no object pipeline to
|
||||
// tune, but failure here makes Yotei tear down its whole ACM context and
|
||||
// abort audio arena bring-up.
|
||||
|
|
@ -305,8 +332,8 @@ public static class AudioOut2Exports
|
|||
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
|
||||
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
var memorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memorySizeAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -324,13 +351,12 @@ public static class AudioOut2Exports
|
|||
contextMemorySize = checked(0x10000UL + (queueDepth * 0x590UL));
|
||||
}
|
||||
|
||||
// The output is a single size_t wherever it lives. Titles keep other locals right after it
|
||||
// (Octopath Traveler II divides by the dword at +12), so nothing else may be written.
|
||||
Span<byte> memorySize = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
|
||||
$"[LOADER][TRACE] audio_out2.context-query-memory " +
|
||||
$"out=0x{memorySizeAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -391,8 +417,20 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||
{
|
||||
// Shared backend lifetime is process-wide; just drop the context entry.
|
||||
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
var contextHandle = ctx[CpuRegister.Rdi];
|
||||
Contexts.TryRemove(contextHandle, out _);
|
||||
foreach (var (portHandle, port) in Ports)
|
||||
{
|
||||
if (port.ContextHandle != contextHandle ||
|
||||
!Ports.TryRemove(portHandle, out var removedPort))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref removedPort.PcmData, null);
|
||||
removedPort.PcmAddress = 0;
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -401,7 +439,38 @@ public static class AudioOut2Exports
|
|||
ExportName = "sceAudioOut2ContextBedWrite",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _bedWriteTraceCount);
|
||||
if (n <= 8 || n % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"context-bed-write#{n} context=0x{ctx[CpuRegister.Rdi]:X} " +
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} " +
|
||||
$"rcx=0x{ctx[CpuRegister.Rcx]:X} r8=0x{ctx[CpuRegister.R8]:X} " +
|
||||
$"r9=0x{ctx[CpuRegister.R9]:X}");
|
||||
|
||||
Span<byte> probe = stackalloc byte[0x40];
|
||||
foreach (var (name, address) in new[]
|
||||
{
|
||||
("rsi", ctx[CpuRegister.Rsi]),
|
||||
("rdx", ctx[CpuRegister.Rdx]),
|
||||
("rcx", ctx[CpuRegister.Rcx]),
|
||||
("r8", ctx[CpuRegister.R8]),
|
||||
("r9", ctx[CpuRegister.R9]),
|
||||
})
|
||||
{
|
||||
if (address != 0 && ctx.Memory.TryRead(address, probe))
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"context-bed-write-probe#{n} {name}=0x{address:X} " +
|
||||
$"bytes={Convert.ToHexString(probe)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aII9h5nli9U",
|
||||
|
|
@ -494,11 +563,6 @@ public static class AudioOut2Exports
|
|||
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
|
||||
var outLevelAddress = ctx[CpuRegister.Rsi];
|
||||
var outAvailableAddress = ctx[CpuRegister.Rdx];
|
||||
if (outLevelAddress == 0)
|
||||
{
|
||||
outLevelAddress = outAvailableAddress;
|
||||
outAvailableAddress = 0;
|
||||
}
|
||||
|
||||
// Titles size their rendering by these: Wwise renders exactly puiAvailableQueues grains per
|
||||
// update and pushes them non-blocking, so a queue always reported empty made it render a
|
||||
|
|
@ -550,6 +614,41 @@ public static class AudioOut2Exports
|
|||
var portHandle = ctx[CpuRegister.Rdi];
|
||||
var attributesAddress = ctx[CpuRegister.Rsi];
|
||||
var attributeCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||
var attributeStride = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||
var traceCall = Interlocked.Increment(ref _attributeCallTraceCount);
|
||||
if (traceCall <= 8 && attributesAddress != 0)
|
||||
{
|
||||
Span<byte> descriptorProbe = stackalloc byte[0x40];
|
||||
if (ctx.Memory.TryRead(attributesAddress, descriptorProbe))
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-set-attributes-raw#{traceCall} port=0x{portHandle:X} " +
|
||||
$"count={attributeCount} stride={attributeStride} address=0x{attributesAddress:X} " +
|
||||
$"bytes={Convert.ToHexString(descriptorProbe)}");
|
||||
|
||||
Span<byte> targetProbe = stackalloc byte[0x20];
|
||||
for (var offset = 0; offset <= descriptorProbe.Length - sizeof(ulong); offset += sizeof(ulong))
|
||||
{
|
||||
var candidate = BinaryPrimitives.ReadUInt64LittleEndian(descriptorProbe[offset..]);
|
||||
if (!IsWritableOutBuffer(candidate) ||
|
||||
!ctx.Memory.TryRead(candidate, targetProbe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TraceAudioOut2(
|
||||
$"port-set-attributes-target#{traceCall} offset=0x{offset:X} " +
|
||||
$"address=0x{candidate:X} bytes={Convert.ToHexString(targetProbe)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-set-attributes-raw#{traceCall} read-failed address=0x{attributesAddress:X} " +
|
||||
$"count={attributeCount} stride={attributeStride}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Ports.TryGetValue(portHandle, out var port))
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
|
|
@ -622,14 +721,38 @@ public static class AudioOut2Exports
|
|||
continue;
|
||||
}
|
||||
|
||||
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
|
||||
var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||
byte[]? pcmData = null;
|
||||
if (pcmAddress != 0 &&
|
||||
TryDecodeDataFormat(port.DataFormat, out var channels, out var bytesPerSample, out _) &&
|
||||
IsSnapshotPcmPort(port.PortType, channels))
|
||||
{
|
||||
int byteLength;
|
||||
try
|
||||
{
|
||||
byteLength = checked((int)port.GrainSamples * channels * bytesPerSample);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
pcmData = new byte[byteLength];
|
||||
if (!ctx.Memory.TryRead(pcmAddress, pcmData))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
port.PcmAddress = pcmAddress;
|
||||
Volatile.Write(ref port.PcmData, pcmData);
|
||||
var n = Interlocked.Increment(ref _attributePcmTraceCount);
|
||||
if (n <= 8 || n % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-set-pcm#{n} port=0x{portHandle:X} pcm=0x{port.PcmAddress:X} " +
|
||||
$"format=0x{port.DataFormat:X} grains={port.GrainSamples}");
|
||||
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
|
||||
$"grains={port.GrainSamples}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -645,10 +768,10 @@ public static class AudioOut2Exports
|
|||
{
|
||||
// ABI: sceAudioOut2PortCreate(SceAudioOut2ContextHandle hCtx,
|
||||
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
|
||||
// rdx is the only out pointer.
|
||||
var contextHandle = ctx[CpuRegister.Rdi];
|
||||
var paramAddress = ctx[CpuRegister.Rsi];
|
||||
var outPortAddress = ctx[CpuRegister.Rdx];
|
||||
var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
|
||||
if (outPortAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
|
|
@ -669,6 +792,14 @@ public static class AudioOut2Exports
|
|||
Span<byte> param = stackalloc byte[PortParamSize];
|
||||
if (ctx.Memory.TryRead(paramAddress, param))
|
||||
{
|
||||
if (traceCreate <= 8 || traceCreate % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-create-param#{traceCreate} context=0x{contextHandle:X} " +
|
||||
$"address=0x{paramAddress:X} out=0x{outPortAddress:X} " +
|
||||
$"bytes={Convert.ToHexString(param)}");
|
||||
}
|
||||
|
||||
portType = BinaryPrimitives.ReadUInt16LittleEndian(param);
|
||||
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
||||
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
||||
|
|
@ -719,13 +850,16 @@ public static class AudioOut2Exports
|
|||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
||||
if (IsGuestStackAddress(stateAddress) &&
|
||||
!(AllowStackOut("portstate") && Ports.ContainsKey(portHandle)))
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-get-state skip-stack handle=0x{portHandle:X} state=0x{stateAddress:X}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
Span<byte> state = stackalloc byte[PortStateSize];
|
||||
state.Clear();
|
||||
// +0x00 u16 output = OUTPUT_PRIMARY (1)
|
||||
// +0x02 u8 numChannels = from port format when known, else 2
|
||||
// +0x04 s16 volume = -1 (not applicable outside PADSPK)
|
||||
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
|
||||
// +0x08 u32 flags = 0 (no 3D, not mono-forced)
|
||||
byte channels = 2;
|
||||
if (Ports.TryGetValue(portHandle, out var port) &&
|
||||
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
|
||||
|
|
@ -735,15 +869,21 @@ public static class AudioOut2Exports
|
|||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
||||
state[0x02] = channels;
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
|
||||
|
||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
||||
TraceAudioOut2(
|
||||
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
|
||||
var traceState = Interlocked.Increment(ref _portStateTraceCount);
|
||||
if (traceState <= 8 || traceState % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-get-state#{traceState} handle=0x{portHandle:X} state=0x{stateAddress:X} " +
|
||||
$"bytes={Convert.ToHexString(state)}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -972,7 +1112,17 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx)
|
||||
{
|
||||
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
var portHandle = ctx[CpuRegister.Rdi];
|
||||
var removed = Ports.TryRemove(portHandle, out var port);
|
||||
var n = Interlocked.Increment(ref _portDestroyTraceCount);
|
||||
if (n <= 8 || n % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-destroy#{n} handle=0x{portHandle:X} removed={removed} " +
|
||||
$"context=0x{port?.ContextHandle ?? 0:X} pcm=0x{port?.PcmAddress ?? 0:X} " +
|
||||
$"remaining={Ports.Count}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -1064,13 +1214,20 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
// Deeper host queue than classic AudioOut: FMOD's bursty
|
||||
// AudioOut2 Push pattern underran a 32 KiB (~171 ms) bed.
|
||||
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
PrimaryBackendName = audio.BackendName + "-primary";
|
||||
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||
if (streamFactory is not null)
|
||||
{
|
||||
PrimaryBackend = streamFactory(context.Frequency);
|
||||
PrimaryBackendName = PrimaryBackend is null ? "silent" : "test-primary";
|
||||
}
|
||||
else
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
PrimaryBackendName = audio.BackendName + "-primary";
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1088,11 +1245,20 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||
if (streamFactory is not null)
|
||||
{
|
||||
SecondaryBackend = streamFactory(context.Frequency);
|
||||
SecondaryBackendName = SecondaryBackend is null ? "silent" : "test-secondary";
|
||||
}
|
||||
else
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1155,7 +1321,6 @@ public static class AudioOut2Exports
|
|||
lock (HostSubmitGate)
|
||||
{
|
||||
var mix = ArrayPool<float>.Shared.Rent(frames * 2);
|
||||
var source = ArrayPool<byte>.Shared.Rent(frames * 16 * sizeof(float));
|
||||
var output = ArrayPool<byte>.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize);
|
||||
try
|
||||
{
|
||||
|
|
@ -1164,29 +1329,42 @@ public static class AudioOut2Exports
|
|||
foreach (var port in Ports.Values)
|
||||
{
|
||||
if (port.ContextHandle != context.Handle ||
|
||||
port.PcmAddress == 0 ||
|
||||
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
|
||||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat) ||
|
||||
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
|
||||
out var leftGain, out var rightGain))
|
||||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pcmData = TakePortPcmSnapshot(port);
|
||||
if (pcmData is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetStereoGains(
|
||||
port.PortType,
|
||||
ch,
|
||||
port.Gain,
|
||||
port.Ambisonics,
|
||||
port.Passthrough,
|
||||
out var leftGain,
|
||||
out var rightGain))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var byteLength = checked(frames * ch * bps);
|
||||
if (byteLength <= 0 || byteLength > source.Length)
|
||||
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceSpan = source.AsSpan(0, byteLength);
|
||||
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
|
||||
if (pcmData.Length < byteLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MixPortIntoStereo(
|
||||
sourceSpan,
|
||||
pcmData.AsSpan(0, byteLength),
|
||||
mix.AsSpan(0, frames * 2),
|
||||
frames,
|
||||
ch,
|
||||
|
|
@ -1238,12 +1416,70 @@ public static class AudioOut2Exports
|
|||
finally
|
||||
{
|
||||
ArrayPool<float>.Shared.Return(mix);
|
||||
ArrayPool<byte>.Shared.Return(source);
|
||||
ArrayPool<byte>.Shared.Return(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[]? GetPortPcmSnapshotForTests(ulong portHandle)
|
||||
{
|
||||
if (!Ports.TryGetValue(portHandle, out var port))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var snapshot = Volatile.Read(ref port.PcmData);
|
||||
return snapshot?.ToArray();
|
||||
}
|
||||
|
||||
internal static bool HasPortForTests(ulong portHandle) => Ports.ContainsKey(portHandle);
|
||||
|
||||
internal static byte[]? TakePortPcmSnapshotForTests(ulong portHandle)
|
||||
{
|
||||
return Ports.TryGetValue(portHandle, out var port)
|
||||
? TakePortPcmSnapshot(port)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static byte[]? TakePortPcmSnapshot(PortState port) =>
|
||||
Interlocked.Exchange(ref port.PcmData, null);
|
||||
|
||||
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
|
||||
Volatile.Write(ref _streamFactoryForTests, streamFactory);
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Ports.Clear();
|
||||
SpeakerArrays.Clear();
|
||||
Interlocked.Exchange(ref _nextContextHandle, 1);
|
||||
Interlocked.Exchange(ref _nextUserHandle, 1);
|
||||
Interlocked.Exchange(ref _nextPortId, 0);
|
||||
Interlocked.Exchange(ref _pushTraceCount, 0);
|
||||
Interlocked.Exchange(ref _submitTraceCount, 0);
|
||||
Interlocked.Exchange(ref _submitSkipTraceCount, 0);
|
||||
Interlocked.Exchange(ref _attributePcmTraceCount, 0);
|
||||
|
||||
lock (HostBackendGate)
|
||||
{
|
||||
PrimaryBackend?.Dispose();
|
||||
SecondaryBackend?.Dispose();
|
||||
PrimaryBackend = null;
|
||||
SecondaryBackend = null;
|
||||
PrimaryBackendName = "none";
|
||||
SecondaryBackendName = "none";
|
||||
PrimaryContextHandle = 0;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _streamFactoryForTests, null);
|
||||
}
|
||||
|
||||
private static bool IsMainOrBgmPort(ushort portType)
|
||||
{
|
||||
var kind = portType & 0xFF;
|
||||
return kind is 0 or 1;
|
||||
}
|
||||
|
||||
internal static bool RoutesToSpeakers(ushort portType) => (portType & 0xFF) is 0 or 1 or 2;
|
||||
|
||||
internal static (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) =>
|
||||
|
|
@ -1298,6 +1534,11 @@ public static class AudioOut2Exports
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSnapshotPcmPort(ushort portType, int channels) =>
|
||||
RoutesToSpeakers(portType) &&
|
||||
(portType & 0xFF) != AudioOut2PortTypeVibration &&
|
||||
(!IsObjectPort(portType) || channels == 1);
|
||||
|
||||
internal static void MixPortIntoStereo(
|
||||
ReadOnlySpan<byte> source,
|
||||
Span<float> mix,
|
||||
|
|
@ -1453,13 +1694,19 @@ public static class AudioOut2Exports
|
|||
private static bool IsDirectMemoryWindowAddress(ulong value) =>
|
||||
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
|
||||
|
||||
// Only sceAudioOut2ContextQueryMemory and sceAudioOut2ContextGetQueueLevel still
|
||||
// pick an out buffer by host address range; both are left untouched here so they do
|
||||
// not collide with the changes already in flight for them, and these two predicates
|
||||
// go away with the last of their callers.
|
||||
private static bool IsGuestStackAddress(ulong value) =>
|
||||
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
|
||||
|
||||
private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary)
|
||||
{
|
||||
if (IsWritableOutBuffer(primary))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
return IsWritableOutBuffer(secondary) ? secondary : 0;
|
||||
}
|
||||
|
||||
private static bool IsWritableOutBuffer(ulong value) =>
|
||||
value != 0 &&
|
||||
value != 0x10000UL &&
|
||||
|
|
|
|||
|
|
@ -881,6 +881,32 @@ public static class AvPlayerExports
|
|||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "N6Oy-EjduiY",
|
||||
ExportName = "sceAvPlayerSetAvailableBandwidth",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
[SysAbiExport(
|
||||
Nid = "+7xJ+lFxmkQ",
|
||||
ExportName = "sceAvPlayerSetAvailableBW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerSetAvailableBandwidth(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var availableBandwidth = ctx[CpuRegister.Rsi];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Players.ContainsKey(handle))
|
||||
{
|
||||
return SetReturn(ctx, InvalidParameters);
|
||||
}
|
||||
|
||||
Trace($"set_available_bandwidth handle=0x{handle:X16} bandwidth={availableBandwidth}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ODJK2sn9w4A",
|
||||
ExportName = "sceAvPlayerEnableStream",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,21 @@ using SharpEmu.Libs.VideoOut;
|
|||
|
||||
namespace SharpEmu.Libs.Codec;
|
||||
|
||||
internal sealed record Videodec2DecodedFrame(
|
||||
byte[] Nv12,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Pitch,
|
||||
bool ErrorFrame)
|
||||
{
|
||||
public ulong Pts { get; init; } = ulong.MaxValue;
|
||||
public ulong Dts { get; init; } = ulong.MaxValue;
|
||||
public ulong AttachedData { get; init; }
|
||||
public bool KeyFrame { get; init; }
|
||||
public uint Profile { get; init; }
|
||||
public uint Level { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder
|
||||
/// handle, feeding it pre-demuxed Annex-B access units from guest memory.
|
||||
|
|
@ -29,6 +44,8 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
// Enough lookahead to absorb decode jitter without adding visible latency.
|
||||
private const int FrameQueueCapacity = 4;
|
||||
|
||||
private const int WorkQueueCapacity = 64;
|
||||
|
||||
// Fallback when the stream doesn't declare a usable framerate.
|
||||
private const double FallbackFps = 30.0;
|
||||
|
||||
|
|
@ -45,12 +62,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
private bool _disposed;
|
||||
|
||||
// Unbounded: access units are small, backpressure lives on the frame queue below.
|
||||
private readonly Channel<byte[]?> _workChannel =
|
||||
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
|
||||
Channel.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
||||
|
|
@ -74,6 +91,10 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private uint _lastWidth;
|
||||
private uint _lastHeight;
|
||||
|
||||
private long _workerInputCount;
|
||||
private long _workerFrameCount;
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
|
||||
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
|
||||
{
|
||||
_codecContext = codecContext;
|
||||
|
|
@ -106,18 +127,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
|
||||
if (codec == null)
|
||||
{
|
||||
Trace("create failed: H.264 decoder not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext = ffmpeg.avcodec_alloc_context3(codec);
|
||||
if (codecContext == null)
|
||||
{
|
||||
Trace("create failed: avcodec_alloc_context3 returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE;
|
||||
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: avcodec_open2 rejected H.264 decoder");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +161,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: frame or packet allocation returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Videodec2Decoder(codecContext, frame, packet);
|
||||
var decoder = new Videodec2Decoder(codecContext, frame, packet);
|
||||
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
|
||||
return decoder;
|
||||
}
|
||||
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
|
||||
{
|
||||
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
|
||||
Trace($"create failed: {ex.GetType().Name}: {ex.Message}");
|
||||
if (codecContext != null)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
|
|
@ -174,18 +203,252 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
public bool EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
return _workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public void RequestDrain()
|
||||
public bool RequestDrain()
|
||||
{
|
||||
_workChannel.Writer.TryWrite(null);
|
||||
return _workChannel.Writer.TryWrite(null);
|
||||
}
|
||||
|
||||
public bool DecodeAccessUnit(
|
||||
byte[] accessUnit,
|
||||
ulong pts,
|
||||
ulong dts,
|
||||
ulong attachedData,
|
||||
out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed || accessUnit.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length);
|
||||
if (packetResult < 0)
|
||||
{
|
||||
Trace($"decode failed: av_new_packet={packetResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* source = accessUnit)
|
||||
{
|
||||
Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length);
|
||||
}
|
||||
|
||||
_packet->pts = ToAvTimestamp(pts);
|
||||
_packet->dts = ToAvTimestamp(dts);
|
||||
|
||||
var havePendingFrame = false;
|
||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (pendingResult < 0)
|
||||
{
|
||||
Trace($"decode failed: receive-before-resend={pendingResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
havePendingFrame = true;
|
||||
sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
}
|
||||
|
||||
if (sendResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_send_packet={sendResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!havePendingFrame)
|
||||
{
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData);
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"flush failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
decodedFrame = ConvertFrameToNv12Locked();
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ffmpeg.avcodec_flush_buffers(_codecContext);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
|
||||
private Videodec2DecodedFrame? ConvertFrameToNv12Locked(
|
||||
ulong fallbackPts = ulong.MaxValue,
|
||||
ulong fallbackDts = ulong.MaxValue,
|
||||
ulong attachedData = 0)
|
||||
{
|
||||
if (_frame->width <= 0 || _frame->height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var width = checked((uint)_frame->width);
|
||||
var height = checked((uint)_frame->height);
|
||||
uint pitch;
|
||||
ulong lumaBytes;
|
||||
ulong requiredBytes;
|
||||
try
|
||||
{
|
||||
pitch = checked((width + 255u) & ~255u);
|
||||
lumaBytes = checked((ulong)pitch * height);
|
||||
var chromaRows = ((ulong)height + 1UL) / 2UL;
|
||||
requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows));
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requiredBytes > int.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_swsContext = ffmpeg.sws_getCachedContext(
|
||||
_swsContext,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
(AVPixelFormat)_frame->format,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
AVPixelFormat.AV_PIX_FMT_NV12,
|
||||
ffmpeg.SWS_FAST_BILINEAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
if (_swsContext == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nv12 = new byte[(int)requiredBytes];
|
||||
fixed (byte* destination = nv12)
|
||||
{
|
||||
var destinationPlanes = new byte*[4]
|
||||
{
|
||||
destination,
|
||||
destination + checked((int)lumaBytes),
|
||||
null,
|
||||
null,
|
||||
};
|
||||
var destinationStrides = new int[4]
|
||||
{
|
||||
checked((int)pitch),
|
||||
checked((int)pitch),
|
||||
0,
|
||||
0,
|
||||
};
|
||||
var rows = ffmpeg.sws_scale(
|
||||
_swsContext,
|
||||
_frame->data,
|
||||
_frame->linesize,
|
||||
0,
|
||||
_frame->height,
|
||||
destinationPlanes,
|
||||
destinationStrides);
|
||||
if (rows != _frame->height)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Videodec2DecodedFrame(
|
||||
nv12,
|
||||
width,
|
||||
height,
|
||||
pitch,
|
||||
(_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0)
|
||||
{
|
||||
Pts = FromAvTimestamp(_frame->pts, fallbackPts),
|
||||
Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts),
|
||||
AttachedData = attachedData,
|
||||
KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0,
|
||||
Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0,
|
||||
Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static long ToAvTimestamp(ulong timestamp) =>
|
||||
timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE;
|
||||
|
||||
private static ulong FromAvTimestamp(long timestamp, ulong fallback) =>
|
||||
timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp;
|
||||
|
||||
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
|
||||
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
|
||||
{
|
||||
|
|
@ -233,15 +496,28 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var inputOrdinal = Interlocked.Increment(ref _workerInputCount);
|
||||
var decodedOk = item is null
|
||||
? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height)
|
||||
: DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height);
|
||||
if (ShouldTrace(inputOrdinal))
|
||||
{
|
||||
Trace(
|
||||
$"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " +
|
||||
$"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}");
|
||||
}
|
||||
|
||||
if (!decodedOk || !hasPicture || bgraFrame is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var frameOrdinal = Interlocked.Increment(ref _workerFrameCount);
|
||||
if (ShouldTrace(frameOrdinal))
|
||||
{
|
||||
Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
|
||||
|
|
@ -564,4 +840,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
|
|
@ -15,10 +16,18 @@ public static class Videodec2Exports
|
|||
{
|
||||
private const int Ok = 0;
|
||||
|
||||
private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024;
|
||||
private const uint FrameBufferAlignment = 0x100;
|
||||
|
||||
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> PictureInfos = new();
|
||||
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
|
||||
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
private static long _decodeCallCount;
|
||||
private static long _decodedPictureCount;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RnDibcGCPKw",
|
||||
ExportName = "sceVideodec2QueryComputeMemoryInfo",
|
||||
|
|
@ -27,24 +36,25 @@ public static class Videodec2Exports
|
|||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
if (paramAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
// Success needs no memory writes; the game initializes from its own fields.
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||
private const int VideodecErrorStructSize = unchecked((int)0x811D0101);
|
||||
private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F);
|
||||
private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202);
|
||||
|
||||
// Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`.
|
||||
private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024;
|
||||
private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024;
|
||||
|
||||
// Opaque token the game hands back unmodified to later Videodec2 calls.
|
||||
private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eD+X2SmxUt4",
|
||||
ExportName = "sceVideodec2AllocateComputeQueue",
|
||||
|
|
@ -52,8 +62,14 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
||||
{
|
||||
var queueAddress = ctx[CpuRegister.Rdi];
|
||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var queueAddress = ctx[CpuRegister.Rdx];
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 ||
|
||||
!ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) ||
|
||||
computeMemoryAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(
|
||||
queueAddress, computeMemoryAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -61,7 +77,18 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
||||
[SysAbiExport(
|
||||
Nid = "UvtA3FAiF4Y",
|
||||
ExportName = "sceVideodec2ReleaseComputeQueue",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2ReleaseComputeQueue(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "qqMCwlULR+E",
|
||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
||||
|
|
@ -69,12 +96,18 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) ||
|
||||
// Frame-slot size: must be nonzero or the game divides its arena by zero.
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000))
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -99,7 +132,9 @@ public static class Videodec2Exports
|
|||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
||||
var decoder = Videodec2Decoder.TryCreate();
|
||||
Decoders[handle] = decoder;
|
||||
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
|
||||
|
||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
||||
{
|
||||
|
|
@ -111,7 +146,6 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
|
||||
[SysAbiExport(
|
||||
Nid = "l1hXwscLuCY",
|
||||
ExportName = "sceVideodec2Flush",
|
||||
|
|
@ -120,33 +154,25 @@ public static class Videodec2Exports
|
|||
public static int Videodec2Flush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rsi];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
if (Decoders.TryGetValue(handle, out var decoder) &&
|
||||
decoder is not null &&
|
||||
decoder.FlushOutput(out var decodedFrame) &&
|
||||
decodedFrame is not null)
|
||||
{
|
||||
// Drain in order: report an already-finished frame before queuing a new drain request.
|
||||
if (decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) &&
|
||||
ctx.TryWriteUInt64(outputInfoAddress + 0x10, height))
|
||||
{
|
||||
_ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
decoder.RequestDrain();
|
||||
}
|
||||
_ = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// No state to reset.
|
||||
[SysAbiExport(
|
||||
Nid = "wJXikG6QFN8",
|
||||
ExportName = "sceVideodec2Reset",
|
||||
|
|
@ -154,6 +180,12 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2Reset(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
decoder.Reset();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +205,6 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// rcx[0] is the picture-ready flag (1 = frame published); it lives in
|
||||
// uninitialized stack and must always be written explicitly.
|
||||
[SysAbiExport(
|
||||
Nid = "852F5+q6+iM",
|
||||
ExportName = "sceVideodec2Decode",
|
||||
|
|
@ -186,8 +216,9 @@ public static class Videodec2Exports
|
|||
var inputAuStruct = ctx[CpuRegister.Rsi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rdx];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rcx];
|
||||
var callOrdinal = Interlocked.Increment(ref _decodeCallCount);
|
||||
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -195,6 +226,7 @@ public static class Videodec2Exports
|
|||
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
|
||||
{
|
||||
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
|
||||
TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -208,35 +240,235 @@ public static class Videodec2Exports
|
|||
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
|
||||
{
|
||||
// Nothing sane to feed/fill this call; not an error.
|
||||
TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData);
|
||||
|
||||
var auBuffer = new byte[auDataSize];
|
||||
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
|
||||
{
|
||||
TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
||||
decoder.EnqueueAccessUnit(auBuffer);
|
||||
var decodeOk = decoder.DecodeAccessUnit(
|
||||
auBuffer, pts, dts, attachedData, out var decodedFrame);
|
||||
if (ShouldTrace(callOrdinal))
|
||||
{
|
||||
var prefixBytes = Math.Min(auBuffer.Length, 8);
|
||||
Trace(
|
||||
$"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " +
|
||||
$"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " +
|
||||
$"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " +
|
||||
$"picture={decodedFrame is not null}");
|
||||
}
|
||||
|
||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
if (!decodeOk || decodedFrame is null)
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
||||
var copied = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
if (copied)
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
Interlocked.Increment(ref _decodedPictureCount);
|
||||
}
|
||||
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NoPicture = [0];
|
||||
private static readonly byte[] PictureReady = [1];
|
||||
[SysAbiExport(
|
||||
Nid = "NtXRa3dRzU0",
|
||||
ExportName = "sceVideodec2GetPictureInfo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2GetPictureInfo(CpuContext ctx)
|
||||
{
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdi];
|
||||
var firstPictureInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var secondPictureInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || firstPictureInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) ||
|
||||
(outputInfoSize != 0x30 && outputInfoSize != 0x38))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) ||
|
||||
!TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) ||
|
||||
!ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) ||
|
||||
isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 ||
|
||||
!PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorOutputInfo);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) ||
|
||||
(requestedSize != 104 && requestedSize != 120))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
var picture = new byte[(int)requestedSize];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize);
|
||||
picture[0x08] = 1;
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData);
|
||||
picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0;
|
||||
picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue);
|
||||
picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u);
|
||||
picture[0x34] = 1;
|
||||
picture[0x4E] = 1;
|
||||
picture[0x4F] = 5;
|
||||
if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (secondPictureInfoAddress != 0 &&
|
||||
!TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"picture_info buffer=0x{pixelsAddress:X16} " +
|
||||
$"pts=0x{decodedFrame.Pts:X16} size={requestedSize}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NotAccepted = [0];
|
||||
private static readonly byte[] Accepted = [1];
|
||||
private static readonly byte[] NoPicture = [0, 0, 0, 0];
|
||||
private static readonly byte[] PictureReady = [1, 0, 1, 0];
|
||||
private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0];
|
||||
|
||||
private static bool TryInitializeNoPicture(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress)
|
||||
{
|
||||
return frameBufferAddress != 0 &&
|
||||
outputInfoAddress != 0 &&
|
||||
ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) &&
|
||||
ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture);
|
||||
}
|
||||
|
||||
internal static bool TryWriteDecodedFrame(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress,
|
||||
Videodec2DecodedFrame decodedFrame)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) ||
|
||||
!ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) ||
|
||||
pixelsAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ulong)decodedFrame.Nv12.Length > slotBytes ||
|
||||
!ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputInfoBytes >= 0x38 &&
|
||||
(!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) ||
|
||||
!ctx.Memory.TryWrite(
|
||||
outputInfoAddress + 0x08,
|
||||
decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PictureInfos[pixelsAddress] = decodedFrame;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var output = new byte[(int)size];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(output, size);
|
||||
return ctx.Memory.TryWrite(address, output);
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void TraceDecode(long ordinal, string message)
|
||||
{
|
||||
if (ShouldTrace(ordinal))
|
||||
{
|
||||
Trace($"decode call={ordinal} {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Headless lifecycle for the system web-browser dialog. SharpEmu does not yet
|
||||
/// provide a host browser, so an opened dialog completes on its first status
|
||||
/// poll instead of leaving games in an infinite RUNNING loop.
|
||||
/// </summary>
|
||||
public static class WebBrowserDialogExports
|
||||
{
|
||||
internal const int StatusNone = 0;
|
||||
internal const int StatusInitialized = 1;
|
||||
internal const int StatusRunning = 2;
|
||||
internal const int StatusFinished = 3;
|
||||
|
||||
private const int ErrorOk = 0;
|
||||
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jqb7HntFQFc",
|
||||
ExportName = "sceWebBrowserDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FraP7debcdg",
|
||||
ExportName = "sceWebBrowserDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogOpen(CpuContext ctx) => Open(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "O7dIZQrwVFY",
|
||||
ExportName = "sceWebBrowserDialogOpenForPredeterminedContent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogOpenForPredeterminedContent(CpuContext ctx) => Open(ctx);
|
||||
|
||||
private static int Open(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _status, StatusRunning);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CFTG6a8TjOU",
|
||||
ExportName = "sceWebBrowserDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "h1dR-t5ISgg",
|
||||
ExportName = "sceWebBrowserDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
private static int PollStatus()
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||
return Volatile.Read(ref _status);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PSK+Eik919Q",
|
||||
ExportName = "sceWebBrowserDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogClose(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotRunning);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vCaW0fgVQmc",
|
||||
ExportName = "sceWebBrowserDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||
result.Clear();
|
||||
if (!ctx.Memory.TryWrite(ctx[CpuRegister.Rdi], result))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wit4LjeoeX4",
|
||||
ExportName = "sceWebBrowserDialogGetEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetEvent(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uYELOMVnmNQ",
|
||||
ExportName = "sceWebBrowserDialogNavigate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogNavigate(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RLhKBOoNyXY",
|
||||
ExportName = "sceWebBrowserDialogSetZoom",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogSetZoom(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ocHtyBwHfys",
|
||||
ExportName = "sceWebBrowserDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
internal static int StatusForTests => Volatile.Read(ref _status);
|
||||
|
||||
internal static void ResetForTests() => Interlocked.Exchange(ref _status, StatusNone);
|
||||
}
|
||||
|
|
@ -896,45 +896,24 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
$"unsupported buffer readback from an asynchronous GPU completion, addr=0x{guestAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
if (!_relay.IsGpuQueueThread && GuestReadsAwaitOffQueue && (AsyncReadback is not null || MainQueuePendingReads))
|
||||
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
|
||||
{
|
||||
// A GPU write that lands after the copy was recorded makes the read stale. Retrying
|
||||
// synchronously stalls the command worker until the GPU drains, so re-issue the read and
|
||||
// wait here again; only the last attempt falls back to the synchronous read.
|
||||
for (var attempt = 1; ; attempt++)
|
||||
PendingDownload? pending = null;
|
||||
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (!_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (pending is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pending.Ticket is { } ticket)
|
||||
{
|
||||
(AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.")).Wait(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
_scheduler.WaitForSubmittedTick(pending.MainQueueTick);
|
||||
}
|
||||
|
||||
var finalAttempt = attempt >= PendingReadAttempts;
|
||||
var applied = false;
|
||||
if (!_relay.TryRunOnGpuQueue(() => applied = CompleteReadMemoryOnGpu(pending, retrySynchronously: finalAttempt)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (applied || finalAttempt)
|
||||
readback.Wait(pending.Ticket);
|
||||
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: true)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
var onQueue = _relay.IsGpuQueueThread;
|
||||
|
|
@ -1165,14 +1144,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
|
||||
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)}");
|
||||
|
||||
// Ticket is set when the readback queue carries the download; otherwise the main queue
|
||||
// does, into MainQueueBuffer, and MainQueueTick is the submission the guest thread waits for.
|
||||
private sealed record PendingDownload
|
||||
{
|
||||
public VulkanAsyncReadback.Ticket? Ticket { get; init; }
|
||||
public ulong MainQueueTick { get; init; }
|
||||
public GpuBuffer? MainQueueBuffer { get; init; }
|
||||
public ulong[] MainQueueOffsets { get; init; } = [];
|
||||
public required VulkanAsyncReadback.Ticket Ticket { get; init; }
|
||||
public required List<DownloadPiece> Copies { get; init; }
|
||||
public required ulong[] WriteTicks { get; init; }
|
||||
public required ulong WindowBegin { get; init; }
|
||||
|
|
@ -1227,31 +1201,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending &&
|
||||
TryBeginDownloadOnMainQueue(copies, out var mainTick, out var mainBuffer, out var mainOffsets, out var mainWriteTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
copy.Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
MainQueueTick = mainTick,
|
||||
MainQueueBuffer = mainBuffer,
|
||||
MainQueueOffsets = mainOffsets,
|
||||
Copies = copies,
|
||||
WriteTicks = mainWriteTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0)
|
||||
{
|
||||
DownloadBufferMemory(copies);
|
||||
|
|
@ -1267,70 +1216,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
// Without a readback queue the main queue copies the pieces into a buffer of their own and
|
||||
// submits at once; the guest thread then waits for that submission instead of the worker
|
||||
// waiting for the whole GPU (a guest read of GPU-written memory otherwise idled the worker
|
||||
// for tens of milliseconds, several times per frame in Silent Hill: The Short Message).
|
||||
private bool TryBeginDownloadOnMainQueue(List<DownloadPiece> copies, out ulong tick, out GpuBuffer buffer, out ulong[] offsets, out ulong[] writeTicks)
|
||||
{
|
||||
tick = 0;
|
||||
buffer = null!;
|
||||
offsets = [];
|
||||
writeTicks = [];
|
||||
if (!MainQueuePendingReads || copies.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ticks = new ulong[copies.Count];
|
||||
var placements = new ulong[copies.Count];
|
||||
var total = 0UL;
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var written = copies[index].Buffer.LastGpuWriteTick;
|
||||
if (written == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ticks[index] = written;
|
||||
placements[index] = total;
|
||||
total += (copies[index].Size + BufferDownloadBatchPlanner.Alignment - 1) & ~(BufferDownloadBatchPlanner.Alignment - 1);
|
||||
}
|
||||
|
||||
if (total > AsyncReadbackLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, BufferUsageFlags.TransferDstBit, total);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
buffer.CopyFrom(
|
||||
_scheduler.Current, copies[index].Buffer, copies[index].SourceOffset, placements[index], copies[index].Size,
|
||||
AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
|
||||
}
|
||||
|
||||
tick = _scheduler.Flush();
|
||||
offsets = placements;
|
||||
writeTicks = ticks;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool MainQueuePendingReads =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_MAIN_QUEUE_PENDING_READS") != "0";
|
||||
|
||||
// Pending reads a guest thread re-issues before it lets the worker read synchronously.
|
||||
private const int PendingReadAttempts = 4;
|
||||
|
||||
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
|
||||
if (pending.Ticket is null)
|
||||
{
|
||||
return CompleteMainQueueRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
|
||||
try
|
||||
{
|
||||
|
|
@ -1381,58 +1269,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private bool CompleteMainQueueRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
var buffer = pending.MainQueueBuffer ?? throw SubmissionScheduler.Fatal("The pending readback lost its buffer.");
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
var copies = pending.Copies;
|
||||
buffer.Invalidate(0, buffer.Size);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var bytes = buffer.Mapped.Slice((int)pending.MainQueueOffsets[index], (int)copies[index].Size);
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{copies[index].Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin);
|
||||
if (pending.IsWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
|
||||
}
|
||||
|
||||
RecordReadback(pending.WindowBegin, pending.WindowEnd, pending.IsWrite, copies, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The guest thread waited for the submission, so the GPU is done with the buffer.
|
||||
buffer.Dispose();
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPendingDownloadCurrent(PendingDownload pending)
|
||||
{
|
||||
var total = 0UL;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ public enum SliceResult
|
|||
// Submissions of every queue; one thread runs the slices, other threads enqueue and wait.
|
||||
public sealed class CommandStreamQueue
|
||||
{
|
||||
private sealed record ControlBarrier(ulong CutoffOrdinal, Action Work, Action Cancel);
|
||||
|
||||
public const int ComputeQueueCount = 56;
|
||||
public const int QueueCount = 1 + ComputeQueueCount;
|
||||
public const int AllBlockedRetryMilliseconds = 100;
|
||||
|
|
@ -37,8 +39,10 @@ public sealed class CommandStreamQueue
|
|||
private readonly object _gate = new();
|
||||
private readonly LinkedList<CommandSubmission>[] _queues = new LinkedList<CommandSubmission>[QueueCount];
|
||||
private readonly GpuCommandInterpreter?[] _interpreters = new GpuCommandInterpreter?[QueueCount];
|
||||
private readonly Queue<ControlBarrier> _controlBarriers = new();
|
||||
private int _nextQueue;
|
||||
private int _submissionCount;
|
||||
private ulong _admissionOrdinal;
|
||||
private bool _accepting = true;
|
||||
private bool _stopping;
|
||||
private bool _processing;
|
||||
|
|
@ -91,7 +95,7 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _submissionCount != 0;
|
||||
return _submissionCount != 0 || _controlBarriers.Count != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,7 +117,7 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return SelectQueueLocked() >= 0;
|
||||
return HasRunnableWorkLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +204,7 @@ public sealed class CommandStreamQueue
|
|||
throw _host.Fatal($"The command stream no longer accepts submissions: queue={submission.QueueId} address=0x{submission.Address:X16}.");
|
||||
}
|
||||
|
||||
submission.AdmissionOrdinal = ++_admissionOrdinal;
|
||||
_queues[submission.QueueId].AddLast(submission);
|
||||
_submissionCount++;
|
||||
if (submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
|
||||
|
|
@ -208,6 +213,23 @@ public sealed class CommandStreamQueue
|
|||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
public bool TryEnqueueControlBarrier(Action work, Action cancel)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(work);
|
||||
ArgumentNullException.ThrowIfNull(cancel);
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_accepting)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_controlBarriers.Enqueue(new ControlBarrier(_admissionOrdinal, work, cancel));
|
||||
Monitor.PulseAll(_gate);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public IdleOutcome Done()
|
||||
{
|
||||
if (_processingThread == Thread.CurrentThread)
|
||||
|
|
@ -257,7 +279,8 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
while (_outcome == IdleOutcome.Completed && (_processing || _submissionCount != 0))
|
||||
while (_outcome == IdleOutcome.Completed &&
|
||||
(_processing || _submissionCount != 0 || _controlBarriers.Count != 0))
|
||||
{
|
||||
Monitor.Wait(_gate);
|
||||
}
|
||||
|
|
@ -279,13 +302,13 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (SelectQueueLocked() >= 0 || _stopping)
|
||||
if (HasRunnableWorkLocked() || _stopping)
|
||||
{
|
||||
return SelectQueueLocked() >= 0;
|
||||
return HasRunnableWorkLocked();
|
||||
}
|
||||
|
||||
Monitor.Wait(_gate, timeoutMilliseconds);
|
||||
return SelectQueueLocked() >= 0;
|
||||
return HasRunnableWorkLocked();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +318,7 @@ public sealed class CommandStreamQueue
|
|||
lock (_gate)
|
||||
{
|
||||
var deadline = Environment.TickCount64 + intervalMilliseconds;
|
||||
while (SelectQueueLocked() < 0)
|
||||
while (!HasRunnableWorkLocked())
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
|
|
@ -313,10 +336,15 @@ public sealed class CommandStreamQueue
|
|||
// Publishes a failure: nothing runs any more and every waiter is released with Failed.
|
||||
public void Fail()
|
||||
{
|
||||
Action[] cancellations;
|
||||
lock (_gate)
|
||||
{
|
||||
FailLocked();
|
||||
cancellations = CancelControlBarriersLocked();
|
||||
}
|
||||
|
||||
foreach (var cancel in cancellations)
|
||||
cancel();
|
||||
}
|
||||
|
||||
private void FailLocked()
|
||||
|
|
@ -364,7 +392,8 @@ public sealed class CommandStreamQueue
|
|||
// Runs one slice of the next unblocked submission on the calling thread.
|
||||
public SliceResult ProcessOne()
|
||||
{
|
||||
CommandSubmission submission;
|
||||
CommandSubmission? submission = null;
|
||||
ControlBarrier? control = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_processing)
|
||||
|
|
@ -372,35 +401,73 @@ public sealed class CommandStreamQueue
|
|||
throw _host.Fatal("The command stream queue is already processing a slice.");
|
||||
}
|
||||
|
||||
var selected = SelectQueueLocked();
|
||||
if (selected < 0)
|
||||
if (IsControlBarrierReadyLocked())
|
||||
{
|
||||
return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork;
|
||||
control = _controlBarriers.Dequeue();
|
||||
}
|
||||
else
|
||||
{
|
||||
var selected = SelectQueueLocked();
|
||||
if (selected < 0)
|
||||
{
|
||||
return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork;
|
||||
}
|
||||
|
||||
var queue = _queues[selected];
|
||||
submission = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
_submissionCount--;
|
||||
_nextQueue = (selected + 1) % QueueCount;
|
||||
}
|
||||
|
||||
var queue = _queues[selected];
|
||||
submission = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
_submissionCount--;
|
||||
_nextQueue = (selected + 1) % QueueCount;
|
||||
_processing = true;
|
||||
_processingThread = Thread.CurrentThread;
|
||||
if (!submission.Started && submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
|
||||
if (submission is { Started: false } &&
|
||||
submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
|
||||
SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.ProcessingStarted, submission.QueueId,
|
||||
submission.SubmissionId, submission.Address, submission.DwordCount, _submissionCount);
|
||||
}
|
||||
|
||||
if (control is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
control.Work();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine($"[GPU][ERROR] Command stream control barrier failed. {exception}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
Fail();
|
||||
throw;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_processing = false;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
return SliceResult.Completed;
|
||||
}
|
||||
|
||||
bool complete;
|
||||
try
|
||||
{
|
||||
complete = RunSlice(submission);
|
||||
complete = RunSlice(submission!);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Report the cause before another submitting thread observes the failed queue.
|
||||
Console.Error.WriteLine($"[GPU][ERROR] Command stream slice failed: queue={submission.QueueId} address=0x{submission.Address:X16}. {exception}");
|
||||
Console.Error.WriteLine($"[GPU][ERROR] Command stream slice failed: queue={submission!.QueueId} address=0x{submission.Address:X16}. {exception}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
@ -416,7 +483,7 @@ public sealed class CommandStreamQueue
|
|||
SliceResult result;
|
||||
if (!complete)
|
||||
{
|
||||
submission.Blocked = true;
|
||||
submission!.Blocked = true;
|
||||
submission.BlockedSinceTicks = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
submission.BlockedPacketAddress = submission.Commands.CurrentPacketAddress;
|
||||
_queues[submission.QueueId].AddFirst(submission);
|
||||
|
|
@ -597,12 +664,17 @@ public sealed class CommandStreamQueue
|
|||
// Drops every submission without stopping admission; the presenter uses it after device loss.
|
||||
public void DiscardAll()
|
||||
{
|
||||
Action[] cancellations;
|
||||
lock (_gate)
|
||||
{
|
||||
_outcome = IdleOutcome.Failed;
|
||||
DropAllLocked();
|
||||
cancellations = CancelControlBarriersLocked();
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
foreach (var cancel in cancellations)
|
||||
cancel();
|
||||
}
|
||||
|
||||
// A diagnostic view of the blocked heads: their count, the oldest age and one sample.
|
||||
|
|
@ -669,6 +741,20 @@ public sealed class CommandStreamQueue
|
|||
|
||||
private int SelectQueueLocked()
|
||||
{
|
||||
if (_controlBarriers.TryPeek(out var control))
|
||||
{
|
||||
for (var offset = 0; offset < QueueCount; offset++)
|
||||
{
|
||||
var queueId = (_nextQueue + offset) % QueueCount;
|
||||
if (_queues[queueId].First is { } head &&
|
||||
head.Value.AdmissionOrdinal <= control.CutoffOrdinal &&
|
||||
!head.Value.Blocked)
|
||||
{
|
||||
return queueId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var offset = 0; offset < QueueCount; offset++)
|
||||
{
|
||||
var queueId = (_nextQueue + offset) % QueueCount;
|
||||
|
|
@ -681,6 +767,42 @@ public sealed class CommandStreamQueue
|
|||
return -1;
|
||||
}
|
||||
|
||||
private bool HasRunnableWorkLocked() => IsControlBarrierReadyLocked() || SelectQueueLocked() >= 0;
|
||||
|
||||
private bool IsControlBarrierReadyLocked()
|
||||
{
|
||||
if (!_controlBarriers.TryPeek(out var control))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var queue in _queues)
|
||||
{
|
||||
if (queue.First is { } head && head.Value.AdmissionOrdinal <= control.CutoffOrdinal)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Action[] CancelControlBarriersLocked()
|
||||
{
|
||||
if (_controlBarriers.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var cancellations = new Action[_controlBarriers.Count];
|
||||
for (var index = 0; index < cancellations.Length; index++)
|
||||
{
|
||||
cancellations[index] = _controlBarriers.Dequeue().Cancel;
|
||||
}
|
||||
|
||||
return cancellations;
|
||||
}
|
||||
|
||||
private void DropAllLocked()
|
||||
{
|
||||
foreach (var queue in _queues)
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ public sealed class CommandSubmission
|
|||
// The submitter's own sequence number, kept for diagnostics and the translation state.
|
||||
public ulong SubmissionId { get; }
|
||||
|
||||
internal ulong AdmissionOrdinal { get; set; }
|
||||
|
||||
// Snapshots captured at submit time; the processor never reads them.
|
||||
public object? GeometrySnapshots { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ internal interface IGuestGpuBackend
|
|||
// Marks the frame boundary; off the worker it first waits for the accepted submissions.
|
||||
IdleOutcome SubmitDone(ICpuMemory memory);
|
||||
|
||||
void RunAfterPendingCommandStreams(Action work)
|
||||
{
|
||||
work();
|
||||
}
|
||||
|
||||
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
||||
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public struct TileTransferArguments
|
|||
// Runs the tiling, detiling, depth conversion and channel swap compute passes.
|
||||
public sealed unsafe class GpuTiler : IDisposable
|
||||
{
|
||||
private const uint KindCount = 9;
|
||||
private const uint KindCount = 10;
|
||||
private const uint ElementSizeCount = 5;
|
||||
private const uint DirectionCount = 2;
|
||||
private const uint PipelineCount = KindCount * ElementSizeCount * DirectionCount;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,271 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
// Surface metadata (HTile, DCC, CMask, FMask) keyed by its guest address.
|
||||
public sealed partial class GuestImageCache
|
||||
{
|
||||
private unsafe void MaterializeDccClear(
|
||||
ResourceSlotIdentifier imageIdentifier,
|
||||
in ImageRequest request,
|
||||
uint metadataBaseLayer)
|
||||
{
|
||||
if (request.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var description = request.Description;
|
||||
var range = description.Metadata.Range;
|
||||
using (var held = _lock.Hold())
|
||||
{
|
||||
var image = _slots.TryGet(imageIdentifier);
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
image.Description.Metadata = description.Metadata;
|
||||
if (range.Size == 0 || description.Resources.Levels != 1 ||
|
||||
image.Description.Resources.Levels != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_surfaceMetadata.Remove(range.Address);
|
||||
}
|
||||
|
||||
var layers = description.TransferLayers;
|
||||
const ulong metadataBlockSize = 0x1000;
|
||||
if (!ImageDescription.IsValidRange(range) ||
|
||||
range.Address % metadataBlockSize != 0 ||
|
||||
layers == 0 ||
|
||||
range.Size % layers != 0 ||
|
||||
(range.Size / layers) % metadataBlockSize != 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"DCC slices must contain aligned 4 KiB blocks: address=0x{range.Address:X16} size=0x{range.Size:X} layers={layers}.");
|
||||
}
|
||||
|
||||
var view = request.View;
|
||||
var volume = description.IsVolume && view.Type == ImageViewType.Type3D;
|
||||
var first = volume ? 0u : metadataBaseLayer;
|
||||
var imageFirst = volume ? 0u : view.BaseLayer;
|
||||
var count = volume ? description.Extent.Depth : view.LayerCount;
|
||||
if (first >= layers || count == 0 || count > layers - first)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC view exceeds its metadata slices: first={first} count={count} layers={layers}.");
|
||||
}
|
||||
|
||||
var sliceSize = range.Size / layers;
|
||||
var synchronizedAddress = range.Address + sliceSize * first;
|
||||
var synchronizedSize = sliceSize * count;
|
||||
|
||||
if (_bufferCache.HasGpuDirtyBytes(synchronizedAddress, synchronizedSize))
|
||||
{
|
||||
_bufferCache.ReadMemory(synchronizedAddress, synchronizedSize, isWrite: false);
|
||||
}
|
||||
|
||||
const int metadataScanChunkSize = 64 * 1024;
|
||||
var bytes = System.Buffers.ArrayPool<byte>.Shared.Rent(
|
||||
checked((int)Math.Min(sliceSize, (ulong)metadataScanChunkSize)));
|
||||
try
|
||||
{
|
||||
Span<byte> firstByte = stackalloc byte[1];
|
||||
for (uint slice = 0; slice < count; slice++)
|
||||
{
|
||||
var address = range.Address + sliceSize * (first + slice);
|
||||
if (!_backing.TryReadBacking(address, firstByte))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"Could not read DCC metadata key: address=0x{address:X16}.");
|
||||
}
|
||||
|
||||
var code = firstByte[0];
|
||||
if (!PackedClearValue.TryDecodeDccColor(
|
||||
request.View.Format,
|
||||
code * 0x01010101u,
|
||||
description.Metadata,
|
||||
allowClearToRegister: request.Role == ImageRole.ColorTarget,
|
||||
out var clear))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var uniform = true;
|
||||
for (ulong position = 0; position < sliceSize && uniform;)
|
||||
{
|
||||
var length = checked((int)Math.Min(
|
||||
sliceSize - position,
|
||||
(ulong)Math.Min(bytes.Length, metadataScanChunkSize)));
|
||||
var chunk = bytes.AsSpan(0, length);
|
||||
if (!_backing.TryReadBacking(address + position, chunk))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"Could not read DCC metadata backing: address=0x{address + position:X16} size=0x{length:X}.");
|
||||
}
|
||||
|
||||
for (var index = 0; index < chunk.Length; index++)
|
||||
{
|
||||
if (chunk[index] != code)
|
||||
{
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
position += (ulong)length;
|
||||
}
|
||||
|
||||
if (!uniform)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var held = _lock.Hold())
|
||||
{
|
||||
if (_slots.TryGet(imageIdentifier) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearDccImage(
|
||||
imageIdentifier,
|
||||
request,
|
||||
new SubresourceRange(view.BaseLevel, view.LevelCount, imageFirst + slice, 1),
|
||||
clear);
|
||||
}
|
||||
|
||||
if (request.Role != ImageRole.DisplaySurface)
|
||||
{
|
||||
_bufferCache.FillBuffer(address, sliceSize, uint.MaxValue, isGds: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ClearDccImage(
|
||||
ResourceSlotIdentifier imageIdentifier,
|
||||
in ImageRequest request,
|
||||
in SubresourceRange range,
|
||||
in ClearColorValue clear)
|
||||
{
|
||||
var image = _slots[imageIdentifier];
|
||||
ref readonly var description = ref image.Description;
|
||||
if (range.BaseLevel >= description.Resources.Levels || range.LevelCount == 0 ||
|
||||
range.LevelCount > description.Resources.Levels - range.BaseLevel)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC clear mip range is invalid: base={range.BaseLevel} count={range.LevelCount} levels={description.Resources.Levels}.");
|
||||
}
|
||||
|
||||
var layers = description.IsVolume
|
||||
? Math.Max(description.Extent.Depth >> (int)range.BaseLevel, 1)
|
||||
: image.Backing.Layers;
|
||||
if (range.LayerCount == 0 || range.BaseLayer >= layers || range.LayerCount > layers - range.BaseLayer)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC clear layer range is invalid: base={range.BaseLayer} count={range.LayerCount} layers={layers}.");
|
||||
}
|
||||
|
||||
var fullImage = range.BaseLevel == 0 && range.LevelCount == description.Resources.Levels &&
|
||||
range.BaseLayer == 0 && range.LayerCount == layers;
|
||||
WatchImage(imageIdentifier);
|
||||
if (!fullImage && (image.IsBufferModified || image.IsCpuDirty))
|
||||
{
|
||||
PopulateFromGuest(imageIdentifier, RefreshRequest(image), "before-dcc-clear");
|
||||
if (image.Description.Samples == 1 && (image.IsBufferModified || image.IsCpuDirty))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC clear left guest ownership in place: address=0x{description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty}.");
|
||||
}
|
||||
}
|
||||
|
||||
var scheduled = _scheduler.Current;
|
||||
if (scheduled.IsInvalid)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("A DCC clear needs a command buffer that is recording.");
|
||||
}
|
||||
|
||||
scheduled.EndRendering();
|
||||
var command = new CommandBuffer(scheduled.Handle);
|
||||
var needsAttachmentClear = request.View.Format != image.Backing.Format ||
|
||||
(description.IsVolume && !fullImage);
|
||||
if (needsAttachmentClear)
|
||||
{
|
||||
var viewDescription = new ImageViewDescription(
|
||||
request.View.Format,
|
||||
range.LayerCount == 1 ? ImageViewType.Type2D : ImageViewType.Type2DArray,
|
||||
ImageAspectFlags.ColorBit,
|
||||
range.BaseLevel,
|
||||
range.LevelCount,
|
||||
range.BaseLayer,
|
||||
range.LayerCount,
|
||||
default,
|
||||
ImageUsageFlags.ColorAttachmentBit);
|
||||
var view = image.GetOrCreateView(viewDescription);
|
||||
image.Transition(
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
AccessFlags.ColorAttachmentWriteBit,
|
||||
range,
|
||||
command);
|
||||
var attachment = new RenderingAttachmentInfo
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = view,
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = AttachmentLoadOp.Clear,
|
||||
StoreOp = AttachmentStoreOp.Store,
|
||||
ClearValue = new ClearValue { Color = clear },
|
||||
};
|
||||
var rendering = new RenderingInfo
|
||||
{
|
||||
SType = StructureType.RenderingInfo,
|
||||
RenderArea = new Rect2D(
|
||||
new Offset2D(0, 0),
|
||||
new Extent2D(
|
||||
Math.Max(description.Extent.Width >> (int)range.BaseLevel, 1),
|
||||
Math.Max(description.Extent.Height >> (int)range.BaseLevel, 1))),
|
||||
LayerCount = range.LayerCount,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachments = &attachment,
|
||||
};
|
||||
_device.Vk.CmdBeginRendering(command, &rendering);
|
||||
_device.Vk.CmdEndRendering(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
|
||||
var nativeRange = new ImageSubresourceRange(
|
||||
ImageAspectFlags.ColorBit,
|
||||
range.BaseLevel,
|
||||
range.LevelCount,
|
||||
description.IsVolume ? 0u : range.BaseLayer,
|
||||
description.IsVolume ? 1u : range.LayerCount);
|
||||
var nativeClear = clear;
|
||||
_device.Vk.CmdClearColorImage(
|
||||
command,
|
||||
image.Backing.Handle,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
&nativeClear,
|
||||
1,
|
||||
&nativeRange);
|
||||
}
|
||||
|
||||
TakeGpuOwnership(image);
|
||||
}
|
||||
|
||||
public bool IsMetadata(ulong address)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
|
|
@ -57,7 +315,6 @@ public sealed partial class GuestImageCache
|
|||
return true;
|
||||
}
|
||||
|
||||
// True when registered DCC absorbed the fill and the guest dispatch can be skipped.
|
||||
public bool TryAbsorbDccFill(ulong address, ulong size, uint fillValue)
|
||||
{
|
||||
if (!IsValidRange(address, size))
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public sealed partial class GuestImageCache
|
|||
private static ColorTransferPlan? PieceHashPlan(CachedImage image, in ImageRequest request)
|
||||
{
|
||||
ref readonly var info = ref image.Description;
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
|
||||
info.Data.Size < PieceHashMinimumImageSize)
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -123,17 +123,48 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
return GetNullImage(request);
|
||||
}
|
||||
|
||||
var found = LookUpImage(ref request, exactFormat);
|
||||
var metadataBaseLayer = request.View.BaseLayer;
|
||||
var result = LookUpImage(ref request, exactFormat);
|
||||
|
||||
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
|
||||
{
|
||||
ref readonly var description = ref _slots[found].Description;
|
||||
if (description.DccSliceSize is var sliceSize and not 0)
|
||||
ulong metadataAddress;
|
||||
ulong sliceSize;
|
||||
using (var held = _lock.Hold())
|
||||
{
|
||||
SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
var image = _slots[result];
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0 &&
|
||||
image.Description.Metadata.Kind == MetadataKind.None)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
}
|
||||
|
||||
ref readonly var description = ref image.Description;
|
||||
metadataAddress = description.Metadata.Range.Address;
|
||||
sliceSize = description.DccSliceSize;
|
||||
}
|
||||
|
||||
if (sliceSize != 0)
|
||||
{
|
||||
SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
MaterializeDccClear(result, request, metadataBaseLayer);
|
||||
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
var image = _slots[result];
|
||||
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
|
||||
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
|
||||
if (!nativeCurrent)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
|
||||
|
|
@ -220,17 +251,6 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
}
|
||||
|
||||
var image = _slots[result];
|
||||
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
|
||||
{
|
||||
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
|
||||
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
|
||||
if (!nativeCurrent)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (viewMip >= 0)
|
||||
{
|
||||
request.View = request.View with { BaseLevel = (uint)viewMip };
|
||||
|
|
@ -342,8 +362,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
image.MarkGpuModified();
|
||||
image.Uses.RenderTarget = true;
|
||||
RefreshFromGuest(imageIdentifier, request);
|
||||
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
var address = request.Description.Metadata.Range.Address;
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ public struct MetadataDescription
|
|||
public GuestSpan Range;
|
||||
public MetadataKind Kind;
|
||||
public uint Control;
|
||||
public uint DccClearWord;
|
||||
public DisplayCompression Compression;
|
||||
public bool StencilCompressed;
|
||||
public bool DccClearRegisterValid;
|
||||
public bool DccAlphaMsb;
|
||||
}
|
||||
|
||||
public readonly record struct SubresourceCount(uint Levels, uint Layers) : IComparable<SubresourceCount>
|
||||
|
|
@ -366,7 +369,8 @@ public struct ImageDescription
|
|||
{
|
||||
case MetadataKind.None:
|
||||
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
|
||||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
|
||||
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
|
||||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
|
||||
}
|
||||
|
|
@ -378,6 +382,11 @@ public struct ImageDescription
|
|||
throw SubmissionScheduler.Fatal($"The HTile metadata is invalid: address=0x{Metadata.Range.Address:X16} size=0x{Metadata.Range.Size:X16}.");
|
||||
}
|
||||
|
||||
if (Metadata.DccClearWord != 0 || Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("HTile metadata carries DCC clear state.");
|
||||
}
|
||||
|
||||
break;
|
||||
case MetadataKind.Dcc:
|
||||
if (Metadata.Range.Address == 0 || Metadata.Range.Address >= TrackerLayout.SpaceBytes ||
|
||||
|
|
@ -507,6 +516,87 @@ public static class DisplayFormatRule
|
|||
// Decoders for the packed 32-bit clear values guest compute fills carry.
|
||||
public static class PackedClearValue
|
||||
{
|
||||
public static bool SupportsDccFixedColor(Format format) => DccComponentCount(format) != 0;
|
||||
|
||||
public static bool TryDecodeDccColor(
|
||||
Format format,
|
||||
uint metadataFill,
|
||||
in MetadataDescription metadata,
|
||||
bool allowClearToRegister,
|
||||
out ClearColorValue clear)
|
||||
{
|
||||
clear = default;
|
||||
var code = (byte)metadataFill;
|
||||
if (metadataFill != code * 0x01010101u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (code == 0x20)
|
||||
{
|
||||
return allowClearToRegister && metadata.DccClearRegisterValid &&
|
||||
TryDecodeColor(format, metadata.DccClearWord, out clear);
|
||||
}
|
||||
|
||||
if (code == 0x00)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var components = DccComponentCount(format);
|
||||
if (components == 0 || code is not (0x40 or 0x80 or 0xc0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rgb = (code & 0x80) != 0 ? 1.0f : 0.0f;
|
||||
var alpha = (code & 0x40) != 0 ? 1.0f : 0.0f;
|
||||
Span<float> channels = stackalloc float[4] { rgb, rgb, rgb, alpha };
|
||||
if (!metadata.DccAlphaMsb)
|
||||
{
|
||||
(channels[0], channels[3]) = (channels[3], channels[0]);
|
||||
}
|
||||
|
||||
if (components == 1)
|
||||
{
|
||||
channels[0] = channels[3];
|
||||
}
|
||||
else if (components == 2)
|
||||
{
|
||||
channels[1] = channels[3];
|
||||
}
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case Format.B8G8R8A8Unorm:
|
||||
case Format.B8G8R8A8Srgb:
|
||||
case Format.A2R10G10B10UnormPack32:
|
||||
case Format.A1R5G5B5UnormPack16:
|
||||
case Format.R5G6B5UnormPack16:
|
||||
(channels[0], channels[2]) = (channels[2], channels[0]);
|
||||
break;
|
||||
case Format.R4G4B4A4UnormPack16:
|
||||
(channels[0], channels[3]) = (channels[3], channels[0]);
|
||||
(channels[1], channels[2]) = (channels[2], channels[1]);
|
||||
break;
|
||||
}
|
||||
|
||||
clear = new ClearColorValue(channels[0], channels[1], channels[2], channels[3]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint DccComponentCount(Format format) => format switch
|
||||
{
|
||||
Format.R8Unorm or Format.R16Unorm or Format.R16Sfloat or Format.R32Sfloat => 1,
|
||||
Format.R8G8Unorm or Format.R16G16Unorm or Format.R16G16Sfloat or Format.R32G32Sfloat => 2,
|
||||
Format.R5G6B5UnormPack16 or Format.B10G11R11UfloatPack32 => 3,
|
||||
Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
|
||||
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.A1R5G5B5UnormPack16 or
|
||||
Format.R4G4B4A4UnormPack16 or Format.R16G16B16A16Unorm or Format.R16G16B16A16Sfloat or
|
||||
Format.R32G32B32A32Sfloat => 4,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static bool TryDecodeColor(Format format, uint packed, out ClearColorValue clear)
|
||||
{
|
||||
static float Unorm8(uint value) => (value & 0xff) / 255.0f;
|
||||
|
|
|
|||
|
|
@ -43,24 +43,38 @@ public static partial class ImageRequestBuilders
|
|||
|
||||
public static uint SampleCount(uint encodedLog2) => encodedLog2 <= 3 ? 1u << (int)encodedLog2 : 0;
|
||||
|
||||
private static bool DccAlphaOnMsb(in ColorTargetWords words)
|
||||
{
|
||||
switch (words.Layout)
|
||||
{
|
||||
case ChannelLayout.Bits10_10_10_2:
|
||||
case ChannelLayout.Bits10_10_10_2Float:
|
||||
case ChannelLayout.Bits5_5_5_1:
|
||||
return true;
|
||||
case ChannelLayout.Bits2_10_10_10:
|
||||
case ChannelLayout.Bits1_5_5_5:
|
||||
return false;
|
||||
}
|
||||
|
||||
var components = GuestPixelFormats.ResolveRenderTargetEncoding(words.Layout, words.NumberType).Components;
|
||||
if (components == 1)
|
||||
{
|
||||
return words.Order != ChannelOrder.Standard;
|
||||
}
|
||||
|
||||
return components == 3 || words.Order is ChannelOrder.Standard or ChannelOrder.Alternate;
|
||||
}
|
||||
|
||||
// DCC clears use the target's packed clear word; the fixed-clear set is a format allow-list.
|
||||
private static (bool Supported, bool FixedSupported, ClearColorValue Value) DccClearInfo(Format format, bool hasDcc, uint packedClear)
|
||||
{
|
||||
ClearColorValue value = default;
|
||||
var supported = hasDcc && PackedClearValue.TryDecodeColor(format, packedClear, out value);
|
||||
var fixedSupported = hasDcc && SupportsDccFixedClear(format);
|
||||
var fixedSupported = hasDcc && PackedClearValue.SupportsDccFixedColor(format);
|
||||
return (supported, fixedSupported, supported ? value : default);
|
||||
}
|
||||
|
||||
public static bool SupportsDccFixedClear(Format format) => format switch
|
||||
{
|
||||
Format.R8Unorm or Format.R8G8Unorm or Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
|
||||
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.R5G6B5UnormPack16 or Format.A1R5G5B5UnormPack16 or
|
||||
Format.R4G4B4A4UnormPack16 or Format.R16Unorm or Format.R16G16Unorm or Format.R16G16B16A16Unorm or Format.R16Sfloat or
|
||||
Format.R16G16Sfloat or Format.R16G16B16A16Sfloat or Format.R32Sfloat or Format.R32G32Sfloat or Format.R32G32B32A32Sfloat or
|
||||
Format.B10G11R11UfloatPack32 => true,
|
||||
_ => false,
|
||||
};
|
||||
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
|
||||
|
||||
// Builds the request for a bound color target. Null when the slot carries no target.
|
||||
public static ColorTargetResolution? ColorTarget(in ColorTargetWords words, uint targetMask, uint drawLayerOffset, bool ignoreTargetMask)
|
||||
|
|
@ -324,8 +338,14 @@ public static partial class ImageRequestBuilders
|
|||
if (hasDcc)
|
||||
{
|
||||
// DCC lives in its own allocation; the address lets the cache match fills seen before the target.
|
||||
_ = TileGeometry.TryGetDccSize(
|
||||
width, height, volume ? depth : view.ImageLayers, bytesPerElement, levels, tileMode,
|
||||
out var metadataSize, words.FragmentsLog2);
|
||||
description.Metadata.Kind = MetadataKind.Dcc;
|
||||
description.Metadata.Range = new GuestSpan(words.DccAddress, 0);
|
||||
description.Metadata.Range = new GuestSpan(words.DccAddress, metadataSize.Size);
|
||||
description.Metadata.DccClearWord = words.ClearWord0;
|
||||
description.Metadata.DccClearRegisterValid = true;
|
||||
description.Metadata.DccAlphaMsb = DccAlphaOnMsb(words);
|
||||
}
|
||||
|
||||
for (var level = 0; level < levels; level++)
|
||||
|
|
|
|||
|
|
@ -58,8 +58,6 @@ public static partial class ImageRequestBuilders
|
|||
return Format.Undefined;
|
||||
}
|
||||
|
||||
private static bool HtileStencilCompatible(bool hasStencil, bool hasHtile, bool htileStencilDisabled) => !hasStencil || !hasHtile || htileStencilDisabled;
|
||||
|
||||
// Builds the request for the bound depth target. Null when no depth or stencil state is active.
|
||||
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) =>
|
||||
DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
|
||||
|
|
@ -127,7 +125,7 @@ public static partial class ImageRequestBuilders
|
|||
|
||||
if (hasStencil)
|
||||
{
|
||||
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) ||
|
||||
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt ||
|
||||
stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
|
|
|
|||
|
|
@ -300,12 +300,15 @@ public static partial class ImageRequestBuilders
|
|||
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
|
||||
description.Samples = samples;
|
||||
description.TileMode = tile;
|
||||
var metadataAddress = descriptor.MetadataAddress << 8;
|
||||
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
|
||||
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
|
||||
if (!shape.R128 && words.Length >= 8 && descriptor.MetadataCompress &&
|
||||
tile != GuestTileMode.Depth && !description.IsDepth)
|
||||
{
|
||||
_ = TileGeometry.TryGetDccSize(
|
||||
width, height, volume ? depth : imageLayers, description.BytesPerBlock, levels, tile,
|
||||
out var metadataSize, (uint)System.Numerics.BitOperations.TrailingZeroCount(samples));
|
||||
description.Metadata.Kind = MetadataKind.Dcc;
|
||||
description.Metadata.Range = new GuestSpan(metadataAddress, 0);
|
||||
description.Metadata.Range = new GuestSpan(descriptor.MetadataAddress << 8, metadataSize.Size);
|
||||
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
|
||||
}
|
||||
|
||||
if (samples > 1)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ public readonly struct TextureDescriptorWords
|
|||
|
||||
public bool MetadataCompress => ((Fields[6] >> 21) & 0x1) == 1;
|
||||
|
||||
public bool DccAlphaMsb => ((Fields[6] >> 22) & 0x1) == 1;
|
||||
|
||||
public ulong MetadataAddress => ((Fields[6] >> 24) & 0xFF) | ((ulong)Fields[7] << 8);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -452,7 +452,9 @@ public sealed class TextureTransferLayout
|
|||
transfer.Width = Math.Max((region.ImageExtent.Width + texture.TexelWidth - 1) / texture.TexelWidth, 1);
|
||||
transfer.Height = Math.Max((logicalHeight + texture.TexelHeight - 1) / texture.TexelHeight, 1);
|
||||
transfer.Depth = 1;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB ? region.ImageSubresource.BaseArrayLayer : 0;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
|
||||
? region.ImageSubresource.BaseArrayLayer
|
||||
: 0;
|
||||
transfer.Pitch = Math.Max((pitch + texture.TexelWidth - 1) / texture.TexelWidth, 1);
|
||||
transfer.Tail = tail;
|
||||
transfer.TailX = tail ? mip.TailX : 0;
|
||||
|
|
|
|||
|
|
@ -230,6 +230,75 @@ public static partial class TileGeometry
|
|||
}
|
||||
}
|
||||
|
||||
private static uint Gen5RenderTargetOffset(uint x, uint y, uint bytesPerElement)
|
||||
{
|
||||
uint offset = 0;
|
||||
switch (bytesPerElement)
|
||||
{
|
||||
case 1:
|
||||
offset ^= (y << 2) & 0x0008;
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 3) & 0x00a0;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 6) & 0x1000;
|
||||
offset ^= (y << 7) & 0x4000;
|
||||
offset ^= x & 0x0007;
|
||||
offset ^= (x << 3) & 0x0040;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
return offset;
|
||||
case 2:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 8) & 0x5000;
|
||||
offset ^= (x << 1) & 0x000e;
|
||||
offset ^= (x << 4) & 0x0480;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
return offset;
|
||||
case 4:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 9) & 0x1000;
|
||||
offset ^= (y << 8) & 0x4000;
|
||||
offset ^= (x << 2) & 0x000c;
|
||||
offset ^= (x << 5) & 0x0380;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 9) & 0xa000;
|
||||
return offset;
|
||||
case 8:
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 10) & 0x5000;
|
||||
offset ^= (x << 3) & 0x0008;
|
||||
offset ^= (x << 4) & 0x0460;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 10) & 0x2000;
|
||||
offset ^= (x << 9) & 0x8000;
|
||||
return offset;
|
||||
case 16:
|
||||
offset ^= (x << 4) & 0x0410;
|
||||
offset ^= (x << 5) & 0x0340;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 11) & 0xa000;
|
||||
offset ^= (y << 5) & 0x0f20;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 10) & 0x1000;
|
||||
offset ^= (y << 11) & 0x4000;
|
||||
return offset;
|
||||
default:
|
||||
throw SubmissionScheduler.Fatal($"The Gen5 render-target tile block does not support this element size: bytes={bytesPerElement}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static uint Depth64KB8X(uint x) =>
|
||||
(x & 0x0001) ^ ((x << 1) & 0x0004) ^ ((x << 2) & 0x0010) ^ ((x << 3) & 0x0040) ^ ((x << 5) & 0x0300) ^
|
||||
((x << 4) & 0x0400) ^ ((x << 6) & 0x0800) ^ ((x << 7) & 0x2000) ^ ((x << 8) & 0x8000);
|
||||
|
|
@ -319,6 +388,14 @@ public static partial class TileGeometry
|
|||
|
||||
offset = RenderTargetOffset(x, y, bytes);
|
||||
break;
|
||||
case TileBlockKind.RenderTarget64KBGen5:
|
||||
if (bytes > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = Gen5RenderTargetOffset(x, y, bytes);
|
||||
break;
|
||||
case TileBlockKind.Depth64KB:
|
||||
switch (bytes)
|
||||
{
|
||||
|
|
@ -364,17 +441,19 @@ public static partial class TileGeometry
|
|||
|
||||
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
|
||||
}
|
||||
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
|
||||
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
|
||||
{
|
||||
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
|
||||
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
|
||||
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
}
|
||||
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
|
||||
{
|
||||
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public enum TileBlockKind : uint
|
|||
Prt64KB3D,
|
||||
RenderTarget64KB,
|
||||
Depth64KB,
|
||||
RenderTarget64KBGen5,
|
||||
}
|
||||
|
||||
public readonly record struct TileBlockLayout(TileBlockKind Kind, uint BytesPerElement, uint BlockSize, uint BlockWidth, uint BlockHeight, uint BlockDepth)
|
||||
|
|
@ -109,6 +110,7 @@ public static partial class TileGeometry
|
|||
new(65536, Thick64KB, 16),
|
||||
new(65536, Thin64KB, 16),
|
||||
new(65536, Thin64KB, 8),
|
||||
new(65536, Thin64KB, 16),
|
||||
];
|
||||
|
||||
private static readonly Log2BlockDimensions[][] MsaaBlocks =
|
||||
|
|
@ -247,6 +249,7 @@ public static partial class TileGeometry
|
|||
case TileBlockKind.Standard64KB:
|
||||
case TileBlockKind.Prt64KB:
|
||||
case TileBlockKind.RenderTarget64KB:
|
||||
case TileBlockKind.RenderTarget64KBGen5:
|
||||
rule = new MipTailRule(TailThin64KB[index], block.BlockWidth >> 1, block.BlockHeight);
|
||||
return true;
|
||||
default:
|
||||
|
|
@ -297,7 +300,7 @@ public static partial class TileGeometry
|
|||
kind = TileBlockKind.Depth64KB;
|
||||
break;
|
||||
case GuestTileMode.RenderTarget:
|
||||
kind = TileBlockKind.RenderTarget64KB;
|
||||
kind = TileBlockKind.RenderTarget64KBGen5;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -308,7 +311,7 @@ public static partial class TileGeometry
|
|||
return false;
|
||||
}
|
||||
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB &&
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
|
||||
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -605,6 +608,39 @@ public static partial class TileGeometry
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetDccSize(
|
||||
uint width,
|
||||
uint height,
|
||||
uint slices,
|
||||
uint bytesPerElement,
|
||||
uint levels,
|
||||
GuestTileMode tile,
|
||||
out TileSizeAndAlignment totalSize,
|
||||
uint fragmentsLog2 = 0)
|
||||
{
|
||||
totalSize = default;
|
||||
if (width == 0 || height == 0 || slices == 0 || levels != 1 || fragmentsLog2 != 0 ||
|
||||
!BitOperations.IsPow2(bytesPerElement) || bytesPerElement > 16 ||
|
||||
tile is not (GuestTileMode.RenderTarget or GuestTileMode.Depth))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var coverageBits = 20 - BitOperations.TrailingZeroCount(bytesPerElement);
|
||||
var blockWidth = 1u << ((coverageBits + 1) / 2);
|
||||
var blockHeight = 1u << (coverageBits / 2);
|
||||
var blocksX = ((ulong)width + blockWidth - 1) / blockWidth;
|
||||
var blocksY = ((ulong)height + blockHeight - 1) / blockHeight;
|
||||
var blocks = blocksX * blocksY;
|
||||
if (blocks == 0 || blocks > uint.MaxValue / 4096u / slices)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
totalSize = new TileSizeAndAlignment((uint)(blocks * slices * 4096u), 4096);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetRenderTargetMipLayout(uint width, uint height, uint pitch, uint bytesPerElement, uint levels, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
|
||||
{
|
||||
totalSize = default;
|
||||
|
|
|
|||
|
|
@ -124,6 +124,57 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend, IGuestImageSnapsh
|
|||
|
||||
public IdleOutcome SubmitDone(ICpuMemory memory) => EnsureCommandStream(memory).Done();
|
||||
|
||||
public void RunAfterPendingCommandStreams(Action work)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(work);
|
||||
|
||||
CommandStreamQueue? commandStream;
|
||||
CommandStreamWorker? worker;
|
||||
lock (_commandStreamGate)
|
||||
{
|
||||
commandStream = _commandStream;
|
||||
worker = _commandStreamWorker;
|
||||
}
|
||||
|
||||
if (commandStream is null || worker?.IsWorkerThread == true)
|
||||
{
|
||||
work();
|
||||
return;
|
||||
}
|
||||
|
||||
using var done = new SemaphoreSlim(0);
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo? failure = null;
|
||||
if (!commandStream.TryEnqueueControlBarrier(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
work();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
done.Release();
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(
|
||||
new OperationCanceledException("The command stream stopped before the video-out state change could run."));
|
||||
done.Release();
|
||||
}))
|
||||
{
|
||||
work();
|
||||
return;
|
||||
}
|
||||
|
||||
done.Wait();
|
||||
failure?.Throw();
|
||||
}
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
||||
MetalVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal interface IShaderPipelineHost
|
|||
// The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be
|
||||
// emitted as real 64-bit atomics instead of a non-atomic 32-bit pair.
|
||||
bool SharedInt64AtomicsEnabled { get; }
|
||||
bool ShaderSignedZeroInfNanPreserveFloat32Supported => false;
|
||||
bool ExecGuardElisionEnabled => true;
|
||||
ShaderPrewarmList? ShaderPrewarm => null;
|
||||
bool PerVertexPixelInputsSupported => true;
|
||||
|
|
|
|||
|
|
@ -593,6 +593,7 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
|
||||
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
|
||||
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
|
||||
switch (source.Stage)
|
||||
{
|
||||
case ShaderStage.Vertex:
|
||||
|
|
@ -605,6 +606,7 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
|
|
@ -632,6 +634,7 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
|
|
@ -643,7 +646,7 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
default:
|
||||
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled);
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -659,7 +662,8 @@ internal sealed class ShaderProgramCache
|
|||
usesDispatchThreadLimits: usesDispatchThreadLimits);
|
||||
|
||||
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision) =>
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
|
||||
bool signedZeroInfNanPreserve) =>
|
||||
new(plan, resources, layout)
|
||||
{
|
||||
WaveSize = info.WaveSize,
|
||||
|
|
@ -667,6 +671,7 @@ internal sealed class ShaderProgramCache
|
|||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
ScratchDwords = info.ScratchDwords,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
ComputeSystemRegisters = systemRegisters,
|
||||
LocalDataShareDwords = info.LocalDataShareDwords,
|
||||
LocalSizeX = Math.Max(info.ThreadsX, 1),
|
||||
|
|
@ -675,7 +680,8 @@ internal sealed class ShaderProgramCache
|
|||
};
|
||||
|
||||
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
|
||||
bool sharedInt64Atomics, bool execGuardElision, out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled,
|
||||
out BindingLayout? layout, out string error)
|
||||
{
|
||||
compiled = null;
|
||||
layout = null;
|
||||
|
|
@ -692,7 +698,7 @@ internal sealed class ShaderProgramCache
|
|||
layout = AllocateLayout(program, plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor,
|
||||
record.Info.DispatchThreadDimensions);
|
||||
var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters,
|
||||
sharedInt64Atomics, execGuardElision);
|
||||
sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve);
|
||||
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
|
|||
|
|
@ -14,16 +14,60 @@ public sealed class GpuWorkerRelay : IGpuQueueRelay
|
|||
private readonly object _gate = new();
|
||||
private readonly Queue<Action> _commands = new();
|
||||
private readonly Action _wake;
|
||||
private readonly Func<Action, Action, bool>? _tryEnqueueAfterPendingWork;
|
||||
private int _pendingCount;
|
||||
private bool _accepting = true;
|
||||
|
||||
public GpuWorkerRelay(Action wake)
|
||||
public GpuWorkerRelay(Action wake, Func<Action, Action, bool>? tryEnqueueAfterPendingWork = null)
|
||||
{
|
||||
_wake = wake;
|
||||
_tryEnqueueAfterPendingWork = tryEnqueueAfterPendingWork;
|
||||
}
|
||||
|
||||
public bool TryRunAfterPendingWork(Action work) => TryRunOnGpuQueue(work);
|
||||
|
||||
public bool TryRunAfterAcceptedCommandStreams(Action work)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(work);
|
||||
if (IsGpuQueueThread || _tryEnqueueAfterPendingWork is null)
|
||||
{
|
||||
return TryRunOnGpuQueue(work);
|
||||
}
|
||||
|
||||
using var done = new SemaphoreSlim(0);
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo? failure = null;
|
||||
if (!_tryEnqueueAfterPendingWork(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
work();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
done.Release();
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(
|
||||
new OperationCanceledException(
|
||||
"The command stream stopped before the ordered GPU worker action could run."));
|
||||
done.Release();
|
||||
}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
done.Wait();
|
||||
failure?.Throw();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsGpuQueueThread => _boundWorker == this;
|
||||
|
||||
public bool HasPendingCommands => Volatile.Read(ref _pendingCount) != 0;
|
||||
|
|
|
|||
|
|
@ -272,18 +272,6 @@ public sealed class SubmissionScheduler : IGpuTickScheduler, IDisposable
|
|||
_lastMemoryDrainTick = nextTick;
|
||||
}
|
||||
|
||||
// Waits for a tick that has already been submitted; safe from any thread, since it only
|
||||
// waits on the timeline and records nothing.
|
||||
public void WaitForSubmittedTick(ulong tick)
|
||||
{
|
||||
if (tick >= CurrentTick)
|
||||
{
|
||||
throw Fatal($"Cannot wait off the queue for a tick that is still recording: {tick} >= {CurrentTick}.");
|
||||
}
|
||||
|
||||
_timeline.Wait(tick);
|
||||
}
|
||||
|
||||
public void Wait(ulong tick)
|
||||
{
|
||||
if (tick > CurrentTick)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||
public IdleOutcome SubmitDone(ICpuMemory memory) =>
|
||||
VulkanVideoPresenter.SubmitDone(memory);
|
||||
|
||||
public void RunAfterPendingCommandStreams(Action work) =>
|
||||
VulkanVideoPresenter.RunAfterPendingCommandStreams(work);
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,25 @@ public static class JsonExports
|
|||
InitializerSetGlobalNullAccessCallback(ctx);
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i1393UBWu1U",
|
||||
ExportName = "_ZN3sce4Json11Initializer28setGlobalSpecialFloatHandlerEPFKNS0_5ValueENS0_12FunctionTypeEdPS3_PvPbES6_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerSetGlobalSpecialFloatHandler(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
JsonObjectHeap.GlobalSpecialFloatHandler = ctx[CpuRegister.Rsi];
|
||||
JsonObjectHeap.GlobalSpecialFloatHandlerContext = ctx[CpuRegister.Rdx];
|
||||
TraceJson("Initializer.setGlobalSpecialFloatHandler", thisAddress, ctx[CpuRegister.Rsi]);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WSOuge5IsCg",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||
|
|
@ -168,6 +187,14 @@ public static class JsonExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GvGvswb0v34",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C2Ev",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitParameter2BaseConstructor(CpuContext ctx) =>
|
||||
InitParameter2Constructor(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "I2QC8PYhJWY",
|
||||
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||
|
|
@ -193,6 +220,14 @@ public static class JsonExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "W72B9ylU2JA",
|
||||
ExportName = "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitParameterRtti2SetAllocatorRtti(CpuContext ctx) =>
|
||||
InitParameter2SetAllocator(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Eu95jmqn5Rw",
|
||||
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||
|
|
@ -210,6 +245,30 @@ public static class JsonExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WVZBP4IyM+E",
|
||||
ExportName = "_ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitParameter2SetSpecialFloatFormatType(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> field = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(field, unchecked((uint)ctx[CpuRegister.Rsi]));
|
||||
if (!ctx.Memory.TryWrite(thisAddress + 0x18, field))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = thisAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IXW-z8pggfg",
|
||||
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ internal static class JsonObjectHeap
|
|||
|
||||
public static ulong GlobalNullAccessCallbackContext;
|
||||
|
||||
public static ulong GlobalSpecialFloatHandler;
|
||||
|
||||
public static ulong GlobalSpecialFloatHandlerContext;
|
||||
|
||||
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
|
||||
|
||||
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
|
||||
|
|
@ -106,5 +110,7 @@ internal static class JsonObjectHeap
|
|||
Strings.Clear();
|
||||
GlobalNullAccessCallback = 0;
|
||||
GlobalNullAccessCallbackContext = 0;
|
||||
GlobalSpecialFloatHandler = 0;
|
||||
GlobalSpecialFloatHandlerContext = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,20 +161,43 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
|
||||
public bool Remove(ulong ident, short filter)
|
||||
=> RemoveAll(ident, filter) != 0;
|
||||
|
||||
public int RemoveAll(ulong ident, short filter)
|
||||
{
|
||||
var index = FindIndex(ident, filter);
|
||||
if (index < 0)
|
||||
var originalCount = Count;
|
||||
var writeIndex = 0;
|
||||
for (var readIndex = 0; readIndex < originalCount; readIndex++)
|
||||
{
|
||||
return false;
|
||||
var candidate = this[readIndex];
|
||||
if (candidate.Ident == ident && candidate.Filter == filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this[writeIndex++] = candidate;
|
||||
}
|
||||
|
||||
for (var i = index; i + 1 < Count; i++)
|
||||
Count = writeIndex;
|
||||
return originalCount - writeIndex;
|
||||
}
|
||||
|
||||
public int UpdateUserData(ulong ident, short filter, ulong userData)
|
||||
{
|
||||
var updatedCount = 0;
|
||||
for (var index = 0; index < Count; index++)
|
||||
{
|
||||
this[i] = this[i + 1];
|
||||
var candidate = this[index];
|
||||
if (candidate.Ident != ident || candidate.Filter != filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this[index] = candidate with { UserData = userData };
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
Count--;
|
||||
return true;
|
||||
return updatedCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -824,52 +847,55 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
|
||||
var waiterId = Interlocked.Increment(ref _nextEventQueueWaiterId);
|
||||
if (timeoutAddress == 0)
|
||||
{
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
state.WakeKey,
|
||||
new EqueueWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
WaiterId = waiterId,
|
||||
});
|
||||
if (requestedBlock)
|
||||
var blockDeadlineTimestamp = timeoutAddress == 0
|
||||
? 0
|
||||
: GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||
TimeSpan.FromMicroseconds(timeoutUsec));
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
state.WakeKey,
|
||||
new EqueueWaiter
|
||||
{
|
||||
var wakeAfterRegistration = false;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
wakeAfterRegistration =
|
||||
!IsLiveEventQueueLocked(state) ||
|
||||
HasPendingEventsLocked(state.Handle);
|
||||
}
|
||||
|
||||
if (wakeAfterRegistration)
|
||||
{
|
||||
WakeEventQueue(
|
||||
state,
|
||||
_logEqueue
|
||||
? "source=post-registration-state-check"
|
||||
: null);
|
||||
}
|
||||
|
||||
if (_logEqueue)
|
||||
{
|
||||
TraceEventQueue(
|
||||
ctx,
|
||||
"wait-block",
|
||||
handle,
|
||||
$"generation={state.Generation} waiter={waiterId} " +
|
||||
$"capacity={eventCapacity} timeout=infinite " +
|
||||
$"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
WaiterId = waiterId,
|
||||
},
|
||||
blockDeadlineTimestamp);
|
||||
if (requestedBlock)
|
||||
{
|
||||
var wakeAfterRegistration = false;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
wakeAfterRegistration =
|
||||
!IsLiveEventQueueLocked(state) ||
|
||||
HasPendingEventsLocked(state.Handle);
|
||||
}
|
||||
|
||||
if (wakeAfterRegistration)
|
||||
{
|
||||
WakeEventQueue(
|
||||
state,
|
||||
_logEqueue
|
||||
? "source=post-registration-state-check"
|
||||
: null);
|
||||
}
|
||||
|
||||
if (_logEqueue)
|
||||
{
|
||||
TraceEventQueue(
|
||||
ctx,
|
||||
"wait-block",
|
||||
handle,
|
||||
$"generation={state.Generation} waiter={waiterId} " +
|
||||
$"capacity={eventCapacity} timeout=" +
|
||||
(timeoutAddress == 0 ? "infinite " : $"{timeoutUsec}_usec ") +
|
||||
$"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
|
|
@ -1141,9 +1167,10 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
|
||||
var key = (ident, filter);
|
||||
var isExistingRegistration = events.ContainsKey(key);
|
||||
var isNewReadRegistration =
|
||||
filter == KernelEventFilterRead &&
|
||||
!events.ContainsKey(key);
|
||||
!isExistingRegistration;
|
||||
events[(ident, filter)] = new KernelEventRegistration(
|
||||
ident,
|
||||
filter,
|
||||
|
|
@ -1152,6 +1179,11 @@ public static class KernelEventQueueCompatExports
|
|||
lowWater,
|
||||
unchecked((ulong)Interlocked.Increment(
|
||||
ref _nextEventRegistrationGeneration)));
|
||||
if (isExistingRegistration &&
|
||||
_pendingEvents.TryGetValue(handle, out var pending))
|
||||
{
|
||||
_ = pending.UpdateUserData(ident, filter, userData);
|
||||
}
|
||||
if (isNewReadRegistration)
|
||||
{
|
||||
_readEventRegistrationCount++;
|
||||
|
|
@ -1296,7 +1328,7 @@ public static class KernelEventQueueCompatExports
|
|||
|
||||
if (_pendingEvents.TryGetValue(handle, out var pending))
|
||||
{
|
||||
_ = pending.Remove(ident, filter);
|
||||
_ = pending.RemoveAll(ident, filter);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -1387,22 +1419,13 @@ public static class KernelEventQueueCompatExports
|
|||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
// Keep each interrupt as a separate event. Limit growth when the queue is not read.
|
||||
var queuedEvent = new KernelQueuedEvent(
|
||||
queue.AddLast(new KernelQueuedEvent(
|
||||
registration.Ident,
|
||||
registration.Filter,
|
||||
registration.Flags,
|
||||
1,
|
||||
data,
|
||||
registration.UserData);
|
||||
if (CountPendingEvents(queue, registration.Ident, registration.Filter) < 256)
|
||||
{
|
||||
queue.AddLast(queuedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueOrUpdateEvent(queue, queuedEvent);
|
||||
}
|
||||
registration.UserData));
|
||||
|
||||
(wakeQueues ??= []).Add(state);
|
||||
triggeredCount++;
|
||||
|
|
@ -1630,24 +1653,6 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
}
|
||||
|
||||
private static int CountPendingEvents(
|
||||
KernelEventDeque queue,
|
||||
ulong ident,
|
||||
short filter)
|
||||
{
|
||||
var count = 0;
|
||||
for (var i = 0; i < queue.Count; i++)
|
||||
{
|
||||
var pending = queue[i];
|
||||
if (pending.Ident == ident && pending.Filter == filter)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void PollReadEvents(object? state)
|
||||
{
|
||||
_ = state;
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ public static class KernelExports
|
|||
ExportName = "sceKernelOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenCore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqQMh1zPPT8",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, GuestFileShare);
|
||||
stream.SetLength(length);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ using System.Threading;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static partial class KernelMemoryCompatExports
|
||||
{
|
||||
private const int MaxGuestStringLength = 4096;
|
||||
internal const FileShare GuestFileShare = FileShare.ReadWrite | FileShare.Delete;
|
||||
private const int WideCharSize = sizeof(ushort);
|
||||
private const int MemsetChunkSize = 16 * 1024;
|
||||
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
|
||||
|
|
@ -96,6 +98,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
private static readonly object _fdGate = new();
|
||||
private static readonly Dictionary<int, FileStream> _openFiles = new();
|
||||
private static readonly HashSet<int> _randomDeviceDescriptors = new();
|
||||
private static readonly Dictionary<int, HostMovieBridge.BinkGuestCompletionShim>
|
||||
_binkGuestCompletionShims = new();
|
||||
private static readonly Dictionary<int, string> _observedBinkGuestFiles = new();
|
||||
|
|
@ -1439,7 +1442,9 @@ public static partial class KernelMemoryCompatExports
|
|||
ExportName = "_open",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpenUnderscore(CpuContext ctx)
|
||||
public static int KernelOpenUnderscore(CpuContext ctx) => PosixOpen(ctx);
|
||||
|
||||
internal static int KernelOpenCore(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
|
@ -1450,6 +1455,19 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (IsRandomDevicePath(guestPath) && (flags & O_DIRECTORY) == 0)
|
||||
{
|
||||
var randomFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
lock (_fdGate)
|
||||
{
|
||||
_randomDeviceDescriptors.Add(randomFd);
|
||||
}
|
||||
|
||||
LogOpenTrace($"_open random-device path='{guestPath}' flags=0x{flags:X8} fd={randomFd}");
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)randomFd);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var hostPath = ResolveGuestPath(guestPath);
|
||||
var access = ResolveOpenAccess(flags);
|
||||
var mode = ResolveOpenMode(flags, access);
|
||||
|
|
@ -1519,7 +1537,7 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
|
||||
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
|
||||
var stream = new FileStream(hostPath, mode, access, GuestFileShare);
|
||||
if ((flags & O_APPEND) != 0)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1677,11 +1695,11 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
// POSIX open(2): translates a failed raw open into -1/errno. On success
|
||||
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
|
||||
// KernelOpenCore already writes the fd into RAX (the import bridge
|
||||
// prefers a written RAX over the return value), so returning 0 is correct.
|
||||
public static int PosixOpen(CpuContext ctx)
|
||||
{
|
||||
var result = KernelOpenUnderscore(ctx);
|
||||
var result = KernelOpenCore(ctx);
|
||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
? 0
|
||||
: PosixFailure(ctx, result);
|
||||
|
|
@ -1734,7 +1752,8 @@ public static partial class KernelMemoryCompatExports
|
|||
{
|
||||
// Stop at the first miss and report its index.
|
||||
// The caller can then use its normal file-open fallback.
|
||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelAprResolveFilepathsToIdsAndFileSizes: '{guestPath}' -> '{hostPath}' not found (index {i} of {count})");
|
||||
if (sizesAddress != 0 &&
|
||||
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
||||
{
|
||||
|
|
@ -2254,7 +2273,12 @@ public static partial class KernelMemoryCompatExports
|
|||
string? observedBinkPath = null;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_openFiles.Remove(fd, out stream))
|
||||
if (_randomDeviceDescriptors.Remove(fd))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
else if (_openFiles.Remove(fd, out stream))
|
||||
{
|
||||
_binkGuestCompletionShims.Remove(fd);
|
||||
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
|
||||
|
|
@ -2307,6 +2331,25 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
bool isRandomDevice;
|
||||
lock (_fdGate)
|
||||
{
|
||||
isRandomDevice = _randomDeviceDescriptors.Contains(fd);
|
||||
}
|
||||
|
||||
if (isRandomDevice)
|
||||
{
|
||||
var randomBytes = GC.AllocateUninitializedArray<byte>(requested);
|
||||
RandomNumberGenerator.Fill(randomBytes);
|
||||
if (!ctx.Memory.TryWrite(bufferAddress, randomBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryReadSocketFd(
|
||||
ctx,
|
||||
fd,
|
||||
|
|
@ -2842,15 +2885,23 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
|
||||
var searchEnd = searchEndRaw <= 0
|
||||
? GuestMemoryLayout.DirectBytes
|
||||
: Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
|
||||
if (searchStart >= searchEnd)
|
||||
if (!ctx.TryWriteUInt64(outAddress, 0) || !ctx.TryWriteUInt64(outSize, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (searchStartRaw < 0 || searchEndRaw < 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var searchStart = (ulong)searchStartRaw;
|
||||
var searchEnd = Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
|
||||
if (searchStart >= searchEnd)
|
||||
{
|
||||
return MemoryNoSpace;
|
||||
}
|
||||
|
||||
bool foundSpan;
|
||||
ulong candidate;
|
||||
ulong rangeAvailable;
|
||||
|
|
@ -2866,7 +2917,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
if (!foundSpan)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return MemoryNoSpace;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
|
||||
|
|
@ -6901,9 +6952,14 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
string? hostPath = null;
|
||||
bool isDirectory = false;
|
||||
bool isRandomDevice = false;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
if (_randomDeviceDescriptors.Contains(fd))
|
||||
{
|
||||
isRandomDevice = true;
|
||||
}
|
||||
else if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
{
|
||||
hostPath = directory.Path;
|
||||
isDirectory = true;
|
||||
|
|
@ -6914,6 +6970,14 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
}
|
||||
|
||||
if (isRandomDevice)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
LogIoTrace("fstat", "/dev/urandom", $"fd={fd} size=0 device=random");
|
||||
return TryWriteKernelStat(ctx, statAddress, isDirectory: false, size: 0,
|
||||
now, now, now, "/dev/urandom");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hostPath))
|
||||
{
|
||||
long size = 0;
|
||||
|
|
@ -6935,6 +6999,10 @@ public static partial class KernelMemoryCompatExports
|
|||
return !string.IsNullOrWhiteSpace(hostPath) && TryWriteHostPathStat(ctx, statAddress, hostPath!, isDirectory);
|
||||
}
|
||||
|
||||
private static bool IsRandomDevicePath(string path) =>
|
||||
string.Equals(path, "/dev/random", StringComparison.Ordinal) ||
|
||||
string.Equals(path, "/dev/urandom", StringComparison.Ordinal);
|
||||
|
||||
private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath)
|
||||
{
|
||||
var isDirectory = Directory.Exists(hostPath);
|
||||
|
|
|
|||
|
|
@ -548,23 +548,38 @@ public static class KernelPthreadExtendedCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ulong stackAddress = 0;
|
||||
ulong stackSize = 0;
|
||||
var hasStackBounds = false;
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is not null &&
|
||||
scheduler.TryGetGuestThreadStackBounds(thread, out stackAddress, out stackSize) &&
|
||||
IsValidStackBounds(stackAddress, stackSize))
|
||||
{
|
||||
hasStackBounds = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
stackAddress = 0;
|
||||
stackSize = 0;
|
||||
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
|
||||
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out stackAddress))
|
||||
{
|
||||
stackSize = NativeGuestStackSize;
|
||||
hasStackBounds = true;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
var threadState = GetOrCreateThreadStateLocked(thread);
|
||||
|
||||
// The native executor maps guest pthread stacks itself, after the
|
||||
// kernel-facing thread object has been created. Report that live
|
||||
// mapping when a thread asks for its own attributes. IL2CPP's
|
||||
// conservative collector uses these two fields to register the stack;
|
||||
// returning the default null address lets it recycle objects that are
|
||||
// still reachable only from guest registers/stack frames.
|
||||
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
|
||||
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out var stackAddress))
|
||||
if (hasStackBounds)
|
||||
{
|
||||
threadState.Attributes = threadState.Attributes with
|
||||
{
|
||||
StackAddress = stackAddress,
|
||||
StackSize = NativeGuestStackSize,
|
||||
StackSize = stackSize,
|
||||
};
|
||||
}
|
||||
_attrStates[outAttrAddress] = threadState.Attributes;
|
||||
|
|
@ -574,6 +589,11 @@ public static class KernelPthreadExtendedCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool IsValidStackBounds(ulong stackAddress, ulong stackSize) =>
|
||||
stackAddress != 0 &&
|
||||
stackSize != 0 &&
|
||||
stackAddress <= ulong.MaxValue - stackSize;
|
||||
|
||||
private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress)
|
||||
{
|
||||
stackAddress = 0;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,13 @@ public static class KernelRuntimeCompatExports
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate ulong RdtscDelegate();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uvT2iYBBnkY",
|
||||
ExportName = "sceKernelSync",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSync(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1jfXLRVzisc",
|
||||
ExportName = "sceKernelUsleep",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public static class KernelSemaphoreCompatExports
|
|||
Count = initialCount,
|
||||
};
|
||||
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
if (!ctx.TryWriteUInt64(semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -113,6 +113,27 @@ public static class KernelSemaphoreCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return WaitSemaphoreCore(
|
||||
ctx,
|
||||
semaphore,
|
||||
handle,
|
||||
needCount,
|
||||
hasTimeout: timeoutAddress != 0,
|
||||
timeoutAddress,
|
||||
timeoutUsec,
|
||||
"sceKernelWaitSema");
|
||||
}
|
||||
|
||||
private static int WaitSemaphoreCore(
|
||||
CpuContext ctx,
|
||||
KernelSemaphoreState semaphore,
|
||||
uint handle,
|
||||
int needCount,
|
||||
bool hasTimeout,
|
||||
ulong timeoutAddress,
|
||||
uint timeoutUsec,
|
||||
string operationName)
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
|
|
@ -125,7 +146,7 @@ public static class KernelSemaphoreCompatExports
|
|||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")}");
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
|
@ -137,7 +158,7 @@ public static class KernelSemaphoreCompatExports
|
|||
// tokens (so a wake commits the acquisition), while the resume
|
||||
// handler distinguishes a real acquisition from a deadline expiry.
|
||||
var acquired = false;
|
||||
var deadline = timeoutAddress != 0
|
||||
var deadline = hasTimeout
|
||||
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||
: 0;
|
||||
|
||||
|
|
@ -187,7 +208,7 @@ public static class KernelSemaphoreCompatExports
|
|||
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
operationName,
|
||||
GetSemaphoreWakeKey(handle),
|
||||
ResumeWait,
|
||||
WakePredicate,
|
||||
|
|
@ -216,14 +237,21 @@ public static class KernelSemaphoreCompatExports
|
|||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Not a guest thread (or no scheduler): fall back to a host-thread
|
||||
// wait so the semantics still hold on non-cooperative callers.
|
||||
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
|
||||
return WaitSemaphoreOnHostThread(
|
||||
ctx,
|
||||
semaphore,
|
||||
handle,
|
||||
needCount,
|
||||
hasTimeout,
|
||||
timeoutAddress,
|
||||
timeoutUsec);
|
||||
}
|
||||
|
||||
private static int WaitSemaphoreOnHostThread(
|
||||
|
|
@ -231,46 +259,62 @@ public static class KernelSemaphoreCompatExports
|
|||
KernelSemaphoreState semaphore,
|
||||
uint handle,
|
||||
int needCount,
|
||||
bool hasTimeout,
|
||||
ulong timeoutAddress,
|
||||
uint timeoutUsec)
|
||||
{
|
||||
var deadlineMs = timeoutAddress != 0
|
||||
var deadlineMs = hasTimeout
|
||||
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
|
||||
: long.MaxValue;
|
||||
lock (semaphore.Gate)
|
||||
if (_traceSema)
|
||||
{
|
||||
if (_traceSema)
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
TraceSemaphore(
|
||||
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
|
||||
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
|
||||
$"count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} {FormatCallSite(ctx)}");
|
||||
}
|
||||
while (semaphore.Count < needCount)
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore(
|
||||
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
|
||||
}
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var remaining = deadlineMs - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
if (hasTimeout && remaining <= 0)
|
||||
{
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
|
||||
var waitMilliseconds = hasTimeout
|
||||
? (int)Math.Max(1L, Math.Min(remaining, 10L))
|
||||
: 10;
|
||||
Monitor.Wait(semaphore.Gate, waitMilliseconds);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore(
|
||||
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
|
||||
}
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,7 +372,7 @@ public static class KernelSemaphoreCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (signalCount <= 0)
|
||||
if (signalCount < 0)
|
||||
{
|
||||
signalTrace.Record(SemaphoreSignalProfile.Stage.Rejected,
|
||||
result: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
|
|
@ -532,6 +576,35 @@ public static class KernelSemaphoreCompatExports
|
|||
: result;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fjN6NQHhK8k",
|
||||
ExportName = "scePthreadSemTimedwait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSemTimedWait(CpuContext ctx)
|
||||
{
|
||||
var timeoutUsec = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
return WaitSemaphoreCore(
|
||||
ctx,
|
||||
semaphore,
|
||||
handle,
|
||||
needCount: 1,
|
||||
hasTimeout: true,
|
||||
timeoutAddress: 0,
|
||||
timeoutUsec,
|
||||
"scePthreadSemTimedwait");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w5IHyvahg-o",
|
||||
ExportName = "sem_timedwait",
|
||||
|
|
|
|||
|
|
@ -10,23 +10,44 @@ namespace SharpEmu.Libs.Kernel;
|
|||
// Wake selects a fixed number of registered waiters at the same address.
|
||||
public static class KernelSyncOnAddressCompatExports
|
||||
{
|
||||
private const int UmtxOperationWait = 2;
|
||||
private const int UmtxOperationWake = 3;
|
||||
private const int PosixEperm = 1;
|
||||
private const int PosixEacces = 13;
|
||||
private const int PosixEfault = 14;
|
||||
private const int PosixEinval = 22;
|
||||
private const int PosixEagain = 35;
|
||||
private const int PosixEtimedout = 60;
|
||||
private const int PosixEcanceled = 85;
|
||||
private const int PosixEother = 1062;
|
||||
|
||||
private sealed class SyncWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
private readonly CpuContext _context;
|
||||
private readonly ulong _expected;
|
||||
private readonly bool _is64Bit;
|
||||
private readonly bool _usePosixResult;
|
||||
private readonly long _deadlineTimestamp;
|
||||
private readonly object _hostGate = new();
|
||||
private readonly long _registeredTicks;
|
||||
private readonly int _registeredManagedThread;
|
||||
private int _wakeRequested;
|
||||
private int _resumeRecorded;
|
||||
|
||||
public SyncWaiter(CpuContext context, ulong address, ulong expected, bool is64Bit)
|
||||
public SyncWaiter(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
ulong expected,
|
||||
bool is64Bit,
|
||||
bool usePosixResult,
|
||||
long deadlineTimestamp)
|
||||
{
|
||||
_context = context;
|
||||
Address = address;
|
||||
_expected = expected;
|
||||
_is64Bit = is64Bit;
|
||||
_usePosixResult = usePosixResult;
|
||||
_deadlineTimestamp = deadlineTimestamp;
|
||||
_registeredTicks = KernelSyncOnAddressProfile.Enabled
|
||||
? Stopwatch.GetTimestamp()
|
||||
: 0L;
|
||||
|
|
@ -50,13 +71,13 @@ public static class KernelSyncOnAddressCompatExports
|
|||
if (Volatile.Read(ref _wakeRequested) != 0)
|
||||
{
|
||||
RecordResume(explicitWake: true, valueChanged: false, timedOut: false, faulted: false);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return Complete((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (!TryReadValue(_context, Address, _is64Bit, out var value))
|
||||
{
|
||||
RecordResume(explicitWake: false, valueChanged: false, timedOut: false, faulted: true);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return Complete((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var valueChanged = value != _expected;
|
||||
|
|
@ -65,15 +86,21 @@ public static class KernelSyncOnAddressCompatExports
|
|||
valueChanged,
|
||||
timedOut: !valueChanged,
|
||||
faulted: false);
|
||||
return valueChanged
|
||||
var result = valueChanged
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
return Complete(result);
|
||||
}
|
||||
|
||||
public bool TryWake() => Volatile.Read(ref _wakeRequested) != 0;
|
||||
|
||||
public bool RequestWake()
|
||||
{
|
||||
if (_deadlineTimestamp != 0 && Stopwatch.GetTimestamp() >= _deadlineTimestamp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Interlocked.Exchange(ref _wakeRequested, 1) != 0)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -87,42 +114,50 @@ public static class KernelSyncOnAddressCompatExports
|
|||
return true;
|
||||
}
|
||||
|
||||
public int WaitOnHost(TimeSpan? timeout)
|
||||
public int WaitOnHost()
|
||||
{
|
||||
if (Volatile.Read(ref _wakeRequested) == 0)
|
||||
while (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
lock (_hostGate)
|
||||
if (!TryReadValue(_context, Address, _is64Bit, out var current) ||
|
||||
current != _expected)
|
||||
{
|
||||
if (timeout is null)
|
||||
{
|
||||
while (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
Monitor.Wait(_hostGate);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var deadline = Stopwatch.GetTimestamp() +
|
||||
(long)Math.Ceiling(timeout.Value.TotalSeconds * Stopwatch.Frequency);
|
||||
while (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
var remainingTicks = deadline - Stopwatch.GetTimestamp();
|
||||
if (remainingTicks <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var remaining = TimeSpan.FromSeconds(
|
||||
remainingTicks / (double)Stopwatch.Frequency);
|
||||
Monitor.Wait(_hostGate, remaining);
|
||||
}
|
||||
var waitSlice = TimeSpan.FromMilliseconds(10);
|
||||
if (_deadlineTimestamp != 0)
|
||||
{
|
||||
var remainingTicks = _deadlineTimestamp - Stopwatch.GetTimestamp();
|
||||
if (remainingTicks <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var remainingSeconds = remainingTicks / (double)Stopwatch.Frequency;
|
||||
if (remainingSeconds < waitSlice.TotalSeconds)
|
||||
{
|
||||
waitSlice = TimeSpan.FromSeconds(remainingSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_hostGate)
|
||||
{
|
||||
if (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
Monitor.Wait(_hostGate, waitSlice);
|
||||
}
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(_context);
|
||||
}
|
||||
|
||||
return Resume();
|
||||
}
|
||||
|
||||
private int Complete(int result) => _usePosixResult
|
||||
? SetPosixReturn(_context, result)
|
||||
: result;
|
||||
|
||||
private void RecordResume(
|
||||
bool explicitWake,
|
||||
bool valueChanged,
|
||||
|
|
@ -171,6 +206,80 @@ public static class KernelSyncOnAddressCompatExports
|
|||
LibraryName = "libKernel")]
|
||||
public static int SyncOnAddressWait64(CpuContext ctx) => Wait(ctx, is64Bit: true);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "04AjkP0jO9U",
|
||||
ExportName = "_umtx_op",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int UmtxOp(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var operation = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var value = ctx[CpuRegister.Rdx];
|
||||
var secondaryAddress = ctx[CpuRegister.Rcx];
|
||||
var timeoutAddress = ctx[CpuRegister.R8];
|
||||
|
||||
if (secondaryAddress != 0)
|
||||
{
|
||||
return SetPosixReturn(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case UmtxOperationWait:
|
||||
{
|
||||
TimeSpan? timeout = null;
|
||||
long deadline = 0;
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(timeoutAddress, out var secondsBits) ||
|
||||
!ctx.TryReadUInt64(timeoutAddress + sizeof(long), out var nanosecondsBits))
|
||||
{
|
||||
return SetPosixReturn(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var seconds = unchecked((long)secondsBits);
|
||||
var nanoseconds = unchecked((long)nanosecondsBits);
|
||||
if (seconds < 0 || nanoseconds < 0 || nanoseconds >= 1_000_000_000L)
|
||||
{
|
||||
return SetPosixReturn(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
timeout = CreateTimespecDuration(seconds, nanoseconds);
|
||||
deadline = GuestThreadExecution.ComputeDeadlineTimestamp(timeout.Value);
|
||||
}
|
||||
|
||||
return WaitCore(
|
||||
ctx,
|
||||
is64Bit: true,
|
||||
address,
|
||||
value,
|
||||
timeout,
|
||||
deadline,
|
||||
usePosixResult: true,
|
||||
reason: "_umtx_op.wait");
|
||||
}
|
||||
|
||||
case UmtxOperationWake:
|
||||
return Wake(
|
||||
ctx,
|
||||
address,
|
||||
unchecked((int)value),
|
||||
usePosixResult: true);
|
||||
|
||||
default:
|
||||
return SetPosixReturn(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "q2y-wDIVWZA",
|
||||
ExportName = "sceKernelSyncOnAddressWake",
|
||||
|
|
@ -180,14 +289,29 @@ public static class KernelSyncOnAddressCompatExports
|
|||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var requested = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
return Wake(ctx, address, requested, usePosixResult: false);
|
||||
}
|
||||
|
||||
private static int Wake(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
int requested,
|
||||
bool usePosixResult)
|
||||
{
|
||||
if (address == 0 || (address & 3) != 0 || requested < 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
if (requested == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
List<SyncWaiter>? selected = null;
|
||||
|
|
@ -259,7 +383,10 @@ public static class KernelSyncOnAddressCompatExports
|
|||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
Environment.CurrentManagedThreadId);
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
private static void CaptureCallStack(
|
||||
|
|
@ -304,15 +431,15 @@ public static class KernelSyncOnAddressCompatExports
|
|||
private static int Wait(CpuContext ctx, bool is64Bit)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var expected = is64Bit
|
||||
? ctx[CpuRegister.Rsi]
|
||||
: unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var alignmentMask = is64Bit ? 7UL : 3UL;
|
||||
if (address == 0 || (address & alignmentMask) != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var expected = is64Bit
|
||||
? ctx[CpuRegister.Rsi]
|
||||
: unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
if (!TryReadValue(ctx, address, is64Bit, out var current))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -334,28 +461,92 @@ public static class KernelSyncOnAddressCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (timeoutMicroseconds == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
timeout = TimeSpan.FromTicks((long)timeoutMicroseconds * 10L);
|
||||
deadline = GuestThreadExecution.ComputeDeadlineTimestamp(timeout.Value);
|
||||
}
|
||||
|
||||
var waiter = new SyncWaiter(ctx, address, expected, is64Bit);
|
||||
return WaitCore(
|
||||
ctx,
|
||||
is64Bit,
|
||||
address,
|
||||
expected,
|
||||
timeout,
|
||||
deadline,
|
||||
usePosixResult: false,
|
||||
reason: is64Bit
|
||||
? "sceKernelSyncOnAddressWait64"
|
||||
: "sceKernelSyncOnAddressWait32");
|
||||
}
|
||||
|
||||
private static int WaitCore(
|
||||
CpuContext ctx,
|
||||
bool is64Bit,
|
||||
ulong address,
|
||||
ulong expected,
|
||||
TimeSpan? timeout,
|
||||
long deadline,
|
||||
bool usePosixResult,
|
||||
string reason)
|
||||
{
|
||||
var alignmentMask = is64Bit ? 7UL : 3UL;
|
||||
if (address == 0 || (address & alignmentMask) != 0)
|
||||
{
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
if (!TryReadValue(ctx, address, is64Bit, out var current))
|
||||
{
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
if (current != expected)
|
||||
{
|
||||
KernelSyncOnAddressProfile.RecordImmediate(address);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
if (timeout == TimeSpan.Zero)
|
||||
{
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
var waiter = new SyncWaiter(
|
||||
ctx,
|
||||
address,
|
||||
expected,
|
||||
is64Bit,
|
||||
usePosixResult,
|
||||
deadline);
|
||||
Register(address, waiter);
|
||||
if (!TryReadValue(ctx, address, is64Bit, out current))
|
||||
{
|
||||
Unregister(waiter);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
if (current != expected || waiter.TryWake())
|
||||
{
|
||||
Unregister(waiter);
|
||||
KernelSyncOnAddressProfile.RecordImmediate(address);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
}
|
||||
|
||||
var returnRip = 0UL;
|
||||
|
|
@ -383,7 +574,7 @@ public static class KernelSyncOnAddressCompatExports
|
|||
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
is64Bit ? "sceKernelSyncOnAddressWait64" : "sceKernelSyncOnAddressWait32",
|
||||
reason,
|
||||
waiter.WakeKey,
|
||||
waiter,
|
||||
deadline))
|
||||
|
|
@ -393,7 +584,24 @@ public static class KernelSyncOnAddressCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, waiter.WaitOnHost(timeout));
|
||||
return SetReturn(ctx, waiter.WaitOnHost());
|
||||
}
|
||||
|
||||
private static TimeSpan CreateTimespecDuration(long seconds, long nanoseconds)
|
||||
{
|
||||
var subsecondTicks = (nanoseconds + 99L) / 100L;
|
||||
if (seconds > TimeSpan.MaxValue.Ticks / TimeSpan.TicksPerSecond)
|
||||
{
|
||||
return TimeSpan.MaxValue;
|
||||
}
|
||||
|
||||
var secondTicks = seconds * TimeSpan.TicksPerSecond;
|
||||
if (secondTicks > TimeSpan.MaxValue.Ticks - subsecondTicks)
|
||||
{
|
||||
return TimeSpan.MaxValue;
|
||||
}
|
||||
|
||||
return TimeSpan.FromTicks(secondTicks + subsecondTicks);
|
||||
}
|
||||
|
||||
private static void Register(ulong address, SyncWaiter waiter)
|
||||
|
|
@ -458,4 +666,31 @@ public static class KernelSyncOnAddressCompatExports
|
|||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int CompleteResult(CpuContext ctx, int result, bool usePosixResult) =>
|
||||
usePosixResult
|
||||
? SetPosixReturn(ctx, result)
|
||||
: SetReturn(ctx, result);
|
||||
|
||||
private static int SetPosixReturn(CpuContext ctx, int result)
|
||||
{
|
||||
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
var errno = result switch
|
||||
{
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => PosixEperm,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => PosixEacces,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => PosixEfault,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => PosixEinval,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => PosixEagain,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => PosixEtimedout,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => PosixEcanceled,
|
||||
_ => PosixEother,
|
||||
};
|
||||
_ = KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
||||
return SetReturn(ctx, -1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
/// <summary>
|
||||
/// Process-argument accessors exported by libKernel for the platform libc.
|
||||
/// </summary>
|
||||
public static class LibcProcessArgumentExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "iKJMWrAumPE",
|
||||
ExportName = "getargc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int GetArgc(CpuContext ctx) =>
|
||||
ctx.SetReturn(HleDataSymbols.ProcessArgumentCount);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FJmglmTMdr4",
|
||||
ExportName = "getargv",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int GetArgv(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = HleDataSymbols.ProcessArgumentVectorAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,20 @@ public static class LibcStdioExports
|
|||
private static readonly object _ctypeTableGate = new();
|
||||
private static nint _ctypeTableBase;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vZkmJmvqueY",
|
||||
ExportName = "_Lockfilelock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int LockFileLock(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0x7rx8TKy2Y",
|
||||
ExportName = "_Unlockfilelock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int UnlockFileLock(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xeYO4u7uyJ0",
|
||||
ExportName = "fopen",
|
||||
|
|
@ -133,7 +147,7 @@ public static class LibcStdioExports
|
|||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
|
||||
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1049,7 +1063,7 @@ public static class LibcStdioExports
|
|||
}
|
||||
}
|
||||
|
||||
var replacement = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
var replacement = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
|
||||
lock (file.Gate)
|
||||
{
|
||||
if (file.IsClosed)
|
||||
|
|
|
|||
|
|
@ -12,29 +12,82 @@ public static class Http2Exports
|
|||
private const int Http2ErrorInvalidVersion = unchecked((int)0x817B106A);
|
||||
private const int Http2ErrorInvalidId = unchecked((int)0x817B1100);
|
||||
private const int Http2ErrorInvalidValue = unchecked((int)0x817B11FE);
|
||||
private const int Http2ErrorNullPointer = unchecked((int)0x817B1225);
|
||||
private const int Http2ErrorProhibited = unchecked((int)0x817B5224);
|
||||
|
||||
private static readonly ConcurrentDictionary<int, Http2Context> Contexts = new();
|
||||
private static readonly ConcurrentDictionary<int, Http2Template> Templates = new();
|
||||
private static readonly ConcurrentDictionary<int, Http2Request> Requests = new();
|
||||
private static readonly ConcurrentDictionary<int, Http2CookieBox> CookieBoxes = new();
|
||||
private static int _nextContextId;
|
||||
private static int _nextTemplateId = 0x1000;
|
||||
private static int _nextRequestId = 0x2000;
|
||||
private static int _nextCookieBoxId = 0x3000;
|
||||
|
||||
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
|
||||
|
||||
private sealed record Http2Template(int ContextId, string UserAgent, int HttpVersion, bool AutoProxyConfig);
|
||||
private sealed record Http2CookieBox;
|
||||
|
||||
private sealed record Http2Request(int TemplateId, string Method, string Url, ulong ContentLength);
|
||||
private sealed record Http2Options(
|
||||
uint MinimumSslVersion,
|
||||
uint SslOptions,
|
||||
ulong RedirectCallback,
|
||||
ulong RedirectUserArgument,
|
||||
bool AutoRedirect,
|
||||
int CookieBoxId,
|
||||
uint ConnectTimeoutMicroseconds,
|
||||
uint ReceiveTimeoutMicroseconds,
|
||||
uint SendTimeoutMicroseconds,
|
||||
bool AuthEnabled,
|
||||
ulong SslCallback,
|
||||
ulong SslUserArgument)
|
||||
{
|
||||
public static readonly Http2Options Default = new(0, 0, 0, 0, true, 0, 0, 0, 0, false, 0, 0);
|
||||
}
|
||||
|
||||
private sealed record Http2Header(string Name, string Value, uint Mode);
|
||||
|
||||
private sealed record Http2Template(
|
||||
int ContextId,
|
||||
string UserAgent,
|
||||
int HttpVersion,
|
||||
bool AutoProxyConfig,
|
||||
Http2Options Options);
|
||||
|
||||
private sealed record Http2Request(
|
||||
int TemplateId,
|
||||
string Method,
|
||||
string Url,
|
||||
ulong ContentLength,
|
||||
Http2Options Options,
|
||||
Http2Header[] Headers);
|
||||
|
||||
internal readonly record struct Http2OptionsSnapshot(
|
||||
uint MinimumSslVersion,
|
||||
uint SslOptions,
|
||||
ulong RedirectCallback,
|
||||
ulong RedirectUserArgument,
|
||||
bool AutoRedirect,
|
||||
int CookieBoxId,
|
||||
uint ConnectTimeoutMicroseconds,
|
||||
uint ReceiveTimeoutMicroseconds,
|
||||
uint SendTimeoutMicroseconds,
|
||||
bool AuthEnabled,
|
||||
ulong SslCallback,
|
||||
ulong SslUserArgument);
|
||||
|
||||
internal readonly record struct Http2HeaderSnapshot(string Name, string Value, uint Mode);
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Templates.Clear();
|
||||
Requests.Clear();
|
||||
CookieBoxes.Clear();
|
||||
_nextContextId = 0;
|
||||
_nextTemplateId = 0x1000;
|
||||
_nextRequestId = 0x2000;
|
||||
_nextCookieBoxId = 0x3000;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -119,7 +172,12 @@ public static class Http2Exports
|
|||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextTemplateId);
|
||||
Templates[id] = new Http2Template(contextId, userAgent, httpVersion, autoProxyConfig != 0);
|
||||
Templates[id] = new Http2Template(
|
||||
contextId,
|
||||
userAgent,
|
||||
httpVersion,
|
||||
autoProxyConfig != 0,
|
||||
Http2Options.Default);
|
||||
TraceHttp2("create_template", id, unchecked((ulong)contextId), ctx[CpuRegister.Rsi], unchecked((ulong)httpVersion), unchecked((ulong)autoProxyConfig));
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
|
@ -140,6 +198,175 @@ public static class Http2Exports
|
|||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "09tk+kIA1Ns",
|
||||
ExportName = "sceHttp2SetMinSslVersion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetMinSslVersion(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var minimumSslVersion = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
|
||||
return TryUpdateOptions(id, options => options with { MinimumSslVersion = minimumSslVersion })
|
||||
? TraceAndReturn(ctx, "set_min_ssl_version", id, minimumSslVersion, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "B37SruheQ5Y",
|
||||
ExportName = "sceHttp2SslDisableOption",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SslDisableOption(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions & ~flags })
|
||||
? TraceAndReturn(ctx, "ssl_disable_option", id, flags, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EWcwMpbr5F8",
|
||||
ExportName = "sceHttp2SslEnableOption",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SslEnableOption(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions | flags })
|
||||
? TraceAndReturn(ctx, "ssl_enable_option", id, flags, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "BJgi0CH7al4",
|
||||
ExportName = "sceHttp2SetRedirectCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetRedirectCallback(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var callback = ctx[CpuRegister.Rsi];
|
||||
var userArgument = ctx[CpuRegister.Rdx];
|
||||
return TryUpdateOptions(
|
||||
id,
|
||||
options => options with
|
||||
{
|
||||
RedirectCallback = callback,
|
||||
RedirectUserArgument = userArgument,
|
||||
})
|
||||
? TraceAndReturn(ctx, "set_redirect_callback", id, callback, userArgument, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b9AvoIaOuHI",
|
||||
ExportName = "sceHttp2SetAutoRedirect",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetAutoRedirect(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
|
||||
return TryUpdateOptions(id, options => options with { AutoRedirect = enabled })
|
||||
? TraceAndReturn(ctx, "set_auto_redirect", id, enabled ? 1UL : 0UL, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "N4UfjvWJsMw",
|
||||
ExportName = "sceHttp2CreateCookieBox",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2CreateCookieBox(CpuContext ctx)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextCookieBoxId);
|
||||
CookieBoxes[id] = new Http2CookieBox();
|
||||
TraceHttp2("create_cookie_box", id, 0, 0, 0, 0);
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jrVHsKCXA0g",
|
||||
ExportName = "sceHttp2SetCookieBox",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetCookieBox(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var cookieBoxId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (cookieBoxId != 0 && !CookieBoxes.ContainsKey(cookieBoxId))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
return TryUpdateOptions(id, options => options with { CookieBoxId = cookieBoxId })
|
||||
? TraceAndReturn(ctx, "set_cookie_box", id, unchecked((ulong)cookieBoxId), 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-HIO4VT87v8",
|
||||
ExportName = "sceHttp2SetConnectTimeOut",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetConnectTimeOut(CpuContext ctx) =>
|
||||
SetTimeout(ctx, static (options, value) => options with { ConnectTimeoutMicroseconds = value }, "connect");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "izvHhqgDt44",
|
||||
ExportName = "sceHttp2SetRecvTimeOut",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetRecvTimeOut(CpuContext ctx) =>
|
||||
SetTimeout(ctx, static (options, value) => options with { ReceiveTimeoutMicroseconds = value }, "recv");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XPtW45xiLHk",
|
||||
ExportName = "sceHttp2SetSendTimeOut",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetSendTimeOut(CpuContext ctx) =>
|
||||
SetTimeout(ctx, static (options, value) => options with { SendTimeoutMicroseconds = value }, "send");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jjFahkBPCYs",
|
||||
ExportName = "sceHttp2SetAuthEnabled",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetAuthEnabled(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
|
||||
return TryUpdateOptions(id, options => options with { AuthEnabled = enabled })
|
||||
? TraceAndReturn(ctx, "set_auth_enabled", id, enabled ? 1UL : 0UL, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YrWX+DhPHQY",
|
||||
ExportName = "sceHttp2SetSslCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetSslCallback(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var callback = ctx[CpuRegister.Rsi];
|
||||
var userArgument = ctx[CpuRegister.Rdx];
|
||||
return TryUpdateOptions(
|
||||
id,
|
||||
options => options with
|
||||
{
|
||||
SslCallback = callback,
|
||||
SslUserArgument = userArgument,
|
||||
})
|
||||
? TraceAndReturn(ctx, "set_ssl_callback", id, callback, userArgument, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceHttp2CreateRequestWithURL",
|
||||
Target = Generation.Gen5,
|
||||
|
|
@ -147,7 +374,7 @@ public static class Http2Exports
|
|||
public static int Http2CreateRequestWithUrl(CpuContext ctx)
|
||||
{
|
||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Templates.ContainsKey(templateId))
|
||||
if (!Templates.TryGetValue(templateId, out var template))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
|
@ -159,11 +386,73 @@ public static class Http2Exports
|
|||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
Requests[id] = new Http2Request(templateId, method, url, ctx[CpuRegister.Rcx]);
|
||||
Requests[id] = new Http2Request(
|
||||
templateId,
|
||||
method,
|
||||
url,
|
||||
ctx[CpuRegister.Rcx],
|
||||
template.Options,
|
||||
[]);
|
||||
TraceHttp2("create_request", id, unchecked((ulong)templateId), ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nrPfOE8TQu0",
|
||||
ExportName = "sceHttp2AddRequestHeader",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2AddRequestHeader(CpuContext ctx)
|
||||
{
|
||||
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var valueAddress = ctx[CpuRegister.Rdx];
|
||||
var mode = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||
if (nameAddress == 0 || valueAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorNullPointer);
|
||||
}
|
||||
|
||||
if (!TryReadUtf8Z(ctx, nameAddress, 4096, out var name) ||
|
||||
!TryReadUtf8Z(ctx, valueAddress, 16384, out var value))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidValue);
|
||||
}
|
||||
|
||||
while (Requests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
var headers = new Http2Header[request.Headers.Length + 1];
|
||||
Array.Copy(request.Headers, headers, request.Headers.Length);
|
||||
headers[^1] = new Http2Header(name, value, mode);
|
||||
if (Requests.TryUpdate(requestId, request with { Headers = headers }, request))
|
||||
{
|
||||
return TraceAndReturn(ctx, "add_request_header", requestId, nameAddress, valueAddress, mode, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FSAFOzi0FpM",
|
||||
ExportName = "sceHttp2SetRequestContentLength",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetRequestContentLength(CpuContext ctx)
|
||||
{
|
||||
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var contentLength = ctx[CpuRegister.Rsi];
|
||||
while (Requests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
if (Requests.TryUpdate(requestId, request with { ContentLength = contentLength }, request))
|
||||
{
|
||||
return TraceAndReturn(ctx, "set_request_content_length", requestId, contentLength, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceHttp2DeleteRequest",
|
||||
Target = Generation.Gen5,
|
||||
|
|
@ -202,6 +491,100 @@ public static class Http2Exports
|
|||
}
|
||||
}
|
||||
|
||||
private static int SetTimeout(
|
||||
CpuContext ctx,
|
||||
Func<Http2Options, uint, Http2Options> update,
|
||||
string timeoutName)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var microseconds = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
return TryUpdateOptions(id, options => update(options, microseconds))
|
||||
? TraceAndReturn(ctx, $"set_{timeoutName}_timeout", id, microseconds, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
private static bool TryUpdateOptions(int id, Func<Http2Options, Http2Options> update)
|
||||
{
|
||||
while (Templates.TryGetValue(id, out var template))
|
||||
{
|
||||
if (Templates.TryUpdate(id, template with { Options = update(template.Options) }, template))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
while (Requests.TryGetValue(id, out var request))
|
||||
{
|
||||
if (Requests.TryUpdate(id, request with { Options = update(request.Options) }, request))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryGetOptionsForTests(int id, out Http2OptionsSnapshot snapshot)
|
||||
{
|
||||
var options = Templates.TryGetValue(id, out var template)
|
||||
? template.Options
|
||||
: Requests.TryGetValue(id, out var request)
|
||||
? request.Options
|
||||
: null;
|
||||
if (options is null)
|
||||
{
|
||||
snapshot = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = new Http2OptionsSnapshot(
|
||||
options.MinimumSslVersion,
|
||||
options.SslOptions,
|
||||
options.RedirectCallback,
|
||||
options.RedirectUserArgument,
|
||||
options.AutoRedirect,
|
||||
options.CookieBoxId,
|
||||
options.ConnectTimeoutMicroseconds,
|
||||
options.ReceiveTimeoutMicroseconds,
|
||||
options.SendTimeoutMicroseconds,
|
||||
options.AuthEnabled,
|
||||
options.SslCallback,
|
||||
options.SslUserArgument);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static Http2HeaderSnapshot[] GetRequestHeadersForTests(int requestId) =>
|
||||
Requests.TryGetValue(requestId, out var request)
|
||||
? request.Headers
|
||||
.Select(static header => new Http2HeaderSnapshot(header.Name, header.Value, header.Mode))
|
||||
.ToArray()
|
||||
: [];
|
||||
|
||||
internal static bool TryGetRequestContentLengthForTests(int requestId, out ulong contentLength)
|
||||
{
|
||||
if (Requests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
contentLength = request.ContentLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
contentLength = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int TraceAndReturn(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
int id,
|
||||
ulong arg0,
|
||||
ulong arg1,
|
||||
ulong arg2,
|
||||
ulong arg3)
|
||||
{
|
||||
TraceHttp2(operation, id, arg0, arg1, arg2, arg3);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
|
|
|
|||
|
|
@ -14,28 +14,42 @@ namespace SharpEmu.Libs.Network;
|
|||
|
||||
public static class NetExports
|
||||
{
|
||||
private const int NetErrorNoEntry = unchecked((int)0x80410102);
|
||||
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
||||
private const int NetErrorFault = unchecked((int)0x8041010E);
|
||||
private const int NetErrorAlreadyExists = unchecked((int)0x80410111);
|
||||
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
||||
private const int NetErrorTooManyFiles = unchecked((int)0x80410118);
|
||||
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
|
||||
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
|
||||
private const int NetErrorTooManyOpenFiles = unchecked((int)0x80410118);
|
||||
private const int NetErrorResolverNoDns = unchecked((int)0x804101E1);
|
||||
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
|
||||
private const int NetErrnoNoEntry = 2;
|
||||
private const int NetErrnoBadFileDescriptor = 9;
|
||||
private const int NetErrnoFault = 14;
|
||||
private const int NetErrnoAlreadyExists = 17;
|
||||
private const int NetErrnoInvalidArgument = 22;
|
||||
private const int NetErrnoTooManyFiles = 24;
|
||||
private const int NetErrnoWouldBlock = 35;
|
||||
private const int NetErrnoAddressInUse = 48;
|
||||
private const int NetErrnoTooManyOpenFiles = 24;
|
||||
private const int NetErrnoNotInitialized = 200;
|
||||
private const int NetErrnoResolverNoDns = 225;
|
||||
private const int MaxNameLength = 256;
|
||||
private const int EpollIdBase = 1024;
|
||||
private const int EpollCapacity = 32;
|
||||
private const int NetEpollEventSize = 24;
|
||||
private const uint NetEpollIn = 0x00000001;
|
||||
private const uint NetEpollOut = 0x00000002;
|
||||
private const uint NetEpollError = 0x00000008;
|
||||
private static ReadOnlySpan<byte> OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
|
||||
|
||||
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
||||
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
||||
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
|
||||
private static readonly object EpollGate = new();
|
||||
private static readonly Dictionary<int, NetEpollContext> _epolls = new();
|
||||
private static int _nextPoolId;
|
||||
private static int _nextResolverId = 0x2000;
|
||||
private static int _nextSocketId = 255;
|
||||
|
|
@ -52,6 +66,26 @@ public static class NetExports
|
|||
|
||||
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
|
||||
|
||||
private sealed class NetEpollContext(string name)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
|
||||
public List<NetEpollRegistration> Registrations { get; } = [];
|
||||
}
|
||||
|
||||
private readonly record struct NetEpollRegistration(int SocketId, NetEpollEvent Event);
|
||||
|
||||
private readonly record struct NetEpollEvent(uint Events, ulong Data);
|
||||
|
||||
internal static void ResetEpollsForTests()
|
||||
{
|
||||
lock (EpollGate)
|
||||
{
|
||||
_epolls.Clear();
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetReadEventState(
|
||||
int socketId,
|
||||
ulong lowWater,
|
||||
|
|
@ -116,6 +150,11 @@ public static class NetExports
|
|||
_initialized = false;
|
||||
_pools.Clear();
|
||||
_resolvers.Clear();
|
||||
lock (EpollGate)
|
||||
{
|
||||
_epolls.Clear();
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
foreach (var socket in _sockets.Values)
|
||||
{
|
||||
socket.Dispose();
|
||||
|
|
@ -195,6 +234,12 @@ public static class NetExports
|
|||
public static int NetSocketClose(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (KernelSocketCompatExports.TryCloseSocketFd(id))
|
||||
{
|
||||
TraceNet("socket.close.posix", id, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (!_sockets.TryRemove(id, out var socket))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
|
|
@ -715,6 +760,291 @@ public static class NetExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "SF47kB2MNTo",
|
||||
ExportName = "sceNetEpollCreate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollCreate(CpuContext ctx)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
|
||||
}
|
||||
|
||||
var nameAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (nameAddress == 0 || flags != 0 ||
|
||||
!TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var name))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
lock (EpollGate)
|
||||
{
|
||||
for (var index = 0; index < EpollCapacity; index++)
|
||||
{
|
||||
var id = EpollIdBase + index;
|
||||
if (_epolls.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_epolls.Add(id, new NetEpollContext(name));
|
||||
TraceNet("epoll.create", id, nameAddress, 0, 0);
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
}
|
||||
|
||||
return SetNetError(ctx, NetErrorTooManyOpenFiles, NetErrnoTooManyOpenFiles);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZVw46bsasAk",
|
||||
ExportName = "sceNetEpollControl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollControl(CpuContext ctx)
|
||||
{
|
||||
const int EpollControlAdd = 1;
|
||||
const int EpollControlModify = 2;
|
||||
const int EpollControlDelete = 3;
|
||||
|
||||
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var operation = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var socketId = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var eventAddress = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (operation is < EpollControlAdd or > EpollControlDelete ||
|
||||
(operation is EpollControlAdd or EpollControlModify && eventAddress == 0) ||
|
||||
(operation == EpollControlDelete && eventAddress != 0))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
if (!_sockets.ContainsKey(socketId))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
NetEpollEvent requestedEvent = default;
|
||||
if (eventAddress != 0 && !TryReadNetEpollEvent(ctx, eventAddress, out requestedEvent))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
|
||||
}
|
||||
|
||||
lock (EpollGate)
|
||||
{
|
||||
if (!_epolls.TryGetValue(epollId, out var epoll))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
var registrationIndex = epoll.Registrations.FindIndex(value => value.SocketId == socketId);
|
||||
switch (operation)
|
||||
{
|
||||
case EpollControlAdd when registrationIndex >= 0:
|
||||
return SetNetError(ctx, NetErrorAlreadyExists, NetErrnoAlreadyExists);
|
||||
case EpollControlAdd:
|
||||
epoll.Registrations.Add(new NetEpollRegistration(socketId, requestedEvent));
|
||||
break;
|
||||
case EpollControlModify when registrationIndex < 0:
|
||||
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
|
||||
case EpollControlModify:
|
||||
epoll.Registrations[registrationIndex] =
|
||||
new NetEpollRegistration(socketId, requestedEvent);
|
||||
break;
|
||||
case EpollControlDelete when registrationIndex < 0:
|
||||
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
|
||||
case EpollControlDelete:
|
||||
epoll.Registrations.RemoveAt(registrationIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
|
||||
TraceNet(
|
||||
"epoll.control",
|
||||
epollId,
|
||||
unchecked((uint)operation),
|
||||
unchecked((uint)socketId),
|
||||
requestedEvent.Events);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "drjIbDbA7UQ",
|
||||
ExportName = "sceNetEpollWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollWait(CpuContext ctx)
|
||||
{
|
||||
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var eventsAddress = ctx[CpuRegister.Rsi];
|
||||
var maxEvents = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var timeoutMicroseconds = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||
|
||||
if (eventsAddress == 0)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
|
||||
}
|
||||
|
||||
if (maxEvents <= 0 || timeoutMicroseconds < -1)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
NetEpollRegistration[] registrations;
|
||||
lock (EpollGate)
|
||||
{
|
||||
if (!_epolls.TryGetValue(epollId, out var epoll))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
if (epoll.Registrations.Count == 0 && timeoutMicroseconds != 0)
|
||||
{
|
||||
if (timeoutMicroseconds < 0)
|
||||
{
|
||||
while (_epolls.TryGetValue(epollId, out epoll) && epoll.Registrations.Count == 0)
|
||||
{
|
||||
Monitor.Wait(EpollGate);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Monitor.Wait(
|
||||
EpollGate,
|
||||
TimeSpan.FromTicks((long)timeoutMicroseconds * TimeSpan.TicksPerMicrosecond));
|
||||
}
|
||||
|
||||
if (!_epolls.TryGetValue(epollId, out epoll))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
registrations = epoll.Registrations.ToArray();
|
||||
}
|
||||
|
||||
if (registrations.Length == 0)
|
||||
{
|
||||
TraceNet("epoll.wait", epollId, unchecked((uint)maxEvents), unchecked((uint)timeoutMicroseconds), 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
var readSockets = new List<Socket>();
|
||||
var writeSockets = new List<Socket>();
|
||||
var errorSockets = new List<Socket>();
|
||||
var hostRegistrations = new Dictionary<Socket, NetEpollRegistration>();
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
if (!_sockets.TryGetValue(registration.SocketId, out var socket))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hostRegistrations[socket] = registration;
|
||||
if ((registration.Event.Events & NetEpollIn) != 0)
|
||||
{
|
||||
readSockets.Add(socket);
|
||||
}
|
||||
if ((registration.Event.Events & NetEpollOut) != 0)
|
||||
{
|
||||
writeSockets.Add(socket);
|
||||
}
|
||||
errorSockets.Add(socket);
|
||||
}
|
||||
|
||||
if (hostRegistrations.Count == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Socket.Select(readSockets, writeSockets, errorSockets, timeoutMicroseconds);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
var readyCount = 0;
|
||||
foreach (var (socket, registration) in hostRegistrations)
|
||||
{
|
||||
uint readyEvents = 0;
|
||||
if (readSockets.Contains(socket))
|
||||
{
|
||||
readyEvents |= NetEpollIn;
|
||||
}
|
||||
if (writeSockets.Contains(socket))
|
||||
{
|
||||
readyEvents |= NetEpollOut;
|
||||
}
|
||||
if (errorSockets.Contains(socket))
|
||||
{
|
||||
readyEvents |= NetEpollError;
|
||||
}
|
||||
if (readyEvents == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputAddress = eventsAddress + (ulong)(readyCount * NetEpollEventSize);
|
||||
if (!TryWriteNetEpollEvent(
|
||||
ctx,
|
||||
outputAddress,
|
||||
readyEvents,
|
||||
registration.SocketId,
|
||||
registration.Event.Data))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
|
||||
}
|
||||
|
||||
readyCount++;
|
||||
if (readyCount == maxEvents)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TraceNet(
|
||||
"epoll.wait",
|
||||
epollId,
|
||||
unchecked((uint)maxEvents),
|
||||
unchecked((uint)timeoutMicroseconds),
|
||||
unchecked((uint)readyCount));
|
||||
return ctx.SetReturn(readyCount);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Inp1lfL+Jdw",
|
||||
ExportName = "sceNetEpollDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollDestroy(CpuContext ctx)
|
||||
{
|
||||
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
lock (EpollGate)
|
||||
{
|
||||
if (!_epolls.Remove(epollId))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
|
||||
TraceNet("epoll.destroy", epollId, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dgJBaeJnGpo",
|
||||
ExportName = "sceNetPoolCreate",
|
||||
|
|
@ -816,6 +1146,53 @@ public static class NetExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v6M4txecCuo",
|
||||
ExportName = "sceNetEtherNtostr",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEtherNtostr(CpuContext ctx)
|
||||
{
|
||||
const int EtherStringLength = 18;
|
||||
const string HexDigits = "0123456789abcdef";
|
||||
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var destination = ctx[CpuRegister.Rsi];
|
||||
var destinationLength = ctx[CpuRegister.Rdx];
|
||||
if (address == 0 || destination == 0 || destinationLength < EtherStringLength)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> macAddress = stackalloc byte[6];
|
||||
if (!ctx.Memory.TryRead(address, macAddress))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> text = stackalloc byte[EtherStringLength];
|
||||
for (var index = 0; index < macAddress.Length; index++)
|
||||
{
|
||||
var value = macAddress[index];
|
||||
var offset = index * 3;
|
||||
text[offset] = unchecked((byte)HexDigits[value >> 4]);
|
||||
text[offset + 1] = unchecked((byte)HexDigits[value & 0xF]);
|
||||
if (index != macAddress.Length - 1)
|
||||
{
|
||||
text[offset + 2] = (byte)':';
|
||||
}
|
||||
}
|
||||
text[^1] = 0;
|
||||
|
||||
if (!ctx.Memory.TryWrite(destination, text))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
TraceNet("ether_ntostr", 0, address, destination, destinationLength);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C4UgDHHPvdw",
|
||||
ExportName = "sceNetResolverCreate",
|
||||
|
|
@ -1025,6 +1402,39 @@ public static class NetExports
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadNetEpollEvent(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
out NetEpollEvent epollEvent)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
epollEvent = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
epollEvent = new NetEpollEvent(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(bytes[16..]));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteNetEpollEvent(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
uint events,
|
||||
int socketId,
|
||||
ulong data)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
|
||||
bytes.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, events);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], unchecked((uint)socketId));
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes[16..], data);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Kcp5d-q1Uo",
|
||||
ExportName = "sceNetInetPton",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal reliable-UDP poll object support. SharpEmu does not expose a host
|
||||
/// RUDP transport yet, but callers still require a distinct non-zero poll ID
|
||||
/// for their offline/event-processing path.
|
||||
/// </summary>
|
||||
public static class RudpExports
|
||||
{
|
||||
private const int MaxLivePolls = 4096;
|
||||
private const ulong PollEventSize = 8;
|
||||
private const int RudpErrorInvalidArgument = unchecked((int)0x80770004);
|
||||
private const int RudpErrorInvalidPollId = unchecked((int)0x8077001F);
|
||||
private const int RudpErrorOutOfMemory = unchecked((int)0x80770003);
|
||||
private static readonly object PollGate = new();
|
||||
private static readonly Dictionary<int, uint> PollCapacities = [];
|
||||
private static int _nextPollId;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MVbmLASjn5M",
|
||||
ExportName = "sceRudpPollCreate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRudp")]
|
||||
public static int RudpPollCreate(CpuContext ctx)
|
||||
{
|
||||
var requestedEvents = ctx[CpuRegister.Rdi];
|
||||
if (requestedEvents is 0 or > uint.MaxValue)
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorInvalidArgument);
|
||||
}
|
||||
|
||||
lock (PollGate)
|
||||
{
|
||||
if (PollCapacities.Count >= MaxLivePolls)
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorOutOfMemory);
|
||||
}
|
||||
|
||||
int pollId;
|
||||
do
|
||||
{
|
||||
_nextPollId = _nextPollId == int.MaxValue ? 1 : _nextPollId + 1;
|
||||
pollId = _nextPollId;
|
||||
}
|
||||
while (PollCapacities.ContainsKey(pollId));
|
||||
|
||||
PollCapacities.Add(pollId, unchecked((uint)requestedEvents));
|
||||
return ctx.SetReturn(pollId);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "M6ggviwXpLs",
|
||||
ExportName = "sceRudpPollWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRudp")]
|
||||
public static int RudpPollWait(CpuContext ctx)
|
||||
{
|
||||
var pollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var events = ctx[CpuRegister.Rsi];
|
||||
var maxEvents = ctx[CpuRegister.Rdx];
|
||||
var timeoutMicroseconds = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (!TryGetPollCapacity(pollId, out _))
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorInvalidPollId);
|
||||
}
|
||||
|
||||
if (events == 0 ||
|
||||
maxEvents == 0 ||
|
||||
maxEvents > uint.MaxValue ||
|
||||
maxEvents > ulong.MaxValue / PollEventSize ||
|
||||
!ctx.Memory.CanRead(events, maxEvents * PollEventSize))
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorInvalidArgument);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
if (timeoutMicroseconds == 0 ||
|
||||
!GuestThreadExecution.IsGuestThread ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out _))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var timeout = timeoutMicroseconds > (ulong)(TimeSpan.MaxValue.Ticks / 10)
|
||||
? TimeSpan.MaxValue
|
||||
: TimeSpan.FromTicks(checked((long)timeoutMicroseconds * 10));
|
||||
_ = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceRudpPollWait",
|
||||
$"rudp-poll:{pollId}",
|
||||
waiter: null,
|
||||
blockDeadlineTimestamp: GuestThreadExecution.ComputeDeadlineTimestamp(timeout));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
lock (PollGate)
|
||||
{
|
||||
PollCapacities.Clear();
|
||||
_nextPollId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetPollCapacity(int pollId, out uint capacity)
|
||||
{
|
||||
lock (PollGate)
|
||||
{
|
||||
return PollCapacities.TryGetValue(pollId, out capacity);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
}
|
||||
|
|
@ -13,15 +13,22 @@ public static class NpAuthExports
|
|||
private const int NpAuthErrorAborted = unchecked((int)0x80550304);
|
||||
private const int NpAuthErrorRequestMaximum = unchecked((int)0x80550305);
|
||||
private const int NpAuthErrorRequestNotFound = unchecked((int)0x80550306);
|
||||
private const int NpAuthErrorServiceDown = unchecked((int)0x80550401);
|
||||
private const int NpAuthErrorInvalidArgument = unchecked((int)0x80550301);
|
||||
private const int NpErrorSignedOut = unchecked((int)0x80550006);
|
||||
private const int AuthorizationCodeSize = 136;
|
||||
private const int IdTokenSize = 4104;
|
||||
private const ulong AsyncParameterSize = 24;
|
||||
|
||||
private static readonly ConcurrentDictionary<int, AuthRequest> Requests = new();
|
||||
private static readonly object RequestGate = new();
|
||||
private static int _nextRequestId;
|
||||
|
||||
private sealed class AuthRequest
|
||||
private sealed class AuthRequest(bool async)
|
||||
{
|
||||
public bool Async { get; } = async;
|
||||
|
||||
public bool Completed { get; set; }
|
||||
|
||||
public bool Aborted { get; set; }
|
||||
|
||||
public int Result { get; set; }
|
||||
|
|
@ -47,7 +54,7 @@ public static class NpAuthExports
|
|||
ExportName = "sceNpAuthCreateRequest",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx);
|
||||
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx, async: false);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthCreateAsyncRequest",
|
||||
|
|
@ -63,7 +70,7 @@ public static class NpAuthExports
|
|||
return ctx.SetReturn(NpAuthErrorInvalidSize);
|
||||
}
|
||||
|
||||
return CreateRequest(ctx);
|
||||
return CreateRequest(ctx, async: true);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -134,21 +141,23 @@ public static class NpAuthExports
|
|||
ExportName = "sceNpAuthGetAuthorizationCodeV3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) =>
|
||||
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], AuthorizationCodeSize, ctx[CpuRegister.Rcx]);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthGetIdTokenV3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetIdTokenV3(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
public static int NpAuthGetIdTokenV3(CpuContext ctx) =>
|
||||
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], IdTokenSize, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthGetAuthorizedAppCode",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => CompleteSignedOut(ctx, 0, 0, 0);
|
||||
|
||||
private static int CreateRequest(CpuContext ctx)
|
||||
private static int CreateRequest(CpuContext ctx, bool async)
|
||||
{
|
||||
lock (RequestGate)
|
||||
{
|
||||
|
|
@ -160,7 +169,7 @@ public static class NpAuthExports
|
|||
while (true)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
if (Requests.TryAdd(id, new AuthRequest()))
|
||||
if (Requests.TryAdd(id, new AuthRequest(async)))
|
||||
{
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
|
@ -168,8 +177,20 @@ public static class NpAuthExports
|
|||
}
|
||||
}
|
||||
|
||||
private static int RejectOnlineOperation(CpuContext ctx)
|
||||
private static int CompleteSignedOut(CpuContext ctx, ulong outputAddress, int outputSize, ulong issuerAddress)
|
||||
{
|
||||
if (outputAddress != 0 && outputSize != 0)
|
||||
{
|
||||
Span<byte> cleared = stackalloc byte[outputSize];
|
||||
cleared.Clear();
|
||||
ctx.Memory.TryWrite(outputAddress, cleared);
|
||||
}
|
||||
|
||||
if (issuerAddress != 0)
|
||||
{
|
||||
ctx.TryWriteUInt32(issuerAddress, 0);
|
||||
}
|
||||
|
||||
if (!Requests.TryGetValue(unchecked((int)ctx[CpuRegister.Rdi]), out var request))
|
||||
{
|
||||
return ctx.SetReturn(NpAuthErrorRequestNotFound);
|
||||
|
|
@ -177,8 +198,21 @@ public static class NpAuthExports
|
|||
|
||||
lock (request)
|
||||
{
|
||||
request.Result = request.Aborted ? NpAuthErrorAborted : NpAuthErrorServiceDown;
|
||||
return ctx.SetReturn(request.Result);
|
||||
if (request.Aborted)
|
||||
{
|
||||
request.Result = NpAuthErrorAborted;
|
||||
return ctx.SetReturn(NpAuthErrorAborted);
|
||||
}
|
||||
|
||||
if (request.Completed)
|
||||
{
|
||||
request.Result = NpAuthErrorInvalidArgument;
|
||||
return ctx.SetReturn(NpAuthErrorInvalidArgument);
|
||||
}
|
||||
|
||||
request.Completed = true;
|
||||
request.Result = NpErrorSignedOut;
|
||||
return ctx.SetReturn(request.Async ? 0 : NpErrorSignedOut);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,26 @@ public static class NpEntitlementAccessExports
|
|||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "y7JD1gfwyfA",
|
||||
ExportName = "sceNpEntitlementAccessGetGameTrialsFlag",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessGetGameTrialsFlag(CpuContext ctx)
|
||||
{
|
||||
var flagAddress = ctx[CpuRegister.Rdi];
|
||||
if (flagAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NpEntitlementAccessErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> flagBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(flagBytes, 0);
|
||||
return ctx.Memory.TryWrite(flagAddress, flagBytes)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TFyU+KFBv54",
|
||||
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public static class NpUniversalDataSystemExports
|
|||
private const int ErrorInvalidArgument = unchecked((int)0x80553102);
|
||||
private const int MaximumGuestStringBytes = 64 * 1024;
|
||||
|
||||
public static void ResetRuntimeState() => NpUniversalDataSystemState.ResetRuntimeState();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sjaobBgqeB4",
|
||||
ExportName = "sceNpUniversalDataSystemInitialize",
|
||||
|
|
|
|||
|
|
@ -64,9 +64,14 @@ internal static class NpUniversalDataSystemState
|
|||
{
|
||||
lock (Gate)
|
||||
{
|
||||
ResetLocked();
|
||||
_initialized = true;
|
||||
if (!_initialized)
|
||||
{
|
||||
ResetLocked();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
_poolSize = poolSize;
|
||||
UpdateMaximumInUseLocked();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -85,8 +90,7 @@ internal static class NpUniversalDataSystemState
|
|||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
context = 0;
|
||||
return false;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
context = NextPositiveId(ref _nextContext);
|
||||
|
|
@ -480,7 +484,7 @@ internal static class NpUniversalDataSystemState
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
internal static void ResetRuntimeState()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
|
|
@ -488,6 +492,8 @@ internal static class NpUniversalDataSystemState
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
|
||||
private static ulong CreateObjectLocked(UdsObjectNode node)
|
||||
{
|
||||
var handle = ObjectHandleBase | ++_nextObject;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,27 @@ public static class NpWebApi2Exports
|
|||
private static int _initialized;
|
||||
private static int _nextLibraryContextHandle;
|
||||
private static int _nextPushEventHandle;
|
||||
private static int _nextPushEventCallbackHandle;
|
||||
private static int _nextUserContextHandle = 1000;
|
||||
private static readonly object _contextGate = new();
|
||||
private static readonly HashSet<int> _libraryContexts = [];
|
||||
private static readonly HashSet<int> _userContexts = [];
|
||||
private static readonly HashSet<int> _pushEventFilters = [];
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
Interlocked.Exchange(ref _nextLibraryContextHandle, 0);
|
||||
Interlocked.Exchange(ref _nextPushEventHandle, 0);
|
||||
Interlocked.Exchange(ref _nextPushEventCallbackHandle, 0);
|
||||
Interlocked.Exchange(ref _nextUserContextHandle, 1000);
|
||||
lock (_contextGate)
|
||||
{
|
||||
_libraryContexts.Clear();
|
||||
_userContexts.Clear();
|
||||
_pushEventFilters.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+o9816YQhqQ",
|
||||
|
|
@ -51,6 +69,11 @@ public static class NpWebApi2Exports
|
|||
}
|
||||
|
||||
var filterHandle = Interlocked.Increment(ref _nextPushEventHandle);
|
||||
lock (_contextGate)
|
||||
{
|
||||
_pushEventFilters.Add(filterHandle);
|
||||
}
|
||||
|
||||
TraceNpWebApi2("push-event-create-filter", libraryContextId, (ulong)filterHandle);
|
||||
return ctx.SetReturn(filterHandle);
|
||||
}
|
||||
|
|
@ -74,6 +97,20 @@ public static class NpWebApi2Exports
|
|||
return ctx.SetReturn(handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fIATVMo4Y1w",
|
||||
ExportName = "sceNpWebApi2PushEventDeleteHandle",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2PushEventDeleteHandle(CpuContext ctx)
|
||||
{
|
||||
TraceNpWebApi2(
|
||||
"push-event-delete-handle",
|
||||
unchecked((int)ctx[CpuRegister.Rdi]),
|
||||
unchecked((uint)ctx[CpuRegister.Rsi]));
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sk54bi6FtYM",
|
||||
ExportName = "sceNpWebApi2CreateUserContext",
|
||||
|
|
@ -97,9 +134,34 @@ public static class NpWebApi2Exports
|
|||
}
|
||||
|
||||
var userContextId = Interlocked.Increment(ref _nextUserContextHandle);
|
||||
lock (_contextGate)
|
||||
{
|
||||
_userContexts.Add(userContextId);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(userContextId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fY3QqeNkF8k",
|
||||
ExportName = "sceNpWebApi2PushEventRegisterCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2PushEventRegisterCallback(CpuContext ctx)
|
||||
{
|
||||
var userContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var filterId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var callback = ctx[CpuRegister.Rdx];
|
||||
if (callback == 0 || !IsRegisteredPushEventPair(userContextId, filterId))
|
||||
{
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var callbackId = Interlocked.Increment(ref _nextPushEventCallbackHandle);
|
||||
TraceNpWebApi2("push-event-register-callback", userContextId, (ulong)callbackId);
|
||||
return ctx.SetReturn(callbackId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bEvXpcEk200",
|
||||
ExportName = "sceNpWebApi2Terminate",
|
||||
|
|
@ -171,10 +233,25 @@ public static class NpWebApi2Exports
|
|||
if (_libraryContexts.Count == 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
_userContexts.Clear();
|
||||
_pushEventFilters.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRegisteredPushEventPair(int userContextId, int filterId)
|
||||
{
|
||||
if (Volatile.Read(ref _initialized) == 0 || userContextId <= 0 || filterId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_contextGate)
|
||||
{
|
||||
return _userContexts.Contains(userContextId) && _pushEventFilters.Contains(filterId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP_WEB_API2"), "1", StringComparison.Ordinal))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace SharpEmu.Libs.Pad;
|
|||
public static class PadExports
|
||||
{
|
||||
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
|
||||
private const int OrbisPadErrorInvalidArgument = unchecked((int)0x80920001);
|
||||
private const int OrbisPadErrorAlreadyOpened = unchecked((int)0x80920004);
|
||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||
|
|
@ -19,8 +21,10 @@ public static class PadExports
|
|||
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
|
||||
// leaving every later keyboard/gamepad read on an invalid handle.
|
||||
private const int PrimaryUserId = 0x10000000;
|
||||
private const int SystemUserId = 0xFF;
|
||||
private const int StandardPortType = 0;
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int SpecialPortType = 2;
|
||||
private const int RemotePortType = 16;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
private const float StandardGravity = 9.80665f;
|
||||
|
|
@ -31,11 +35,32 @@ public static class PadExports
|
|||
// user slots and treats every valid handle as a connected controller, so accepting
|
||||
// 0 there reports phantom pads that repeat the primary pad's buttons.
|
||||
private static int _padOpened;
|
||||
private static readonly object PadHandleGate = new();
|
||||
private static readonly Dictionary<PadOpenKey, int> PadHandleByKey = new();
|
||||
private static readonly Dictionary<int, PadOpenKey> PadKeyByHandle = new();
|
||||
private static int _nextPadHandle = 1;
|
||||
|
||||
private static bool IsPrimaryPadHandle(int handle) =>
|
||||
handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(ref _padOpened) == 0);
|
||||
private readonly record struct PadOpenKey(int UserId, int Type, int Index);
|
||||
|
||||
internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0);
|
||||
private static bool IsOpenPadHandle(int handle)
|
||||
{
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
return PadKeyByHandle.ContainsKey(handle) || (handle == 0 && _padOpened == 0);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetOpenedPadForTests()
|
||||
{
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
PadHandleByKey.Clear();
|
||||
PadKeyByHandle.Clear();
|
||||
_nextPadHandle = 1;
|
||||
_padOpened = 0;
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||
|
||||
[ThreadStatic]
|
||||
|
|
@ -45,6 +70,9 @@ public static class PadExports
|
|||
private static PadState _cachedInputState;
|
||||
|
||||
private static bool _initialized;
|
||||
private static readonly bool LogPadState =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PAD_STATE"), "1", StringComparison.Ordinal);
|
||||
private static long _loggedPadSignature = -1;
|
||||
// Motion data is reported until a title turns it off: Astro Bot reads it for
|
||||
// shake/tilt without ever importing scePadSetMotionSensorState.
|
||||
private static int _motionSensorEnabled = 1;
|
||||
|
|
@ -62,6 +90,18 @@ public static class PadExports
|
|||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "n3kSX62fgNo",
|
||||
ExportName = "scePadUnknownN3kSX62fgNo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadUnknownN3kSX62fgNo(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xk0AcarP3V4",
|
||||
ExportName = "scePadOpen",
|
||||
|
|
@ -76,12 +116,6 @@ public static class PadExports
|
|||
LibraryName = "libScePad")]
|
||||
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||
|
||||
// scePadGetHandle(userId, type, index): returns the handle of an already-open
|
||||
// pad without opening a new one. Dead Cells calls it every frame to poll
|
||||
// input; leaving it unresolved returned a garbage handle so the input path
|
||||
// (and the game loop that drives it) misbehaved. Same validation as
|
||||
// scePadOpen — the one primary pad — returning its handle or a not-connected
|
||||
// error, never opening or logging.
|
||||
[SysAbiExport(
|
||||
Nid = "u1GRHp+oWoY",
|
||||
ExportName = "scePadGetHandle",
|
||||
|
|
@ -97,22 +131,24 @@ public static class PadExports
|
|||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
|
||||
if (userId == -1)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
return PadHandleByKey.TryGetValue(new PadOpenKey(userId, type, index), out var handle)
|
||||
? ctx.SetReturn(handle)
|
||||
: ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||
if (!_initialized)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
|
|
@ -123,8 +159,7 @@ public static class PadExports
|
|||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||
if (!IsSupportedOpenTuple(userId, type, index, extended))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
|
@ -138,8 +173,42 @@ public static class PadExports
|
|||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||
}
|
||||
|
||||
Volatile.Write(ref _padOpened, 1);
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
var key = new PadOpenKey(userId, type, index);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
if (PadHandleByKey.ContainsKey(key))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorAlreadyOpened);
|
||||
}
|
||||
|
||||
var handle = _nextPadHandle++;
|
||||
PadHandleByKey.Add(key, handle);
|
||||
PadKeyByHandle.Add(handle, key);
|
||||
_padOpened = 1;
|
||||
return ctx.SetReturn(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedOpenTuple(int userId, int type, int index, bool extended)
|
||||
{
|
||||
if (index != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userId == SystemUserId && type == RemotePortType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extended
|
||||
? type is 0 or 1 or 2
|
||||
: type is StandardPortType or SpecialPortType;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -150,9 +219,16 @@ public static class PadExports
|
|||
public static int PadClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
if (!PadKeyByHandle.Remove(handle, out var key))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
PadHandleByKey.Remove(key);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -163,7 +239,7 @@ public static class PadExports
|
|||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -180,7 +256,7 @@ public static class PadExports
|
|||
public static int PadSetTiltCorrectionState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -193,7 +269,7 @@ public static class PadExports
|
|||
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -207,7 +283,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -258,7 +334,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -272,7 +348,7 @@ public static class PadExports
|
|||
// (DualSense). We emulate no special peripheral (guitar/drums/wheel), so
|
||||
// the class-data union stays zeroed — the guest treats it as a plain
|
||||
// controller with no extended capabilities.
|
||||
Span<byte> information = stackalloc byte[0x20];
|
||||
Span<byte> information = stackalloc byte[0x14];
|
||||
information.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
|
||||
|
||||
|
|
@ -281,6 +357,57 @@ public static class PadExports
|
|||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IHPqcbc0zCA",
|
||||
ExportName = "scePadDeviceClassParseData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadDeviceClassParseData(CpuContext ctx)
|
||||
{
|
||||
const int deviceClassDataSize = 24;
|
||||
const int deviceUniqueDataLengthOffset = 0x6B;
|
||||
const int deviceUniqueDataOffset = 0x6C;
|
||||
const int maximumDeviceUniqueDataLength = 12;
|
||||
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var padDataAddress = ctx[CpuRegister.Rsi];
|
||||
var classDataAddress = ctx[CpuRegister.Rdx];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (padDataAddress == 0 || classDataAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> padData = stackalloc byte[PadDataSize];
|
||||
if (!ctx.Memory.TryRead(padDataAddress, padData))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Span<byte> classData = stackalloc byte[deviceClassDataSize];
|
||||
classData.Clear();
|
||||
classData[0x04] = padData[0x4C] != 0 ? (byte)1 : (byte)0;
|
||||
|
||||
var uniqueDataLength = Math.Min(
|
||||
(int)padData[deviceUniqueDataLengthOffset],
|
||||
maximumDeviceUniqueDataLength);
|
||||
if (uniqueDataLength > 0)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(classData[0x00..], -1);
|
||||
classData[0x08] = (byte)uniqueDataLength;
|
||||
padData.Slice(deviceUniqueDataOffset, uniqueDataLength)
|
||||
.CopyTo(classData.Slice(0x0C, uniqueDataLength));
|
||||
}
|
||||
|
||||
return ctx.Memory.TryWrite(classDataAddress, classData)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YndgXqQVV7c",
|
||||
ExportName = "scePadReadState",
|
||||
|
|
@ -290,7 +417,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -315,7 +442,7 @@ public static class PadExports
|
|||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -349,7 +476,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -387,7 +514,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -575,7 +702,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -605,7 +732,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -634,7 +761,7 @@ public static class PadExports
|
|||
public static int PadResetLightBar(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -651,7 +778,7 @@ public static class PadExports
|
|||
public static int PadResetOrientation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -664,7 +791,7 @@ public static class PadExports
|
|||
public static int PadIsRemoteController(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -765,6 +892,17 @@ public static class PadExports
|
|||
buttons |= 0x4000;
|
||||
}
|
||||
|
||||
if (LogPadState)
|
||||
{
|
||||
var signature = ((ulong)buttons << 8) | ((ulong)(acceptsKeyboardInput ? 1 : 0) << 4) | (uint)gamepadCount;
|
||||
if (Interlocked.Exchange(ref _loggedPadSignature, (long)signature) != (long)signature)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] pad state: focused={acceptsKeyboardInput} gamepads={gamepadCount} buttons=0x{buttons:X8} " +
|
||||
$"left={leftX},{leftY} thread={Environment.CurrentManagedThreadId}");
|
||||
}
|
||||
}
|
||||
|
||||
_cachedInputState = new PadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public static class PlayGoExports
|
|||
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
|
||||
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
|
||||
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
|
||||
private const int OrbisPlayGoErrorBadOptionalType = unchecked((int)0x80B20024);
|
||||
private const ulong PlayGoInitBufAddrOffset = 0;
|
||||
private const ulong PlayGoInitBufSizeOffset = 8;
|
||||
private const uint PlayGoMinimumInitBufferSize = 0x200000;
|
||||
|
|
@ -29,6 +30,10 @@ public static class PlayGoExports
|
|||
private const int PlayGoInstallSpeedSuspended = 0;
|
||||
private const int PlayGoInstallSpeedTrickle = 1;
|
||||
private const int PlayGoInstallSpeedFull = 2;
|
||||
private const int PlayGoOptionalTypeLanguage = 0;
|
||||
private const int PlayGoOptionalTypeScenario = 1;
|
||||
private const ulong PlayGoLanguageMaskAll = ulong.MaxValue;
|
||||
private const ulong PlayGoScenarioMaskAll = 0x1f;
|
||||
private const uint MaxPlayGoQueryEntries = 0x4000;
|
||||
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
|
||||
|
||||
|
|
@ -231,7 +236,7 @@ public static class PlayGoExports
|
|||
chunkIds = _metadata.ChunkIds;
|
||||
}
|
||||
|
||||
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
|
||||
var availableEntries = (uint)chunkIds.Length;
|
||||
if (outChunkIdList == 0)
|
||||
{
|
||||
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
||||
|
|
@ -249,7 +254,7 @@ public static class PlayGoExports
|
|||
|
||||
for (uint i = 0; i < entriesToWrite; i++)
|
||||
{
|
||||
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
||||
var chunkId = chunkIds[i];
|
||||
if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
|
|
@ -262,6 +267,91 @@ public static class PlayGoExports
|
|||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8-e7E989rCU",
|
||||
ExportName = "scePlayGoGetInstallChunkId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetInstallChunkId(CpuContext ctx)
|
||||
{
|
||||
return PlayGoGetChunkId(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "g4AZyxpSAlA",
|
||||
ExportName = "scePlayGoGetOptionalChunk",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetOptionalChunk(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var outOption = ctx[CpuRegister.Rdx];
|
||||
|
||||
var validation = ValidateHandle(handle);
|
||||
if (validation != 0)
|
||||
{
|
||||
return validation;
|
||||
}
|
||||
|
||||
if (outOption == 0)
|
||||
{
|
||||
return OrbisPlayGoErrorBadPointer;
|
||||
}
|
||||
|
||||
var option = type switch
|
||||
{
|
||||
PlayGoOptionalTypeLanguage => PlayGoLanguageMaskAll,
|
||||
PlayGoOptionalTypeScenario => PlayGoScenarioMaskAll,
|
||||
_ => 0UL,
|
||||
};
|
||||
if (type is not PlayGoOptionalTypeLanguage and not PlayGoOptionalTypeScenario)
|
||||
{
|
||||
return OrbisPlayGoErrorBadOptionalType;
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outOption, option)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IfiN+-oeVWI",
|
||||
ExportName = "scePlayGoGetSupportedOptionalChunk",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetSupportedOptionalChunk(CpuContext ctx)
|
||||
{
|
||||
return PlayGoGetOptionalChunk(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HVAa744ecdw",
|
||||
ExportName = "scePlayGoPrefetchOptionalChunk",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoPrefetchOptionalChunk(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var option = ctx[CpuRegister.Rdx];
|
||||
|
||||
var validation = ValidateHandle(handle);
|
||||
if (validation != 0)
|
||||
{
|
||||
return validation;
|
||||
}
|
||||
|
||||
if (option == 0)
|
||||
{
|
||||
return OrbisPlayGoErrorBadPointer;
|
||||
}
|
||||
|
||||
return type is PlayGoOptionalTypeLanguage or PlayGoOptionalTypeScenario
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisPlayGoErrorBadOptionalType;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v6EZ-YWRdMs",
|
||||
ExportName = "scePlayGoGetEta",
|
||||
|
|
@ -685,10 +775,7 @@ public static class PlayGoExports
|
|||
{
|
||||
// No app0 override to probe for sidecar files: same fully-installed
|
||||
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
[(ushort)0],
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
return CreateBaseChunkMetadata(PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
|
|
@ -718,14 +805,27 @@ public static class PlayGoExports
|
|||
}
|
||||
|
||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||
if (chunkIds.Length == 0)
|
||||
{
|
||||
TracePlayGo("metadata_has_no_chunk_definitions; installed chunk ids unknown");
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
Array.Empty<ushort>(),
|
||||
PlayGoChunkIdKnowledge.Unknown);
|
||||
}
|
||||
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
chunkIds,
|
||||
chunkIds.Length == 0
|
||||
? PlayGoChunkIdKnowledge.Unknown
|
||||
: PlayGoChunkIdKnowledge.Authoritative);
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
private static PlayGoMetadata CreateBaseChunkMetadata(PlayGoChunkIdKnowledge knowledge) =>
|
||||
new(
|
||||
true,
|
||||
[(ushort)0],
|
||||
knowledge);
|
||||
|
||||
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
|
||||
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
|
||||
// is the base chunk and must resolve even for a title with no pak files at
|
||||
|
|
@ -831,7 +931,7 @@ public static class PlayGoExports
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
|
|
@ -846,6 +946,8 @@ public static class PlayGoExports
|
|||
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
|
||||
private enum PlayGoChunkIdKnowledge
|
||||
{
|
||||
Unknown,
|
||||
|
|
|
|||
|
|
@ -74,9 +74,11 @@ public static class SaveDataExports
|
|||
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
|
||||
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
|
||||
// SceSaveDataEventType
|
||||
private const uint EventTypeBackupEnd = 2;
|
||||
private const uint EventTypeSaveDataMemorySyncEnd = 3;
|
||||
private const int SaveDataEventSize = 0x60;
|
||||
private const int MountInfoSize = 0x40;
|
||||
private const int SaveDataTitleIdStructSize = 0x10;
|
||||
private const int SaveDataEventSize = 0x68;
|
||||
private const int MountInfoSize = 0x30;
|
||||
private const uint SaveDataBlockSize = 65536;
|
||||
private const ulong SaveDataBlocksMax = 16384;
|
||||
|
||||
|
|
@ -86,16 +88,79 @@ public static class SaveDataExports
|
|||
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
|
||||
private static readonly Dictionary<string, MountEntry> _mounts = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly record struct SaveDataEvent(uint Type, int ErrorCode, int UserId, string DirName);
|
||||
private readonly record struct SaveDataEvent(
|
||||
uint Type,
|
||||
int ErrorCode,
|
||||
int UserId,
|
||||
string TitleId,
|
||||
string DirName);
|
||||
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
|
||||
|
||||
private static void EnqueueEvent(uint type, int userId, string dirName, int errorCode = 0)
|
||||
private static void EnqueueEvent(
|
||||
uint type,
|
||||
int userId,
|
||||
string titleId,
|
||||
string dirName,
|
||||
int errorCode = 0)
|
||||
{
|
||||
lock (_eventGate)
|
||||
{
|
||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, dirName));
|
||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, titleId, dirName));
|
||||
}
|
||||
TraceSaveData($"event.enqueue type={type} user={userId} dir='{dirName}' err=0x{errorCode:X}");
|
||||
TraceSaveData($"event.enqueue type={type} user={userId} title='{titleId}' dir='{dirName}' err=0x{errorCode:X}");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "z1JA8-iJt3k",
|
||||
ExportName = "sceSaveDataBackup",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataBackup(CpuContext ctx)
|
||||
{
|
||||
var backupAddress = ctx[CpuRegister.Rdi];
|
||||
if (backupAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, backupAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(backupAddress + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(backupAddress + 0x10, out var dirNameAddress))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || dirNameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
string titleId;
|
||||
if (titleIdAddress == 0)
|
||||
{
|
||||
titleId = ResolveConfiguredTitleId();
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, titleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(titleId))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
EnqueueEvent(EventTypeBackupEnd, userId, titleId, dirName);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -131,7 +196,14 @@ public static class SaveDataExports
|
|||
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
|
||||
WriteAscii(ev.Slice(0x10, SaveDataDirNameSize), pending.DirName);
|
||||
WriteFixedAscii(
|
||||
ev.Slice(0x10, SaveDataTitleIdStructSize),
|
||||
pending.TitleId,
|
||||
SaveDataTitleIdSize);
|
||||
WriteFixedAscii(
|
||||
ev.Slice(0x20, SaveDataDirNameSize),
|
||||
pending.DirName,
|
||||
SaveDataDirNameSize);
|
||||
if (!ctx.Memory.TryWrite(eventAddress, ev))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -617,6 +689,11 @@ public static class SaveDataExports
|
|||
ExportName = "sceSaveDataDirNameSearchPs4",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
[SysAbiExport(
|
||||
Nid = "PHnuI4LhuRk",
|
||||
ExportName = "sceSaveDataDirNameSearch2",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataDirNameSearch(CpuContext ctx)
|
||||
{
|
||||
var condAddress = ctx[CpuRegister.Rdi];
|
||||
|
|
@ -1241,6 +1318,19 @@ public static class SaveDataExports
|
|||
}
|
||||
}
|
||||
|
||||
private static void WriteFixedAscii(
|
||||
Span<byte> destination,
|
||||
string value,
|
||||
int maxContentLength)
|
||||
{
|
||||
var count = Math.Min(value.Length, Math.Min(destination.Length, maxContentLength));
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var ch = value[i];
|
||||
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
|
|
@ -1486,7 +1576,11 @@ public static class SaveDataExports
|
|||
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
|
||||
// until the SAVE_DATA_MEMORY_SYNC_END event arrives. Post it so that
|
||||
// poll completes (this is what wedged Dead Cells at FLIP 0 in-level).
|
||||
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
|
||||
EnqueueEvent(
|
||||
EventTypeSaveDataMemorySyncEnd,
|
||||
userId,
|
||||
ResolveConfiguredTitleId(),
|
||||
"sce_sdmemory");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Share;
|
||||
|
||||
public static class SharePlayExports
|
||||
{
|
||||
private static int _controllerProhibited;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FzQS6DREDfk",
|
||||
ExportName = "sceSharePlayProhibitController",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSharePlay")]
|
||||
public static int SharePlayProhibitController(CpuContext ctx)
|
||||
{
|
||||
var prohibited = ctx[CpuRegister.Rdi];
|
||||
if (prohibited > 1)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _controllerProhibited, unchecked((int)prohibited));
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
public static void ResetRuntimeState() => Volatile.Write(ref _controllerProhibited, 0);
|
||||
|
||||
internal static bool IsControllerProhibited => Volatile.Read(ref _controllerProhibited) != 0;
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
}
|
||||
|
|
@ -64,14 +64,6 @@ public static class GameServiceStubs
|
|||
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
|
||||
public static int NpGameIntentTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "jqb7HntFQFc", ExportName = "sceWebBrowserDialogInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "ocHtyBwHfys", ExportName = "sceWebBrowserDialogTerminate",
|
||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "mlYGfmqE3fQ", ExportName = "sceSigninDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")]
|
||||
public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
|
@ -114,6 +106,14 @@ public static class GameServiceStubs
|
|||
public static int TextToSpeech2Open(CpuContext ctx) =>
|
||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
|
||||
[SysAbiExport(Nid = "2jiIxUmcsGo", ExportName = "sceTextToSpeech2Cancel",
|
||||
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
|
||||
public static int TextToSpeech2Cancel(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "08JSg9p6bgQ", ExportName = "sceTextToSpeech2GetSpeechStatus",
|
||||
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
|
||||
public static int TextToSpeech2GetSpeechStatus(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "kvYEw2lBndk", ExportName = "sceGameLiveStreamingInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceGameLiveStreaming")]
|
||||
public static int GameLiveStreamingInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ public static class UserServiceExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bwFjS+bX9mA",
|
||||
ExportName = "sceUserServiceTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceTerminate(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _loginEventDelivered, 0);
|
||||
return SetReturnWithTrace(ctx, 0, "terminate");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CdWp0oHWGr0",
|
||||
ExportName = "sceUserServiceGetInitialUser",
|
||||
|
|
@ -144,6 +155,30 @@ public static class UserServiceExports
|
|||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "qbwy0Ub8b3M",
|
||||
ExportName = "sceUserServiceGetUserNumber",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetUserNumber(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var numberAddress = ctx[CpuRegister.Rsi];
|
||||
if (numberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId && userId != 1)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, numberAddress, 1)
|
||||
? SetReturnWithTrace(ctx, 0, $"get_user_number user={userId} number=1 out=0x{numberAddress:X16}")
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
// 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(
|
||||
|
|
|
|||
|
|
@ -398,15 +398,18 @@ public static partial class VideoOutExports
|
|||
public static int VideoOutClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
lock (_stateGate)
|
||||
GuestGpu.Current.RunAfterPendingCommandStreams(() =>
|
||||
{
|
||||
foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray())
|
||||
lock (_stateGate)
|
||||
{
|
||||
CancelFlipLocked(request);
|
||||
foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray())
|
||||
{
|
||||
CancelFlipLocked(request);
|
||||
}
|
||||
_ports.Remove(handle);
|
||||
Monitor.PulseAll(_stateGate);
|
||||
}
|
||||
_ports.Remove(handle);
|
||||
Monitor.PulseAll(_stateGate);
|
||||
}
|
||||
});
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
|
@ -1246,36 +1249,42 @@ public static partial class VideoOutExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var attributeIndex = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (attributeIndex < 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
var result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
GuestGpu.Current.RunAfterPendingCommandStreams(() =>
|
||||
{
|
||||
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||
lock (_stateGate)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
port.Groups[attributeIndex] = null;
|
||||
foreach (var slot in port.BufferSlots)
|
||||
{
|
||||
if (slot.GroupIndex == attributeIndex)
|
||||
if (!_ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
slot.GroupIndex = -1;
|
||||
slot.AddressLeft = 0;
|
||||
slot.AddressRight = 0;
|
||||
result = OrbisVideoOutErrorInvalidHandle;
|
||||
return;
|
||||
}
|
||||
|
||||
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||
{
|
||||
result = OrbisVideoOutErrorInvalidValue;
|
||||
return;
|
||||
}
|
||||
|
||||
port.Groups[attributeIndex] = null;
|
||||
foreach (var slot in port.BufferSlots)
|
||||
{
|
||||
if (slot.GroupIndex == attributeIndex)
|
||||
{
|
||||
slot.GroupIndex = -1;
|
||||
slot.AddressLeft = 0;
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private bool ResolveDccAttachmentClear(ColorAttachment target, out ClearColorValue clearValue)
|
||||
{
|
||||
clearValue = default;
|
||||
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc || target.Request.Description.Metadata.Range.Size != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,26 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
? testStream.Done()
|
||||
: IdleOutcome.Completed;
|
||||
|
||||
public static void RunAfterPendingCommandStreams(Action work)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(work);
|
||||
if (TryGetActivePresenter(out var presenter))
|
||||
{
|
||||
if (presenter.Relay.TryRunAfterAcceptedCommandStreams(work))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _closed) &&
|
||||
!Volatile.Read(ref _presenterCloseRequested))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("The GPU worker rejected an ordered video-out state change.");
|
||||
}
|
||||
}
|
||||
|
||||
work();
|
||||
}
|
||||
|
||||
// The blocked heads of the stream this memory submits to; null when no stream exists for it.
|
||||
internal static BlockedSnapshot? SnapshotBlockedCommandStream(ICpuMemory? memory)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
DepthCompare: image.DepthCompare,
|
||||
Atomic: image.Atomic);
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, byte> _tracedTextureBindings = new();
|
||||
|
||||
// Render-state discovery for one shader image; the view is acquired later with the draw.
|
||||
private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index)
|
||||
{
|
||||
|
|
@ -127,11 +129,12 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
|
||||
BindImage(imageIdentifier, storage);
|
||||
var descriptor = new TextureDescriptorWords(words);
|
||||
if (ShouldTraceTextureBindings())
|
||||
if (ShouldTraceTextureBindings() &&
|
||||
(!_traceVolumeTextureBindingsOnly || image.Dimension == ImageDimension.Dim3D))
|
||||
{
|
||||
var cached = _imageCache.GetImage(imageIdentifier);
|
||||
var description = cached.Description;
|
||||
Console.Error.WriteLine(
|
||||
var line =
|
||||
$"TextureBinding stage={program.Stage} hash=0x{program.Hash:X16} index={index} " +
|
||||
$"address=0x{new TextureDescriptorWords(words).BaseAddress:X16} " +
|
||||
$"descriptor={descriptor.BaseAddress:X16} size={descriptor.Width + 1}x{descriptor.Height + 1} " +
|
||||
|
|
@ -139,7 +142,14 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
$"image=0x{description.Data.Address:X16} size=0x{description.Data.Size:X} " +
|
||||
$"extent={description.Extent.Width}x{description.Extent.Height} pitch={description.Pitch} " +
|
||||
$"guestFormat={(uint)description.GuestFormat} imageTile={(uint)description.TileMode} " +
|
||||
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format}");
|
||||
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format} " +
|
||||
$"depth={descriptor.Depth + 1} type={descriptor.Type} dcc={descriptor.MetadataCompress} " +
|
||||
$"metadata=0x{descriptor.MetadataAddress << 8:X} metaKind={description.Metadata.Kind} " +
|
||||
$"words={string.Join(',', words.Select(static word => word.ToString("X8")))}";
|
||||
if (!_traceVolumeTextureBindingsOnly || _tracedTextureBindings.TryAdd(line, 0))
|
||||
{
|
||||
Console.Error.WriteLine(line);
|
||||
}
|
||||
}
|
||||
return new TextureResource
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private ulong _minStorageBufferOffsetAlignment = 1;
|
||||
private bool _supportsIndependentBlend;
|
||||
private bool _supportsDepthBiasClamp;
|
||||
private bool _supportsShaderSignedZeroInfNanPreserveFloat32;
|
||||
private uint _maxColorAttachments;
|
||||
private Device _device;
|
||||
private PipelineCache _pipelineCache;
|
||||
|
|
@ -583,10 +584,15 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr,
|
||||
};
|
||||
var floatControls = new PhysicalDeviceFloatControlsProperties
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceFloatControlsProperties,
|
||||
PNext = &pushDescriptorProperties,
|
||||
};
|
||||
var subgroupSizeControl = new PhysicalDeviceSubgroupSizeControlProperties
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceSubgroupSizeControlProperties,
|
||||
PNext = &pushDescriptorProperties,
|
||||
PNext = &floatControls,
|
||||
};
|
||||
var subgroup = new PhysicalDeviceSubgroupProperties
|
||||
{
|
||||
|
|
@ -599,6 +605,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
PNext = &subgroup,
|
||||
};
|
||||
_vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2);
|
||||
_supportsShaderSignedZeroInfNanPreserveFloat32 =
|
||||
floatControls.ShaderSignedZeroInfNanPreserveFloat32;
|
||||
SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages);
|
||||
_canRequireComputeSubgroup32 =
|
||||
subgroup.SubgroupSize != RdnaSubgroupSize &&
|
||||
|
|
@ -657,6 +665,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_ when GraphicsSubgroupOperationsEnabled => "auto-native-wave32",
|
||||
_ => "auto-fallback",
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan shaderSignedZeroInfNanPreserveFloat32 " +
|
||||
$"enabled={_supportsShaderSignedZeroInfNanPreserveFloat32}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan graphics subgroup operations " +
|
||||
$"enabled={GraphicsSubgroupOperationsEnabled} " +
|
||||
|
|
@ -684,7 +695,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
uint? compute = null;
|
||||
uint? general = null;
|
||||
for (uint index = 0; index < count; index++)
|
||||
{
|
||||
var flags = families[index].QueueFlags;
|
||||
|
|
@ -702,16 +712,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
compute ??= index;
|
||||
}
|
||||
|
||||
// MoltenVK exposes only general families, one queue each, every one its own
|
||||
// Metal command queue: a second one still runs readbacks beside the main queue.
|
||||
if ((flags & (QueueFlags.TransferBit | QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) != 0)
|
||||
{
|
||||
general ??= index;
|
||||
}
|
||||
}
|
||||
|
||||
return compute ?? general;
|
||||
return compute;
|
||||
}
|
||||
|
||||
private bool _supportsFragmentShaderBarycentric;
|
||||
|
|
|
|||
|
|
@ -91,10 +91,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceTextureBindingsEnabled =
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") is "1" or "volume";
|
||||
private static readonly bool _traceVolumeTextureBindingsOnly =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") == "volume";
|
||||
private static readonly uint _traceGuestImageWidth =
|
||||
uint.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_WIDTH"),
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
|
||||
|
||||
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
|
||||
bool IShaderPipelineHost.ShaderSignedZeroInfNanPreserveFloat32Supported =>
|
||||
_supportsShaderSignedZeroInfNanPreserveFloat32;
|
||||
// NVIDIA's compiler rejects the elided-EXEC wave64 compute module with NVVM error 3.
|
||||
bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId;
|
||||
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
|
||||
|
|
|
|||
|
|
@ -574,7 +574,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
const byte DccClearToZero = 0x00;
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc ||
|
||||
image.Description.Metadata.Range.Size != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
// This partial owns the submission scheduler and the GPU worker relay.
|
||||
|
||||
private readonly object _queueGate = new();
|
||||
private readonly GpuWorkerRelay _relay = new(WakeRenderThread);
|
||||
private readonly GpuWorkerRelay _relay;
|
||||
|
||||
internal GpuWorkerRelay Relay => _relay;
|
||||
private readonly SubmissionContext _submissionContext = new();
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
bool execGuardElision)
|
||||
{
|
||||
if (!ShaderProgramCache.TryCompilePrewarm(
|
||||
record, code, compiler, sharedInt64Atomics, execGuardElision, out var compiled, out var layout, out var error))
|
||||
record, code, compiler, sharedInt64Atomics, execGuardElision,
|
||||
_supportsShaderSignedZeroInfNanPreserveFloat32, out var compiled, out var layout, out var error))
|
||||
{
|
||||
NoteShaderPrewarmFailure(record, error);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using SharpEmu.Libs.AvPlayer;
|
|||
using SharpEmu.Libs.Media;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
|
@ -255,6 +256,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
public Presenter(uint width, uint height)
|
||||
{
|
||||
_commandStream = new CommandStreamQueue(this);
|
||||
_relay = new GpuWorkerRelay(WakeRenderThread, _commandStream.TryEnqueueControlBarrier);
|
||||
_hostBufferPool = new VulkanHostBufferPool(
|
||||
MaximumCachedHostBufferBytes,
|
||||
DestroyHostBufferAllocation);
|
||||
|
|
|
|||
|
|
@ -442,6 +442,15 @@ public static partial class Gen5SpirvTranslator
|
|||
break;
|
||||
}
|
||||
case "VRcpF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.FDiv,
|
||||
_floatType,
|
||||
Float(1),
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
break;
|
||||
case "VRcpIflagF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
|
|
@ -454,7 +463,11 @@ public static partial class Gen5SpirvTranslator
|
|||
case "VLogF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(30, _floatType, GetFloatSource(instruction, 0)));
|
||||
Ext(
|
||||
30,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
break;
|
||||
case "VLdexpF32":
|
||||
result = EmitFloatResult(
|
||||
|
|
@ -468,12 +481,20 @@ public static partial class Gen5SpirvTranslator
|
|||
case "VExpF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(29, _floatType, GetFloatSource(instruction, 0)));
|
||||
Ext(
|
||||
29,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
break;
|
||||
case "VRsqF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(32, _floatType, GetFloatSource(instruction, 0)));
|
||||
Ext(
|
||||
32,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
break;
|
||||
case "VRcpF16":
|
||||
result = EmitFloat16Result(instruction, destination, _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1), GetFloat16Source(instruction, 0)));
|
||||
|
|
@ -542,7 +563,11 @@ public static partial class Gen5SpirvTranslator
|
|||
case "VSqrtF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(31, _floatType, GetFloatSource(instruction, 0)));
|
||||
Ext(
|
||||
31,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
break;
|
||||
case "VSinF32":
|
||||
result = EmitFloatResult(
|
||||
|
|
@ -553,7 +578,9 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
GetFloatSource(instruction, 0),
|
||||
EmitTrigCycleF32(
|
||||
GetFloatSource(instruction, 0),
|
||||
preserveSignedZero: true),
|
||||
Float(MathF.Tau))));
|
||||
break;
|
||||
case "VCosF32":
|
||||
|
|
@ -565,7 +592,9 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
GetFloatSource(instruction, 0),
|
||||
EmitTrigCycleF32(
|
||||
GetFloatSource(instruction, 0),
|
||||
preserveSignedZero: false),
|
||||
Float(MathF.Tau))));
|
||||
break;
|
||||
case "VAddF32":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
public static partial class Gen5SpirvTranslator
|
||||
{
|
||||
private sealed partial class CompilationContext
|
||||
{
|
||||
private uint EmitFlushF32DenormToSignedZero(uint value)
|
||||
{
|
||||
var bits = Bitcast(_uintType, value);
|
||||
var absolute = _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_uintType,
|
||||
bits,
|
||||
UInt(0x7FFF_FFFF));
|
||||
var sign = _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_uintType,
|
||||
bits,
|
||||
UInt(0x8000_0000));
|
||||
var nonZero = IsNotZero(absolute);
|
||||
var subnormal = _module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
absolute,
|
||||
UInt(0x0080_0000));
|
||||
var flush = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
nonZero,
|
||||
subnormal);
|
||||
var selected = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
flush,
|
||||
sign,
|
||||
bits);
|
||||
return Bitcast(_floatType, selected);
|
||||
}
|
||||
|
||||
private uint EmitTrigCycleF32(uint source, bool preserveSignedZero)
|
||||
{
|
||||
var fraction = Ext(10, _floatType, source);
|
||||
var bits = Bitcast(_uintType, source);
|
||||
var absolute = _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_uintType,
|
||||
bits,
|
||||
UInt(0x7FFF_FFFF));
|
||||
var large = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
_boolType,
|
||||
absolute,
|
||||
UInt(0x4B00_0000));
|
||||
var finite = _module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
absolute,
|
||||
UInt(0x7F80_0000));
|
||||
var largeFinite = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
large,
|
||||
finite);
|
||||
var reduced = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_floatType,
|
||||
largeFinite,
|
||||
Float(0),
|
||||
fraction);
|
||||
if (!preserveSignedZero)
|
||||
{
|
||||
return reduced;
|
||||
}
|
||||
|
||||
var zero = _module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
absolute,
|
||||
UInt(0));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_floatType,
|
||||
zero,
|
||||
source,
|
||||
reduced);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -518,6 +518,13 @@ public static partial class Gen5SpirvTranslator
|
|||
_ => SpirvExecutionModel.GLCompute,
|
||||
};
|
||||
_module.AddEntryPoint(model, main, "main", _interfaces);
|
||||
if (_request.ShaderSignedZeroInfNanPreserveFloat32Supported)
|
||||
{
|
||||
_module.AddExecutionMode(
|
||||
main,
|
||||
SpirvExecutionMode.SignedZeroInfNanPreserve,
|
||||
32);
|
||||
}
|
||||
if (_stage == Gen5SpirvStage.Pixel)
|
||||
{
|
||||
_module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
|
|
@ -555,6 +562,11 @@ public static partial class Gen5SpirvTranslator
|
|||
private void DeclareModule()
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.Shader);
|
||||
if (_request.ShaderSignedZeroInfNanPreserveFloat32Supported)
|
||||
{
|
||||
_module.AddExtension("SPV_KHR_float_controls");
|
||||
_module.AddCapability(SpirvCapability.SignedZeroInfNanPreserve);
|
||||
}
|
||||
_module.AddCapability(SpirvCapability.Int64);
|
||||
_module.AddCapability(SpirvCapability.ImageQuery);
|
||||
if (UsesSubgroupOperations())
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ public enum SpirvCapability : uint
|
|||
ShaderViewportIndexLayerExt = 5254,
|
||||
RuntimeDescriptorArray = 5302,
|
||||
PhysicalStorageBufferAddresses = 5347,
|
||||
SignedZeroInfNanPreserve = 4466,
|
||||
}
|
||||
|
||||
public enum SpirvStorageClass : uint
|
||||
|
|
@ -242,6 +243,7 @@ public enum SpirvExecutionMode : uint
|
|||
OriginUpperLeft = 7,
|
||||
DepthReplacing = 12,
|
||||
LocalSize = 17,
|
||||
SignedZeroInfNanPreserve = 4461,
|
||||
}
|
||||
|
||||
public enum SpirvDecoration : uint
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public enum TilerBlockShape
|
|||
Prt64KB3D,
|
||||
RenderTarget64KB,
|
||||
Depth64KB,
|
||||
RenderTarget64KBGen5,
|
||||
}
|
||||
|
||||
// Assembles the tiler compute shaders: block copies per shape and element size,
|
||||
|
|
@ -251,6 +252,15 @@ public static class TilerShaders
|
|||
return offset;
|
||||
}
|
||||
|
||||
private static Term[] Gen5RenderTargetTerms(uint bytes) => bytes switch
|
||||
{
|
||||
1 => [Y(2, 0x008), Y(4, 0x010), Y(3, 0x0a0), Y(5, 0xf00), Y(6, 0x1000), Y(7, 0x4000), X(0, 0x007), X(3, 0x040), X(5, 0x300), X(4, 0x400), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
|
||||
2 => [Y(4, 0x070), Y(5, 0xf00), Y(8, 0x5000), X(1, 0x00e), X(4, 0x480), X(5, 0x300), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
|
||||
4 => [Y(4, 0x070), Y(5, 0xf00), Y(9, 0x1000), Y(8, 0x4000), X(2, 0x00c), X(5, 0x380), X(4, 0x400), X(6, 0x800), X(9, 0xa000)],
|
||||
8 => [Y(4, 0x010), Y(6, 0x080), Y(5, 0xf00), Y(10, 0x5000), X(3, 0x008), X(4, 0x460), X(5, 0x300), X(6, 0x800), X(10, 0x2000), X(9, 0x8000)],
|
||||
_ => [X(4, 0x410), X(5, 0x340), X(6, 0x800), X(11, 0xa000), Y(5, 0xf20), Y(6, 0x080), Y(10, 0x1000), Y(11, 0x4000)],
|
||||
};
|
||||
|
||||
private static Term[] DepthTerms(uint bytes) => bytes switch
|
||||
{
|
||||
1 => [X(0, 1), X(1, 0x004), X(2, 0x010), X(3, 0x040), X(5, 0x300), X(4, 0x400), X(6, 0x800), X(7, 0x2000), X(8, 0x8000), Y(1, 0x002), Y(2, 0x008), Y(3, 0x0a0), Y(5, 0xf00), Y(6, 0x1000), Y(7, 0x4000)],
|
||||
|
|
@ -314,7 +324,7 @@ public static class TilerShaders
|
|||
TilerBlockShape.Standard256B => (ThinExtent(256, bytes, 16, 8, 4), 256),
|
||||
TilerBlockShape.Standard4KB => (ThinExtent(4096, bytes, 64, 32, 16), 4096),
|
||||
TilerBlockShape.Standard4KB3D => (Thick4KBExtent(bytes), 4096),
|
||||
TilerBlockShape.Standard64KB or TilerBlockShape.Prt64KB or TilerBlockShape.RenderTarget64KB => (ThinExtent(65536, bytes, 256, 128, 64), 65536),
|
||||
TilerBlockShape.Standard64KB or TilerBlockShape.Prt64KB or TilerBlockShape.RenderTarget64KB or TilerBlockShape.RenderTarget64KBGen5 => (ThinExtent(65536, bytes, 256, 128, 64), 65536),
|
||||
TilerBlockShape.Standard64KB3D or TilerBlockShape.Prt64KB3D => (Thick64KBExtent(bytes), 65536),
|
||||
TilerBlockShape.Depth64KB => (ThinExtent(65536, bytes, 256, 128, 128), 65536),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(shape)),
|
||||
|
|
@ -348,6 +358,8 @@ public static class TilerShaders
|
|||
}
|
||||
case TilerBlockShape.RenderTarget64KB:
|
||||
return shaderModule.Xor(RenderTargetOffset(shaderModule, x, y, bytes), ZSpread(shaderModule, z));
|
||||
case TilerBlockShape.RenderTarget64KBGen5:
|
||||
return shaderModule.Xor(XorTerms(shaderModule, x, y, z, Gen5RenderTargetTerms(bytes)), ZSpread(shaderModule, z));
|
||||
case TilerBlockShape.Depth64KB:
|
||||
return shaderModule.Xor(ZSpread(shaderModule, z), XorTerms(shaderModule, x, y, z, DepthTerms(bytes)));
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -158,12 +158,14 @@ public static class Gen5ExecFullAnalysis
|
|||
written |= Bit(sdst) | Bit(sdst + 1);
|
||||
}
|
||||
|
||||
if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null)
|
||||
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||
{
|
||||
// VOPC compares write VCC, V_CMPX writes EXEC.
|
||||
written |= opcode.StartsWith("VCmpx", StringComparison.Ordinal)
|
||||
? Bit(ExecLow) | Bit(ExecHigh)
|
||||
: Bit(106) | Bit(107);
|
||||
written |= Bit(ExecLow) | Bit(ExecHigh);
|
||||
}
|
||||
else if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null)
|
||||
{
|
||||
// VOPC compares write VCC.
|
||||
written |= Bit(106) | Bit(107);
|
||||
}
|
||||
|
||||
if (opcode.StartsWith('V') && opcode.Contains("Co", StringComparison.Ordinal) && scalarDestination is null)
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public sealed class ShaderCompileRequest
|
|||
// 64-bit atomics are emitted as real 64-bit atomics instead of a pair of
|
||||
// 32-bit ones, which is not atomic as a pair.
|
||||
public bool SupportsSharedInt64Atomics { get; init; }
|
||||
public bool ShaderSignedZeroInfNanPreserveFloat32Supported { get; init; }
|
||||
public Gen5ComputeSystemRegisters? ComputeSystemRegisters { get; init; }
|
||||
|
||||
public IReadOnlyList<Gen5PixelOutputBinding> PixelOutputs { get; init; } = [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
public sealed class AudioOut2ContextGetQueueLevelExportsTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const ulong ParamAddress = MemoryBase + 0x100;
|
||||
private const ulong ContextMemoryAddress = MemoryBase + 0x200;
|
||||
private const ulong OutContextAddress = MemoryBase + 0x300;
|
||||
private const ulong LevelAddress = MemoryBase + 0x400;
|
||||
private const ulong AvailableAddress = MemoryBase + 0x404;
|
||||
private const uint QueueDepth = 7;
|
||||
|
||||
private static (CpuContext Context, FakeCpuMemory Memory, ulong Handle) CreateContext()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
Span<byte> param = stackalloc byte[0x40];
|
||||
param.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], QueueDepth);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], 512);
|
||||
Assert.True(memory.TryWrite(ParamAddress, param));
|
||||
|
||||
ctx[CpuRegister.Rdi] = ParamAddress;
|
||||
ctx[CpuRegister.Rsi] = ContextMemoryAddress;
|
||||
ctx[CpuRegister.Rdx] = 0x1000;
|
||||
ctx[CpuRegister.Rcx] = OutContextAddress;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextCreate(ctx));
|
||||
|
||||
Span<byte> handleBytes = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(OutContextAddress, handleBytes));
|
||||
return (ctx, memory, BinaryPrimitives.ReadUInt64LittleEndian(handleBytes));
|
||||
}
|
||||
|
||||
private static uint ReadU32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> value = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, value));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextGetQueueLevel_AvailableOnlyWritesQueueCapacity()
|
||||
{
|
||||
var (ctx, memory, handle) = CreateContext();
|
||||
ctx[CpuRegister.Rdi] = handle;
|
||||
ctx[CpuRegister.Rsi] = 0;
|
||||
ctx[CpuRegister.Rdx] = AvailableAddress;
|
||||
|
||||
var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(QueueDepth, ReadU32(memory, AvailableAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextGetQueueLevel_LevelOnlyWritesZero()
|
||||
{
|
||||
var (ctx, memory, handle) = CreateContext();
|
||||
ctx[CpuRegister.Rdi] = handle;
|
||||
ctx[CpuRegister.Rsi] = LevelAddress;
|
||||
ctx[CpuRegister.Rdx] = 0;
|
||||
|
||||
var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(0u, ReadU32(memory, LevelAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextGetQueueLevel_BothOutputsRemainDistinct()
|
||||
{
|
||||
var (ctx, memory, handle) = CreateContext();
|
||||
ctx[CpuRegister.Rdi] = handle;
|
||||
ctx[CpuRegister.Rsi] = LevelAddress;
|
||||
ctx[CpuRegister.Rdx] = AvailableAddress;
|
||||
|
||||
var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(0u, ReadU32(memory, LevelAddress));
|
||||
Assert.Equal(QueueDepth, ReadU32(memory, AvailableAddress));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
public sealed class AudioOut2ContextQueryMemoryExportsTests
|
||||
{
|
||||
private const int InvalidPointer = unchecked((int)0x8026_800C);
|
||||
private const ulong MemoryBase = 0x10_0000_0000;
|
||||
private const ulong ParamAddress = MemoryBase + 0x100;
|
||||
private const ulong StackPointer = MemoryBase + 0x400;
|
||||
private const ulong MemorySizeAddress = StackPointer + 0x10;
|
||||
private const ulong StaleThirdArgument = MemoryBase + 0x800;
|
||||
|
||||
[Fact]
|
||||
public void ContextQueryMemory_WritesOnlyOneSizeTAndIgnoresRdx()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
Span<byte> param = stackalloc byte[0x40];
|
||||
param.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 4);
|
||||
Assert.True(memory.TryWrite(ParamAddress, param));
|
||||
|
||||
Span<byte> guard = stackalloc byte[0x10];
|
||||
guard.Fill(0xA5);
|
||||
Assert.True(memory.TryWrite(MemorySizeAddress, guard));
|
||||
Assert.True(memory.TryWrite(StaleThirdArgument, guard));
|
||||
|
||||
ctx[CpuRegister.Rdi] = ParamAddress;
|
||||
ctx[CpuRegister.Rsi] = MemorySizeAddress;
|
||||
ctx[CpuRegister.Rdx] = StaleThirdArgument;
|
||||
ctx[CpuRegister.Rsp] = StackPointer;
|
||||
ctx[CpuRegister.Rbp] = StackPointer + 0x48;
|
||||
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextQueryMemory(ctx));
|
||||
|
||||
Span<byte> output = stackalloc byte[0x10];
|
||||
Assert.True(memory.TryRead(MemorySizeAddress, output));
|
||||
Assert.Equal(0x11640UL, BinaryPrimitives.ReadUInt64LittleEndian(output));
|
||||
Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5 }, output[8..].ToArray());
|
||||
|
||||
Span<byte> stale = stackalloc byte[0x10];
|
||||
Assert.True(memory.TryRead(StaleThirdArgument, stale));
|
||||
Assert.Equal(guard.ToArray(), stale.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextQueryMemory_RejectsMissingSecondArgument()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
ctx[CpuRegister.Rdi] = ParamAddress;
|
||||
ctx[CpuRegister.Rsi] = 0;
|
||||
ctx[CpuRegister.Rdx] = StaleThirdArgument;
|
||||
|
||||
Assert.Equal(
|
||||
InvalidPointer,
|
||||
AudioOut2Exports.AudioOut2ContextQueryMemory(ctx));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
public sealed class AudioOut2MasteringExportsTests
|
||||
{
|
||||
[Fact]
|
||||
public void MasteringSetParam_AcceptsOpaqueParameters()
|
||||
{
|
||||
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = 0x1_0000_0100;
|
||||
ctx[CpuRegister.Rsi] = 2;
|
||||
ctx[CpuRegister.Rdx] = 1;
|
||||
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||
|
||||
var result = AudioOut2Exports.AudioOut2MasteringSetParam(ctx);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MasteringSetParam_RegistersForGen5()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport("v8iOE+j8a5o", out var export));
|
||||
Assert.Equal("sceAudioOut2MasteringSetParam", export.Name);
|
||||
Assert.Equal("libSceAudioOut2", export.LibraryName);
|
||||
Assert.Equal(Generation.Gen5, export.Target);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
[Collection(AudioOutStateCollection.Name)]
|
||||
public sealed class AudioOut2PcmSnapshotTests : IDisposable
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const ulong ContextParamAddress = MemoryBase + 0x100;
|
||||
private const ulong ContextOutAddress = MemoryBase + 0x200;
|
||||
private const ulong PortParamAddress = MemoryBase + 0x300;
|
||||
private const ulong FirstPortOutAddress = MemoryBase + 0x400;
|
||||
private const ulong SecondPortOutAddress = MemoryBase + 0x408;
|
||||
private const ulong AttributeAddress = MemoryBase + 0x500;
|
||||
private const ulong PcmAttributeValueAddress = MemoryBase + 0x600;
|
||||
private const ulong ContextMemoryAddress = MemoryBase + 0x1000;
|
||||
private const ulong SharedPcmAddress = MemoryBase + 0x4000;
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x10000);
|
||||
private readonly CpuContext _ctx;
|
||||
|
||||
public AudioOut2PcmSnapshotTests()
|
||||
{
|
||||
AudioOut2Exports.ResetForTests();
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PortSetAttributes_SnapshotsPcmWhenPortsReuseGuestBuffer()
|
||||
{
|
||||
var context = CreateContext(grainSamples: 64);
|
||||
var firstPort = CreateSigned16StereoPort(context, FirstPortOutAddress);
|
||||
var secondPort = CreateSigned16StereoPort(context, SecondPortOutAddress);
|
||||
WritePcmAttribute();
|
||||
|
||||
var firstPcm = Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||
.Select(static value => unchecked((byte)(value + 1)))
|
||||
.ToArray();
|
||||
var secondPcm = Enumerable.Range(0, firstPcm.Length)
|
||||
.Select(static value => unchecked((byte)(255 - value)))
|
||||
.ToArray();
|
||||
|
||||
Assert.True(_memory.TryWrite(SharedPcmAddress, firstPcm));
|
||||
SetPcmAttribute(firstPort);
|
||||
|
||||
Assert.True(_memory.TryWrite(SharedPcmAddress, secondPcm));
|
||||
SetPcmAttribute(secondPort);
|
||||
|
||||
Assert.True(_memory.TryWrite(SharedPcmAddress, new byte[firstPcm.Length]));
|
||||
|
||||
Assert.Equal(firstPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort));
|
||||
Assert.Equal(secondPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(secondPort));
|
||||
|
||||
Assert.Equal(firstPcm, AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort));
|
||||
Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort));
|
||||
Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort));
|
||||
|
||||
Assert.Equal(secondPcm, AudioOut2Exports.TakePortPcmSnapshotForTests(secondPort));
|
||||
Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(secondPort));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData((ushort)0x0100)]
|
||||
[InlineData((ushort)6)]
|
||||
public void ContextPush_DoesNotSubmitObjectOrVibrationPorts(ushort portType)
|
||||
{
|
||||
var stream = new RecordingAudioStream();
|
||||
var openCalls = 0;
|
||||
AudioOut2Exports.SetStreamFactoryForTests(_ =>
|
||||
{
|
||||
openCalls++;
|
||||
return stream;
|
||||
});
|
||||
|
||||
var context = CreateContext(grainSamples: 64);
|
||||
var port = CreateSigned16StereoPort(context, FirstPortOutAddress, portType);
|
||||
WritePcmAttribute();
|
||||
var pcm = Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||
.Select(static value => unchecked((byte)(value + 1)))
|
||||
.ToArray();
|
||||
Assert.True(_memory.TryWrite(SharedPcmAddress, pcm));
|
||||
SetPcmAttribute(port);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = context;
|
||||
_ctx[CpuRegister.Rsi] = 1;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx));
|
||||
|
||||
Assert.Equal(0, openCalls);
|
||||
Assert.Empty(stream.Submissions);
|
||||
Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(port));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextPush_SubmitsDevicePortSnapshotOnlyOnce()
|
||||
{
|
||||
var stream = new RecordingAudioStream();
|
||||
AudioOut2Exports.SetStreamFactoryForTests(_ => stream);
|
||||
|
||||
var context = CreateContext(grainSamples: 64);
|
||||
var port = CreateSigned16StereoPort(context, FirstPortOutAddress);
|
||||
WritePcmAttribute();
|
||||
Assert.True(_memory.TryWrite(
|
||||
SharedPcmAddress,
|
||||
Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||
.Select(static value => unchecked((byte)(value + 1)))
|
||||
.ToArray()));
|
||||
SetPcmAttribute(port);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = context;
|
||||
_ctx[CpuRegister.Rsi] = 1;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx));
|
||||
Assert.Single(stream.Submissions);
|
||||
Assert.Equal(64 * 2 * sizeof(short), stream.Submissions[0].Length);
|
||||
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx));
|
||||
Assert.Single(stream.Submissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextAdvance_SubmitsDevicePortSnapshotOnlyOnce()
|
||||
{
|
||||
var stream = new RecordingAudioStream();
|
||||
AudioOut2Exports.SetStreamFactoryForTests(_ => stream);
|
||||
|
||||
var context = CreateContext(grainSamples: 64);
|
||||
var port = CreateSigned16StereoPort(context, FirstPortOutAddress);
|
||||
WritePcmAttribute();
|
||||
Assert.True(_memory.TryWrite(
|
||||
SharedPcmAddress,
|
||||
Enumerable.Range(0, 64 * 2 * sizeof(short))
|
||||
.Select(static value => unchecked((byte)(value + 1)))
|
||||
.ToArray()));
|
||||
SetPcmAttribute(port);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = context;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextAdvance(_ctx));
|
||||
Assert.Single(stream.Submissions);
|
||||
Assert.Equal(64 * 2 * sizeof(short), stream.Submissions[0].Length);
|
||||
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextAdvance(_ctx));
|
||||
Assert.Single(stream.Submissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextDestroy_RemovesOwnedPortsAndPendingSnapshots()
|
||||
{
|
||||
var firstContext = CreateContext(grainSamples: 64);
|
||||
var secondContext = CreateContext(grainSamples: 64);
|
||||
var firstPort = CreateSigned16StereoPort(firstContext, FirstPortOutAddress);
|
||||
var secondPort = CreateSigned16StereoPort(secondContext, SecondPortOutAddress);
|
||||
WritePcmAttribute();
|
||||
|
||||
byte[] firstPcm = new byte[64 * 2 * sizeof(short)];
|
||||
byte[] secondPcm = Enumerable.Repeat((byte)0x5A, firstPcm.Length).ToArray();
|
||||
Assert.True(_memory.TryWrite(SharedPcmAddress, firstPcm));
|
||||
SetPcmAttribute(firstPort);
|
||||
Assert.True(_memory.TryWrite(SharedPcmAddress, secondPcm));
|
||||
SetPcmAttribute(secondPort);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = firstContext;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextDestroy(_ctx));
|
||||
|
||||
Assert.False(AudioOut2Exports.HasPortForTests(firstPort));
|
||||
Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort));
|
||||
Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort));
|
||||
Assert.True(AudioOut2Exports.HasPortForTests(secondPort));
|
||||
Assert.Equal(secondPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(secondPort));
|
||||
}
|
||||
|
||||
public void Dispose() => AudioOut2Exports.ResetForTests();
|
||||
|
||||
private ulong CreateContext(uint grainSamples)
|
||||
{
|
||||
Span<byte> param = stackalloc byte[0x40];
|
||||
param.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 2);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], grainSamples);
|
||||
Assert.True(_memory.TryWrite(ContextParamAddress, param));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = ContextParamAddress;
|
||||
_ctx[CpuRegister.Rsi] = ContextMemoryAddress;
|
||||
_ctx[CpuRegister.Rdx] = 0x2000;
|
||||
_ctx[CpuRegister.Rcx] = ContextOutAddress;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2ContextCreate(_ctx));
|
||||
return ReadUInt64(ContextOutAddress);
|
||||
}
|
||||
|
||||
private ulong CreateSigned16StereoPort(ulong context, ulong outAddress, ushort portType = 0)
|
||||
{
|
||||
Span<byte> param = stackalloc byte[0x40];
|
||||
param.Clear();
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(param, portType);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 0x201);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
|
||||
Assert.True(_memory.TryWrite(PortParamAddress, param));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = context;
|
||||
_ctx[CpuRegister.Rsi] = PortParamAddress;
|
||||
_ctx[CpuRegister.Rdx] = outAddress;
|
||||
_ctx[CpuRegister.Rcx] = 0;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2PortCreate(_ctx));
|
||||
return ReadUInt64(outAddress);
|
||||
}
|
||||
|
||||
private void WritePcmAttribute()
|
||||
{
|
||||
Span<byte> pcm = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(pcm, SharedPcmAddress);
|
||||
Assert.True(_memory.TryWrite(PcmAttributeValueAddress, pcm));
|
||||
|
||||
Span<byte> attribute = stackalloc byte[0x18];
|
||||
attribute.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(attribute, 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x08..], PcmAttributeValueAddress);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x10..], 8);
|
||||
Assert.True(_memory.TryWrite(AttributeAddress, attribute));
|
||||
}
|
||||
|
||||
private void SetPcmAttribute(ulong port)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = port;
|
||||
_ctx[CpuRegister.Rsi] = AttributeAddress;
|
||||
_ctx[CpuRegister.Rdx] = 1;
|
||||
Assert.Equal(0, AudioOut2Exports.AudioOut2PortSetAttributes(_ctx));
|
||||
}
|
||||
|
||||
private ulong ReadUInt64(ulong address)
|
||||
{
|
||||
Span<byte> value = stackalloc byte[8];
|
||||
Assert.True(_memory.TryRead(address, value));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(value);
|
||||
}
|
||||
|
||||
private sealed class RecordingAudioStream : IHostAudioStream
|
||||
{
|
||||
public List<byte[]> Submissions { get; } = [];
|
||||
|
||||
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||
{
|
||||
Submissions.Add(stereoPcm16.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,9 +42,12 @@ public sealed class AudioOut2PortGetStateExportsTests
|
|||
// SceAudioOut2PortState: output@0, numChannels@2, volume@4.
|
||||
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
|
||||
Assert.Equal(2, state[2]);
|
||||
Assert.Equal(-1, BinaryPrimitives.ReadInt16LittleEndian(state[4..]));
|
||||
Assert.Equal(0, state[3]);
|
||||
Assert.Equal(127, BinaryPrimitives.ReadInt16LittleEndian(state[4..]));
|
||||
Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(state[6..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[8..]));
|
||||
// sizeof(SceAudioOut2PortState) is 0x40; nothing past it may be touched.
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[0x0C..]));
|
||||
Assert.All(state[0x10..0x40].ToArray(), value => Assert.Equal(0, value));
|
||||
Assert.Equal(0xAB, state[0x40]);
|
||||
Assert.Equal(0xAB, state[0x7F]);
|
||||
}
|
||||
|
|
@ -102,15 +105,19 @@ public sealed class AudioOut2PortGetStateExportsTests
|
|||
Assert.Equal(0, result);
|
||||
Span<byte> info = stackalloc byte[0x80];
|
||||
Assert.True(memory.TryRead(StateAddress, info));
|
||||
// type@0 = SPEAKER_TYPE_TV, availableBits@4 = FRONT_LEFT | FRONT_RIGHT.
|
||||
Assert.Equal(0, info[0]);
|
||||
Assert.Equal(3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
|
||||
Assert.Equal(0, info[1]);
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[2..]));
|
||||
Assert.Equal(0x3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[8..]));
|
||||
// aSpeakerAngle[0..1] = { -30, 0 }, { +30, 0 } degrees.
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[0x0C..]));
|
||||
Assert.Equal(-30, BinaryPrimitives.ReadInt16LittleEndian(info[0x10..]));
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x12..]));
|
||||
Assert.Equal(30, BinaryPrimitives.ReadInt16LittleEndian(info[0x14..]));
|
||||
// sizeof(SceAudioOut2SpeakerInfo) is 0x50.
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x16..]));
|
||||
Assert.All(info[0x18..0x50].ToArray(), value => Assert.Equal(0, value));
|
||||
Assert.Equal(0xCD, info[0x50]);
|
||||
Assert.Equal(0xCD, info[0x7F]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.AvPlayer;
|
||||
using SharpEmu.Libs.Tests.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.AvPlayer;
|
||||
|
||||
[Collection(KernelMemoryCompatStateCollection.Name)]
|
||||
public sealed class AvPlayerControlTests : IDisposable
|
||||
{
|
||||
private const int InvalidParameters = unchecked((int)0x806A0001);
|
||||
private const ulong Handle = 0xA0_0000_2400;
|
||||
private readonly CpuContext _context = new(
|
||||
new FakeCpuMemory(0x1_0000_0000, 0x1000),
|
||||
Generation.Gen5);
|
||||
|
||||
public AvPlayerControlTests()
|
||||
{
|
||||
AvPlayerExports.RegisterPlayerForTest(
|
||||
Handle,
|
||||
width: 16,
|
||||
height: 16,
|
||||
durationMilliseconds: 1000);
|
||||
_context[CpuRegister.Rdi] = Handle;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvailableBandwidthAcceptsAValidPlayerWithoutAStreamingSource()
|
||||
{
|
||||
_context[CpuRegister.Rsi] = 0;
|
||||
|
||||
Assert.Equal(0, AvPlayerExports.AvPlayerSetAvailableBandwidth(_context));
|
||||
Assert.Equal(0UL, _context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvailableBandwidthRejectsAnUnknownPlayer()
|
||||
{
|
||||
_context[CpuRegister.Rdi] = Handle + 1;
|
||||
_context[CpuRegister.Rsi] = 25_000_000;
|
||||
|
||||
Assert.Equal(
|
||||
InvalidParameters,
|
||||
AvPlayerExports.AvPlayerSetAvailableBandwidth(_context));
|
||||
Assert.Equal(
|
||||
unchecked((ulong)InvalidParameters),
|
||||
_context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
public void Dispose() => AvPlayerExports.RemovePlayerForTest(Handle);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.CommonDialog;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.CommonDialog;
|
||||
|
||||
[CollectionDefinition("WebBrowserDialog", DisableParallelization = true)]
|
||||
public sealed class WebBrowserDialogCollection;
|
||||
|
||||
[Collection("WebBrowserDialog")]
|
||||
public sealed class WebBrowserDialogExportsTests : IDisposable
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000);
|
||||
private readonly CpuContext _context;
|
||||
|
||||
public WebBrowserDialogExportsTests()
|
||||
{
|
||||
WebBrowserDialogExports.ResetForTests();
|
||||
_context = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
public void Dispose() => WebBrowserDialogExports.ResetForTests();
|
||||
|
||||
[Fact]
|
||||
public void HeadlessLifecycle_CompletesOnFirstStatusPoll()
|
||||
{
|
||||
Assert.Equal(WebBrowserDialogExports.StatusNone,
|
||||
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogInitialize(_context));
|
||||
Assert.Equal(WebBrowserDialogExports.StatusInitialized,
|
||||
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||
|
||||
_context[CpuRegister.Rdi] = MemoryBase + 0x100;
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogOpen(_context));
|
||||
Assert.Equal(WebBrowserDialogExports.StatusRunning, WebBrowserDialogExports.StatusForTests);
|
||||
Assert.Equal(WebBrowserDialogExports.StatusFinished,
|
||||
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||
|
||||
var resultAddress = MemoryBase + 0x200;
|
||||
_context[CpuRegister.Rdi] = resultAddress;
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogGetResult(_context));
|
||||
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||
Assert.True(_memory.TryRead(resultAddress, result));
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(result));
|
||||
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogTerminate(_context));
|
||||
Assert.Equal(WebBrowserDialogExports.StatusNone,
|
||||
WebBrowserDialogExports.WebBrowserDialogGetStatus(_context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Close_FinishesRunningDialogWithoutPolling()
|
||||
{
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogInitialize(_context));
|
||||
_context[CpuRegister.Rdi] = MemoryBase + 0x100;
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogOpenForPredeterminedContent(_context));
|
||||
Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogClose(_context));
|
||||
Assert.Equal(WebBrowserDialogExports.StatusFinished, WebBrowserDialogExports.StatusForTests);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class GuestExceptionContextLayoutTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ExceptionContext_UsesBareMcontextAndSonyStackAlias(bool saved)
|
||||
{
|
||||
const ulong address = 0x1_0000_0100;
|
||||
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5)
|
||||
{
|
||||
Rip = 0x101000,
|
||||
Rflags = 0x246,
|
||||
FsBase = 0x303000,
|
||||
GsBase = 0x404000,
|
||||
};
|
||||
context[CpuRegister.Rdi] = 0x11;
|
||||
context[CpuRegister.Rbx] = 0x22;
|
||||
context[CpuRegister.Rsp] = 0x202000;
|
||||
var continuation = saved ? new GuestCpuContinuation
|
||||
{
|
||||
Rip = 0x505000,
|
||||
Rsp = 0x606000,
|
||||
Rflags = 0x203,
|
||||
Rdi = 0x33,
|
||||
Rbx = 0x44,
|
||||
FsBase = 0x707000,
|
||||
GsBase = 0x808000,
|
||||
} : default;
|
||||
|
||||
Assert.True(DirectExecutionBackend.TryWriteGuestExceptionContext(context, address, continuation, 0x500));
|
||||
var bytes = new byte[0x500];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
|
||||
Assert.Equal(saved ? continuation.Rdi : context[CpuRegister.Rdi], Read64(0x08));
|
||||
Assert.Equal(saved ? continuation.Rbx : context[CpuRegister.Rbx], Read64(0x40));
|
||||
Assert.Equal(saved ? continuation.Rip : context.Rip, Read64(0xA0));
|
||||
Assert.Equal(saved ? continuation.Rflags : context.Rflags, Read64(0xB0));
|
||||
Assert.Equal(saved ? continuation.Rsp : context[CpuRegister.Rsp], Read64(0xB8));
|
||||
Assert.Equal(Read64(0xB8), Read64(0xF8));
|
||||
Assert.Equal(0x480UL, Read64(0xC8));
|
||||
Assert.Equal(saved ? continuation.FsBase : context.FsBase, Read64(0x440));
|
||||
Assert.Equal(saved ? continuation.GsBase : context.GsBase, Read64(0x448));
|
||||
|
||||
ulong Read64(int offset) => BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class GuestThreadStackBoundsTests
|
||||
{
|
||||
[NativeX64Fact]
|
||||
public void SchedulerReturnsAuthoritativeBoundsAcrossTheFullSlotWindow()
|
||||
{
|
||||
const BindingFlags privateInstance = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
const ulong stackSize = 0x20_0000;
|
||||
const ulong stackStride = 0x100_0000;
|
||||
var highestStack = OperatingSystem.IsWindows()
|
||||
? 0x0000_7FFF_E000_0000UL
|
||||
: 0x0000_6FFF_E000_0000UL;
|
||||
var backendType = typeof(DirectExecutionBackend);
|
||||
var threadType = backendType.GetNestedType("GuestThreadState", BindingFlags.NonPublic)!;
|
||||
var modules = new ModuleManager();
|
||||
modules.Freeze();
|
||||
using var backend = new DirectExecutionBackend(modules);
|
||||
var gate = backendType.GetField("_guestThreadGate", privateInstance)!.GetValue(backend)!;
|
||||
var threads = (IDictionary)backendType.GetField("_guestThreads", privateInstance)!.GetValue(backend)!;
|
||||
|
||||
foreach (var slot in new[] { 0, 48, 56, 1023 })
|
||||
{
|
||||
var handle = 0x1000UL + (ulong)slot;
|
||||
var expectedBase = highestStack - ((ulong)slot * stackStride);
|
||||
var thread = Activator.CreateInstance(threadType)!;
|
||||
threadType.GetProperty("ThreadHandle")!.SetValue(thread, handle);
|
||||
threadType.GetProperty("StackBase")!.SetValue(thread, expectedBase);
|
||||
threadType.GetProperty("StackSize")!.SetValue(thread, stackSize);
|
||||
lock (gate)
|
||||
threads.Add(handle, thread);
|
||||
|
||||
Assert.True(backend.TryGetGuestThreadStackBounds(handle, out var actualBase, out var actualSize));
|
||||
Assert.Equal(expectedBase, actualBase);
|
||||
Assert.Equal(stackSize, actualSize);
|
||||
}
|
||||
|
||||
Assert.False(backend.TryGetGuestThreadStackBounds(0xDEAD, out var missingBase, out var missingSize));
|
||||
Assert.Equal(0UL, missingBase);
|
||||
Assert.Equal(0UL, missingSize);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,33 +48,6 @@ public sealed class GuestBufferAsyncReadbackTests(HeadlessVulkanFixture fixture)
|
|||
harness.Shutdown();
|
||||
}
|
||||
|
||||
// Without a readback queue the main queue carries the download and the guest thread waits
|
||||
// for that submission; the worker records the copies and moves on.
|
||||
[Fact]
|
||||
public void AGuestReadWithoutAReadbackQueueStillWaitsOffTheQueueThread()
|
||||
{
|
||||
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
using var fatal = new FatalScope();
|
||||
Assert.Null(harness.Cache.AsyncReadback);
|
||||
var address = harness.MapBacked(0x10000, ReadWrite);
|
||||
GpuWrite(harness, address, 0x55667788);
|
||||
Assert.True(harness.Cache.HasGpuDirtyBytes(address, 0x100));
|
||||
|
||||
Assert.True(harness.Cache.DownloadToCpu(address + 8, 4));
|
||||
|
||||
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
|
||||
Assert.Equal((1L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
|
||||
Assert.Equal(0x55667788u, BitConverter.ToUInt32(harness.Read(address + 0xF0, 4)));
|
||||
|
||||
// A second write after the first read is picked up by the next read, not served stale.
|
||||
GpuWrite(harness, address, 0x99AABBCCu);
|
||||
Assert.True(harness.Cache.DownloadToCpu(address, 4));
|
||||
Assert.Equal(0x99AABBCCu, BitConverter.ToUInt32(harness.Read(address + 0x40, 4)));
|
||||
Assert.Equal((2L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
private static void GpuWrite(CacheHarness harness, ulong address, uint value) => harness.Worker.Run(() =>
|
||||
{
|
||||
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
||||
|
|
|
|||
|
|
@ -154,6 +154,83 @@ public sealed class CommandStreamQueueTests
|
|||
Assert.Equal(new[] { "begin 0 1", "begin 1 3", "begin 0 2" }, host.Calls.Where(call => call.StartsWith("begin", StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ControlBarrier_RunsBetweenTheAcceptedSliceAndLaterAdmissions()
|
||||
{
|
||||
var (host, queue) = NewQueue();
|
||||
Enqueue(host, queue, Graphics, 1, CreateInstanceCountPacket(1));
|
||||
var cancelled = false;
|
||||
host.PendingCommands.Enqueue(() =>
|
||||
{
|
||||
Assert.True(queue.TryEnqueueControlBarrier(
|
||||
() => host.Calls.Add("mapping_change"),
|
||||
() => cancelled = true));
|
||||
Enqueue(host, queue, Graphics + 0x100, 2, CreateInstanceCountPacket(2));
|
||||
});
|
||||
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
Assert.DoesNotContain("mapping_change", host.Calls);
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
|
||||
var first = host.Calls.IndexOf("begin 0 1");
|
||||
var barrier = host.Calls.IndexOf("mapping_change");
|
||||
var second = host.Calls.IndexOf("begin 0 2");
|
||||
Assert.True(first >= 0 && barrier > first && second > barrier, string.Join(", ", host.Calls));
|
||||
Assert.False(cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ControlBarrier_PreservesCrossQueueProgressWhileAnOlderHeadIsBlocked()
|
||||
{
|
||||
var (host, queue) = NewQueue();
|
||||
host.WriteDword(Label, 0);
|
||||
Enqueue(host, queue, Graphics, 1, WaitEqual(Label, 1));
|
||||
Assert.Equal(SliceResult.BlockedWithoutProgress, queue.ProcessOne());
|
||||
Assert.True(queue.TryEnqueueControlBarrier(() => host.Calls.Add("mapping_change"), () => { }));
|
||||
EnqueueCompute(host, queue, 0x20, Compute, 2, CreateInstanceCountPacket(2));
|
||||
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
Assert.DoesNotContain("mapping_change", host.Calls);
|
||||
host.WriteDword(Label, 1);
|
||||
queue.RetryBlocked();
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
|
||||
var compute = host.Calls.IndexOf("begin 1 2");
|
||||
var retriedGraphics = host.Calls.FindLastIndex(call => call == "begin 0 1");
|
||||
var barrier = host.Calls.IndexOf("mapping_change");
|
||||
Assert.True(compute >= 0 && retriedGraphics > compute && barrier > retriedGraphics, string.Join(", ", host.Calls));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ControlBarrier_IsCancelledWhenTheQueueIsDiscarded()
|
||||
{
|
||||
var (_, queue) = NewQueue();
|
||||
var ran = false;
|
||||
var cancelled = false;
|
||||
Assert.True(queue.TryEnqueueControlBarrier(() => ran = true, () => cancelled = true));
|
||||
|
||||
queue.DiscardAll();
|
||||
|
||||
Assert.False(ran);
|
||||
Assert.True(cancelled);
|
||||
Assert.False(queue.HasPending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShutdownDrain_RunsAnAcceptedControlBarrier()
|
||||
{
|
||||
var (_, queue) = NewQueue();
|
||||
var ran = false;
|
||||
Assert.True(queue.TryEnqueueControlBarrier(() => ran = true, () => { }));
|
||||
queue.StopAccepting();
|
||||
|
||||
Assert.Equal(IdleOutcome.Completed, queue.DrainForShutdown(cancelBlockedOnNoProgress: true));
|
||||
Assert.True(ran);
|
||||
Assert.False(queue.HasPending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlockedHead_IsRequeuedAndRetriedWithoutStoppingOtherQueues()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ internal static class TilerCases
|
|||
transfer.Tail = true;
|
||||
break;
|
||||
case Shape.Layered:
|
||||
if (kind is not (TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB))
|
||||
if (kind is not (TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KBGen5))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ public sealed class GpuTilerTests : IClassFixture<HeadlessVulkanFixture>
|
|||
public void CpuTwinCases_CoverEveryKindAndElementSize()
|
||||
{
|
||||
var cases = TilerCases.All().ToList();
|
||||
Assert.Equal(44 + 39 + 9, cases.Count);
|
||||
Assert.Equal((44 + 39 + 9) + 15, cases.Count);
|
||||
Assert.All(cases, tilerCase => Assert.True(tilerCase.Transfer.SurfaceZ != 0 || tilerCase.Tiled.Length == (int)tilerCase.Transfer.TiledSize));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,31 +100,31 @@ public sealed partial class GuestImageCacheTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void DccFills_AreAbsorbedOnlyByRegisteredDcc()
|
||||
public void DccFills_RemainGuestProducersUntilDescriptorDiscovery()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var fatal = new FatalScope();
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x20000, ReadWrite);
|
||||
var metadata = address + 0x10000;
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0x20202020u));
|
||||
harness.Write(metadata, Bytes(0x11223344u));
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0x20202020u));
|
||||
Assert.False(harness.Images.IsMetadata(metadata));
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0x40404040u));
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0x40404040u));
|
||||
|
||||
harness.Write(address + 0xc000, Bytes(0x55667788u));
|
||||
var target = LinearRequest(address + 0xc000, 4, Format.R8G8B8A8Srgb, GuestPixelFormat.Bits8_8_8_8Srgb, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
|
||||
target = AsColorTarget(target);
|
||||
target.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
target.Description.Metadata.Range = new GuestSpan(metadata, 0x100);
|
||||
target.Description.Metadata.Range = new GuestSpan(metadata, 0x1000);
|
||||
target.Description.Metadata.Compression = DisplayCompression.Dcc256_256_0;
|
||||
var targetId = harness.Acquire(ref target);
|
||||
Assert.True(harness.Images.IsMetadata(metadata));
|
||||
Assert.True(harness.Images.IsMetadataCleared(metadata, 0, out var fill));
|
||||
Assert.Equal(0x40404040u, fill);
|
||||
Assert.True(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0x01010101u));
|
||||
Assert.False(harness.Images.IsMetadata(metadata));
|
||||
Assert.False(harness.Images.IsMetadataCleared(metadata, 0));
|
||||
Assert.True(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0xc0c0c0c0u));
|
||||
Assert.True(harness.Images.IsMetadataCleared(metadata, 5));
|
||||
Assert.Equal(0x11223344u, harness.ReadUInt32(metadata));
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0x01010101u));
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0xc0c0c0c0u));
|
||||
Assert.False(harness.Images.IsMetadataCleared(metadata, 5));
|
||||
Assert.Equal(0x11223344u, harness.ReadUInt32(metadata));
|
||||
Assert.False(harness.Images.ClearMetadata(metadata));
|
||||
|
||||
// A compressed target rejects readback and read claims but keeps GPU ownership.
|
||||
|
|
@ -144,15 +144,169 @@ public sealed partial class GuestImageCacheTests
|
|||
Assert.False(videoImage.Binding.NeedsRebind);
|
||||
Assert.False(videoImage.Uses.VideoOut);
|
||||
|
||||
// A depth target must not reuse DCC metadata.
|
||||
var depth = LinearRequest(address + 0xe000, 4, Format.D32Sfloat, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
|
||||
depth = AsDepthTarget(depth, Format.D32Sfloat);
|
||||
depth.Description.Metadata.Kind = MetadataKind.Htile;
|
||||
depth.Description.Metadata.Range = new GuestSpan(metadata, 0x100);
|
||||
var depthId = harness.Find(ref depth);
|
||||
var depthLocal = depth;
|
||||
harness.Worker.Run(() => Assert.Throws<SchedulerFatalException>(() => harness.Images.AcquireDepthTargetView(depthId, depthLocal)));
|
||||
Assert.Contains(fatal.Messages, message => message.Contains("depth target reuses metadata that is not HTile"));
|
||||
harness.Worker.Run(() => Assert.NotEqual(
|
||||
0UL,
|
||||
harness.Images.AcquireDepthTargetView(depthId, depthLocal).Handle));
|
||||
Assert.True(harness.Images.IsMetadata(metadata));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DccVolumeOverThirtyTwoSlices_MaterializesAndConsumesEveryUniformSlice()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x80000, ReadWrite);
|
||||
var imageAddress = address + 0x1000;
|
||||
var metadataAddress = address + 0x10000;
|
||||
const uint depth = 64;
|
||||
const ulong metadataSliceSize = 0x1000;
|
||||
var metadataBytes = new byte[checked((int)(depth * metadataSliceSize))];
|
||||
Array.Fill(metadataBytes, (byte)0xc0);
|
||||
harness.Write(metadataAddress, metadataBytes);
|
||||
|
||||
const uint width = 4;
|
||||
const uint height = 4;
|
||||
var imageSize = (ulong)width * height * depth * 4;
|
||||
var target = LinearRequest(
|
||||
imageAddress,
|
||||
imageSize,
|
||||
Format.R8G8B8A8Unorm,
|
||||
GuestPixelFormat.Bits8_8_8_8UNorm,
|
||||
GuestImageType.Color3D,
|
||||
new Extent3D(width, height, depth),
|
||||
1,
|
||||
4,
|
||||
1);
|
||||
target = AsColorTarget(target);
|
||||
target.View = target.View with
|
||||
{
|
||||
Type = ImageViewType.Type2DArray,
|
||||
BaseLayer = 0,
|
||||
LayerCount = depth,
|
||||
};
|
||||
target.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
target.Description.Metadata.Range = new GuestSpan(metadataAddress, depth * metadataSliceSize);
|
||||
target.Description.Metadata.DccAlphaMsb = true;
|
||||
|
||||
Assert.False(harness.Images.TryAbsorbDccFill(
|
||||
metadataAddress,
|
||||
depth * metadataSliceSize,
|
||||
0xc0c0c0c0u));
|
||||
var targetId = harness.Find(ref target);
|
||||
harness.Finish();
|
||||
|
||||
Assert.True(targetId.IsValid);
|
||||
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress));
|
||||
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress + 31 * metadataSliceSize));
|
||||
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress + 32 * metadataSliceSize));
|
||||
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress + (depth - 1) * metadataSliceSize));
|
||||
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 0));
|
||||
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 31));
|
||||
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 32));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelflossShapeDccVolume_HandlesOneHundredTwentyEightSlicesIndependently()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x140000, ReadWrite);
|
||||
var imageAddress = address + 0x1000;
|
||||
var metadataAddress = address + 0x20000;
|
||||
const uint depth = 128;
|
||||
const ulong metadataSliceSize = 0x2000;
|
||||
var metadataBytes = new byte[checked((int)(depth * metadataSliceSize))];
|
||||
Array.Fill(metadataBytes, (byte)0xc0);
|
||||
const uint mixedSlice = 73;
|
||||
Array.Fill(
|
||||
metadataBytes,
|
||||
(byte)0x55,
|
||||
checked((int)(mixedSlice * metadataSliceSize + 1)),
|
||||
checked((int)metadataSliceSize - 1));
|
||||
harness.Write(metadataAddress, metadataBytes);
|
||||
|
||||
const uint width = 4;
|
||||
const uint height = 4;
|
||||
var imageSize = (ulong)width * height * depth * 8;
|
||||
var texture = LinearRequest(
|
||||
imageAddress,
|
||||
imageSize,
|
||||
Format.R16G16B16A16Sfloat,
|
||||
GuestPixelFormat.Bits16_16_16_16Float,
|
||||
GuestImageType.Color3D,
|
||||
new Extent3D(width, height, depth),
|
||||
1,
|
||||
8,
|
||||
1);
|
||||
texture.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
texture.Description.Metadata.Range = new GuestSpan(
|
||||
metadataAddress,
|
||||
depth * metadataSliceSize);
|
||||
texture.Description.Metadata.DccAlphaMsb = true;
|
||||
|
||||
var textureId = harness.Find(ref texture);
|
||||
harness.Finish();
|
||||
|
||||
Assert.True(textureId.IsValid);
|
||||
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress));
|
||||
Assert.Equal(
|
||||
0x555555c0u,
|
||||
harness.ReadUInt32(metadataAddress + mixedSlice * metadataSliceSize));
|
||||
Assert.Equal(
|
||||
uint.MaxValue,
|
||||
harness.ReadUInt32(metadataAddress + (depth - 1) * metadataSliceSize));
|
||||
Assert.False(harness.Images.IsMetadata(metadataAddress));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DccMetadataScan_RejectsAMismatchBeyondTheFirstPooledChunk()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x50000, ReadWrite);
|
||||
var imageAddress = address + 0x1000;
|
||||
var metadataAddress = address + 0x10000;
|
||||
const uint layers = 2;
|
||||
const ulong metadataSliceSize = 0x11000;
|
||||
var metadataBytes = new byte[checked((int)(layers * metadataSliceSize))];
|
||||
Array.Fill(metadataBytes, (byte)0xc0);
|
||||
metadataBytes[^1] = 0x55;
|
||||
harness.Write(metadataAddress, metadataBytes);
|
||||
|
||||
const uint width = 4;
|
||||
const uint height = 4;
|
||||
var texture = LinearRequest(
|
||||
imageAddress,
|
||||
(ulong)width * height * layers * 4,
|
||||
Format.R8G8B8A8Unorm,
|
||||
GuestPixelFormat.Bits8_8_8_8UNorm,
|
||||
GuestImageType.Color2D,
|
||||
new Extent3D(width, height, 1),
|
||||
layers,
|
||||
4,
|
||||
1);
|
||||
texture.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
texture.Description.Metadata.Range = new GuestSpan(
|
||||
metadataAddress,
|
||||
layers * metadataSliceSize);
|
||||
texture.Description.Metadata.DccAlphaMsb = true;
|
||||
|
||||
var textureId = harness.Find(ref texture);
|
||||
harness.Finish();
|
||||
|
||||
Assert.True(textureId.IsValid);
|
||||
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress));
|
||||
Assert.Equal(0xc0c0c0c0u, harness.ReadUInt32(metadataAddress + metadataSliceSize));
|
||||
Assert.Equal(0x55, harness.Read(metadataAddress + 2 * metadataSliceSize - 1, 1)[0]);
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +350,44 @@ public sealed partial class GuestImageCacheTests
|
|||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DccDescriptor_SynchronizesMetadataOnAnImageFoundWithoutIt(bool native)
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
const ulong SliceSize = 0x1000;
|
||||
var address = harness.MapBacked(0x20000, ReadWrite);
|
||||
var metadata = address + 0x10000;
|
||||
var plain = LinearRequest(address + 0xc000, 4, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
|
||||
var imageId = harness.Find(ref plain);
|
||||
var image = harness.Image(imageId);
|
||||
image.Description.TileMode = GuestTileMode.RenderTarget;
|
||||
Assert.Equal(MetadataKind.None, image.Description.Metadata.Kind);
|
||||
harness.Write(metadata, Enumerable.Repeat((byte)0x55, (int)SliceSize).ToArray());
|
||||
harness.Worker.Run(() =>
|
||||
{
|
||||
Assert.NotNull(harness.Cache.ObtainBuffer(metadata, SliceSize, isWritten: true).Buffer);
|
||||
harness.Cache.FillBuffer(metadata, SliceSize, 0, isGds: false);
|
||||
});
|
||||
Assert.True(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize));
|
||||
|
||||
var compressed = plain;
|
||||
compressed.Description.TileMode = GuestTileMode.RenderTarget;
|
||||
compressed.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
compressed.Description.Metadata.Range = new GuestSpan(metadata, native ? SliceSize : 0);
|
||||
Assert.Equal(imageId, harness.Find(ref compressed));
|
||||
Assert.Equal(MetadataKind.Dcc, image.Description.Metadata.Kind);
|
||||
Assert.Equal(metadata, image.Description.Metadata.Range.Address);
|
||||
Assert.Equal(SliceSize, image.Description.DccSliceSize);
|
||||
Assert.False(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize));
|
||||
harness.Finish();
|
||||
Assert.Equal(native ? uint.MaxValue : 0u, harness.ReadUInt32(metadata));
|
||||
Assert.Equal(!native, harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out _));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuestDccClear_ReadsOnlyUniformClearSlices()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -154,6 +154,21 @@ public sealed class ImageDescriptionTests
|
|||
Assert.False(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Unsupported, renderTarget: true, gpuModified: true, guestModified: false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 0.0f, 1.0f)]
|
||||
[InlineData(false, 1.0f, 0.0f)]
|
||||
public void DccFixedClear_RespectsTheAlphaPosition(bool alphaMsb, float red, float alpha)
|
||||
{
|
||||
var metadata = new MetadataDescription { DccAlphaMsb = alphaMsb };
|
||||
Assert.True(PackedClearValue.TryDecodeDccColor(
|
||||
Format.A2B10G10R10UnormPack32, 0x40404040, metadata, false, out var clear));
|
||||
Assert.Equal((red, 0.0f, 0.0f, alpha), (clear.Float32_0, clear.Float32_1, clear.Float32_2, clear.Float32_3));
|
||||
Assert.False(PackedClearValue.TryDecodeDccColor(
|
||||
Format.A2B10G10R10UnormPack32, 0x40400040, metadata, false, out _));
|
||||
Assert.False(PackedClearValue.TryDecodeDccColor(
|
||||
Format.A2B10G10R10UnormPack32, 0xffffffff, metadata, false, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackedClears_DecodePerFormat()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,21 +74,40 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(GuestImageType.Color2D, 1u, false)]
|
||||
[InlineData(GuestImageType.Color2D, 1u, true)]
|
||||
[InlineData(GuestImageType.Color3D, 32u, false)]
|
||||
[InlineData(GuestImageType.Color3D, 32u, true)]
|
||||
[InlineData(GuestImageType.Color2DArray, 4u, false)]
|
||||
public void CompressedTexture_CarriesDccMetadataOnlyForSingleLayer2D(GuestImageType type, uint layers, bool expected)
|
||||
[InlineData(GuestImageType.Color2DArray, 4u, true)]
|
||||
public void CompressedTexture_CarriesDccMetadataForEverySlice(GuestImageType type, uint layers, bool storage)
|
||||
{
|
||||
const ulong metadata = 0x1_2000_0000;
|
||||
var words = RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8_8_8UNorm, 32, 32, type, GuestTileMode.RenderTarget, layers: layers);
|
||||
words[6] |= (1u << 21) | (uint)(((metadata >> 8) & 0xFF) << 24);
|
||||
words[7] = (uint)(metadata >> 16);
|
||||
var shape = Sampled2D with { Volume = type == GuestImageType.Color3D, Arrayed = type == GuestImageType.Color2DArray };
|
||||
var shape = Sampled2D with { Volume = type == GuestImageType.Color3D, Arrayed = type == GuestImageType.Color2DArray, Storage = storage };
|
||||
|
||||
var description = ImageRequestBuilders.Texture(words, shape).Request.Description;
|
||||
|
||||
Assert.Equal(expected ? MetadataKind.Dcc : MetadataKind.None, description.Metadata.Kind);
|
||||
Assert.Equal(expected ? metadata : 0UL, description.Metadata.Range.Address);
|
||||
Assert.Equal(MetadataKind.Dcc, description.Metadata.Kind);
|
||||
Assert.Equal(metadata, description.Metadata.Range.Address);
|
||||
Assert.Equal((ulong)layers * 0x1000, description.Metadata.Range.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemonSlayerColorLut_CarriesItsVolumeDccFootprintAndAlphaPosition()
|
||||
{
|
||||
uint[] words = [0x606D4E00, 0xC3200000, 0x0007C007, 0xA1B00FAC, 0x0000001F, 0x00700000, 0x006B0000, 0x00606D6E];
|
||||
var description = ImageRequestBuilders.Texture(words, Sampled2D with { Volume = true }).Request.Description;
|
||||
|
||||
Assert.Equal(GuestImageType.Color3D, description.Type);
|
||||
Assert.Equal(new Extent3D(32, 32, 32), description.Extent);
|
||||
Assert.Equal(Format.A2B10G10R10UnormPack32, description.PixelFormat);
|
||||
Assert.Equal(MetadataKind.Dcc, description.Metadata.Kind);
|
||||
Assert.Equal(0x606D6E0000UL, description.Metadata.Range.Address);
|
||||
Assert.Equal(0x20000UL, description.Metadata.Range.Size);
|
||||
Assert.True(description.Metadata.DccAlphaMsb);
|
||||
}
|
||||
|
||||
private readonly HeadlessVulkan? _vulkan;
|
||||
|
|
@ -361,6 +380,38 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
|
|||
Assert.False(value.StencilClearEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthTarget_StencilIsCompressedOnlyWithHtileBacking()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
const ulong stencilBase = Base + 0x80000;
|
||||
const ulong htileBase = Base + 0x100000;
|
||||
var words = RegisterWords.Depth(Base, 64, 64, stencilBase: stencilBase) with
|
||||
{
|
||||
ZInfo = (uint)GuestDepthFormat.Z32Float | (1u << 29),
|
||||
StencilInfo = 0x00100981,
|
||||
HtileBase = htileBase,
|
||||
};
|
||||
|
||||
var resolution = ImageRequestBuilders.DepthTarget(words, _vulkan.DeviceInfo);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
var value = resolution.Value;
|
||||
Assert.True(value.HasStencil);
|
||||
Assert.True(value.HasHtile);
|
||||
Assert.Equal(MetadataKind.Htile, value.Request.Description.Metadata.Kind);
|
||||
Assert.Equal(htileBase, value.Request.Description.Metadata.Range.Address);
|
||||
Assert.True(value.Request.Description.Metadata.StencilCompressed);
|
||||
|
||||
var missingHtile = words with { ZInfo = (uint)GuestDepthFormat.Z32Float, HtileBase = 0 };
|
||||
var uncompressed = ImageRequestBuilders.DepthTarget(missingHtile, _vulkan.DeviceInfo);
|
||||
|
||||
Assert.NotNull(uncompressed);
|
||||
Assert.True(uncompressed.Value.HasStencil);
|
||||
Assert.False(uncompressed.Value.HasHtile);
|
||||
Assert.False(uncompressed.Value.Request.Description.Metadata.StencilCompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthTarget_NoAttachmentWhenNothingIsActive()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ public sealed class TextureTransferLayoutTests
|
|||
var target = TextureTransferLayout.Compute(GuestPixelFormat.Bits8_8_8_8UNorm, 128, 128, 1, 2, GuestTileMode.RenderTarget, 0, false, false, "test");
|
||||
Assert.True(target.TryBuildTileTransfers(2 * 65536, target.BuildCopies(), 1, out var targetTransfers));
|
||||
Assert.Equal(1u, targetTransfers[1].SurfaceZ);
|
||||
Assert.Equal(TileBlockKind.RenderTarget64KB, targetTransfers[1].Kind);
|
||||
Assert.Equal(TileBlockKind.RenderTarget64KBGen5, targetTransfers[1].Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Gpu.Images;
|
||||
|
|
@ -39,6 +40,7 @@ public sealed class TileGeometryTests
|
|||
[InlineData(TileBlockKind.Prt64KB3D, 65536u, 16u)]
|
||||
[InlineData(TileBlockKind.RenderTarget64KB, 65536u, 16u)]
|
||||
[InlineData(TileBlockKind.Depth64KB, 65536u, 8u)]
|
||||
[InlineData(TileBlockKind.RenderTarget64KBGen5, 65536u, 16u)]
|
||||
public void BlockLayouts_CoverEveryElementSize(TileBlockKind kind, uint blockSize, uint maxBytes)
|
||||
{
|
||||
for (uint bytes = 1; bytes <= 16; bytes *= 2)
|
||||
|
|
@ -107,6 +109,83 @@ public sealed class TileGeometryTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1u, 0x0B75u, 0x8000u, 0x4000u)]
|
||||
[InlineData(2u, 0x0B7Au, 0x8000u, 0x4100u)]
|
||||
[InlineData(4u, 0x0BF4u, 0x8100u, 0x4100u)]
|
||||
[InlineData(8u, 0x1BD8u, 0x4100u, 0x8800u)]
|
||||
[InlineData(16u, 0x3BB0u, 0x4400u, 0x8800u)]
|
||||
public void RenderTargetBlockOffsets_MatchRbPlusReferenceSpotValues(
|
||||
uint bytes,
|
||||
uint elementOffset,
|
||||
uint xBlockXor,
|
||||
uint yBlockXor)
|
||||
{
|
||||
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KB, bytes, out var layout));
|
||||
Assert.True(TileGeometry.TryGetBlockOffset(layout, 37, 23, 0, out var offset));
|
||||
Assert.Equal(elementOffset, offset);
|
||||
|
||||
Assert.True(TileGeometry.TryGetBlockXor(layout, 1, 0, out var blockX));
|
||||
Assert.Equal(xBlockXor, blockX);
|
||||
Assert.True(TileGeometry.TryGetBlockXor(layout, 0, 1, out var blockY));
|
||||
Assert.Equal(yBlockXor, blockY);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1u, 0x0b75u, 0x0abdu)]
|
||||
[InlineData(2u, 0x0b7au, 0x1a7au)]
|
||||
[InlineData(4u, 0x0bf4u, 0x0af4u)]
|
||||
[InlineData(8u, 0x1bd8u, 0x5ad8u)]
|
||||
[InlineData(16u, 0x3bb0u, 0x3ab0u)]
|
||||
public void Gen5RenderTargetBlockOffsets_MatchReferenceAndDifferFromRbPlus(
|
||||
uint bytes,
|
||||
uint expectedRbPlus,
|
||||
uint expectedGen5)
|
||||
{
|
||||
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KB, bytes, out var rbPlus));
|
||||
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KBGen5, bytes, out var gen5));
|
||||
|
||||
Assert.True(TileGeometry.TryGetBlockOffset(rbPlus, 37, 23, 0, out var rbPlusOffset));
|
||||
Assert.True(TileGeometry.TryGetBlockOffset(gen5, 37, 23, 0, out var gen5Offset));
|
||||
Assert.Equal(expectedRbPlus, rbPlusOffset);
|
||||
Assert.Equal(expectedGen5, gen5Offset);
|
||||
Assert.NotEqual(rbPlusOffset, gen5Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gen5RenderTargetGeometry_MatchesTheReferenceVolumeAllocation()
|
||||
{
|
||||
const uint width = 480;
|
||||
const uint height = 270;
|
||||
const uint depth = 128;
|
||||
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KBGen5, 8, out var block));
|
||||
Assert.Equal((128u, 64u, 1u), (block.BlockWidth, block.BlockHeight, block.BlockDepth));
|
||||
|
||||
var paddedWidth = (width + block.BlockWidth - 1) / block.BlockWidth * block.BlockWidth;
|
||||
var paddedHeight = (height + block.BlockHeight - 1) / block.BlockHeight * block.BlockHeight;
|
||||
var blocksPerSlice = (ulong)(paddedWidth / block.BlockWidth) * (paddedHeight / block.BlockHeight);
|
||||
var sliceSize = blocksPerSlice * block.BlockSize;
|
||||
var totalSize = sliceSize * depth;
|
||||
|
||||
Assert.Equal((512u, 320u), (paddedWidth, paddedHeight));
|
||||
Assert.Equal(0x140000UL, sliceSize);
|
||||
Assert.Equal(0xA000000UL, totalSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CpuAndShaderBlockKindOrdinals_StayAligned()
|
||||
{
|
||||
var cpuKinds = Enum.GetValues<TileBlockKind>();
|
||||
var shaderKinds = Enum.GetValues<TilerBlockShape>();
|
||||
Assert.Equal(cpuKinds.Length, shaderKinds.Length);
|
||||
|
||||
for (var index = 0; index < cpuKinds.Length; index++)
|
||||
{
|
||||
Assert.Equal(cpuKinds[index].ToString(), shaderKinds[index].ToString());
|
||||
Assert.Equal((uint)cpuKinds[index], (uint)shaderKinds[index]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MipTail_StartsWhereTheLevelFitsAndOffsetsRunBackwards()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ public sealed class TilerParityTests : IClassFixture<HeadlessVulkanFixture>
|
|||
foreach (var tilerCase in TilerCases.All())
|
||||
{
|
||||
var transfer = tilerCase.Transfer;
|
||||
if (transfer.Kind == TileBlockKind.RenderTarget64KBGen5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var reference = blobs[(int)transfer.Kind]!;
|
||||
foreach (var toTiled in new[] { false, true })
|
||||
{
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public sealed class ShaderPrewarmListTests : IDisposable
|
|||
Assert.True(
|
||||
ShaderProgramCache.TryCompilePrewarm(
|
||||
record, code, new FakeShaderCompiler(Compile), host.SharedInt64AtomicsEnabled, host.ExecGuardElisionEnabled,
|
||||
out var compiled, out var layout, out var error),
|
||||
host.ShaderSignedZeroInfNanPreserveFloat32Supported, out var compiled, out var layout, out var error),
|
||||
error);
|
||||
Assert.NotNull(layout);
|
||||
Assert.Equal(runtime, Assert.IsType<FakeCompiledShader>(compiled).Spirv);
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ public sealed class GpuWorkerRelayTests : IDisposable
|
|||
private readonly Thread _thread;
|
||||
private bool _stop;
|
||||
|
||||
public TestWorker()
|
||||
public TestWorker(Func<Action, Action, bool>? tryEnqueueAfterPendingWork = null)
|
||||
{
|
||||
Relay = new GpuWorkerRelay(Wake);
|
||||
Relay = new GpuWorkerRelay(Wake, tryEnqueueAfterPendingWork);
|
||||
_thread = new Thread(Run) { IsBackground = true };
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +160,116 @@ public sealed class GpuWorkerRelayTests : IDisposable
|
|||
device.Complete(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AfterPendingWork_BypassesTheCommandStreamBarrier()
|
||||
{
|
||||
var barrierCalled = false;
|
||||
using var worker = new TestWorker(
|
||||
(_, _) =>
|
||||
{
|
||||
barrierCalled = true;
|
||||
return true;
|
||||
});
|
||||
worker.Start();
|
||||
var workRan = false;
|
||||
var ranOnWorker = false;
|
||||
|
||||
Assert.True(worker.Relay.TryRunAfterPendingWork(() =>
|
||||
{
|
||||
workRan = true;
|
||||
ranOnWorker = worker.Relay.IsGpuQueueThread;
|
||||
}));
|
||||
Assert.True(workRan);
|
||||
Assert.True(ranOnWorker);
|
||||
Assert.False(barrierCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AfterPendingWork_ReturnsFalseWhenTheWorkerIsClosed()
|
||||
{
|
||||
var barrierCalled = false;
|
||||
var relay = new GpuWorkerRelay(
|
||||
() => { },
|
||||
(_, _) =>
|
||||
{
|
||||
barrierCalled = true;
|
||||
return true;
|
||||
});
|
||||
relay.StopAcceptingWork();
|
||||
|
||||
Assert.False(relay.TryRunAfterPendingWork(() => { }));
|
||||
Assert.False(barrierCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AfterAcceptedCommandStreams_WaitsForTheControlBarrier()
|
||||
{
|
||||
Action? run = null;
|
||||
Action? cancel = null;
|
||||
var relay = new GpuWorkerRelay(
|
||||
() => { },
|
||||
(work, cancellation) =>
|
||||
{
|
||||
run = work;
|
||||
cancel = cancellation;
|
||||
return true;
|
||||
});
|
||||
var workRan = false;
|
||||
|
||||
var completion = Task.Run(() =>
|
||||
relay.TryRunAfterAcceptedCommandStreams(() => workRan = true));
|
||||
Assert.True(WaitUntil(() => Volatile.Read(ref run) is not null));
|
||||
Assert.False(await CompletesWithin(completion, 50));
|
||||
|
||||
run!();
|
||||
|
||||
Assert.True(await completion.WaitAsync(TimeSpan.FromSeconds(2)));
|
||||
Assert.True(workRan);
|
||||
Assert.NotNull(cancel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AfterAcceptedCommandStreams_CancellationReleasesTheCaller()
|
||||
{
|
||||
Action? cancel = null;
|
||||
var relay = new GpuWorkerRelay(
|
||||
() => { },
|
||||
(_, cancellation) =>
|
||||
{
|
||||
cancel = cancellation;
|
||||
return true;
|
||||
});
|
||||
|
||||
var completion = Task.Run(() => Record.Exception(() =>
|
||||
relay.TryRunAfterAcceptedCommandStreams(() => { })));
|
||||
Assert.True(WaitUntil(() => Volatile.Read(ref cancel) is not null));
|
||||
cancel!();
|
||||
|
||||
Assert.IsType<OperationCanceledException>(
|
||||
await completion.WaitAsync(TimeSpan.FromSeconds(2)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AfterAcceptedCommandStreams_RethrowsTheWorkException()
|
||||
{
|
||||
Action? run = null;
|
||||
var relay = new GpuWorkerRelay(
|
||||
() => { },
|
||||
(work, _) =>
|
||||
{
|
||||
run = work;
|
||||
return true;
|
||||
});
|
||||
var expected = new InvalidOperationException("ordered state change failed");
|
||||
|
||||
var completion = Task.Run(() => Record.Exception(() =>
|
||||
relay.TryRunAfterAcceptedCommandStreams(() => throw expected)));
|
||||
Assert.True(WaitUntil(() => Volatile.Read(ref run) is not null));
|
||||
run!();
|
||||
|
||||
Assert.Same(expected, await completion.WaitAsync(TimeSpan.FromSeconds(2)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandsRunBeforeGuestWork()
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue