Compare commits
5 Commits
1016_part3
...
des_page_f
| Author | SHA1 | Date |
|---|---|---|
|
|
5f26576d6b | |
|
|
ca102a67c7 | |
|
|
a7cc266702 | |
|
|
767bca9e26 | |
|
|
ac1f739353 |
Binary file not shown.
|
Before Width: | Height: | Size: 356 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 485 KiB After Width: | Height: | Size: 449 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 538 KiB |
13
README.md
13
README.md
|
|
@ -35,9 +35,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
||||
---
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Official channels:** Only **sharpemu.app** and the links listed in this GitHub repository are affiliated with SharpEmu. Any other websites, accounts, or donation pages are unofficial and unauthorized.
|
||||
|
||||
> [!NOTE]
|
||||
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
|
||||
> can run the macOS x64 build through Rosetta 2, and Windows on ARM devices
|
||||
|
|
@ -58,17 +55,17 @@ Our goal is **not** to emulate PS4 games, as there is already an excellent emula
|
|||
|
||||
## Games Tested
|
||||
|
||||
| Demon's Souls Remake | ASTRO BOT |
|
||||
| Demons Souls Remake | Astro Bot |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
| ASTRO's PLAYROOM | Dead Cells |
|
||||
| Hades | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| God of War: Sons of Sparta | Tomb Raider V Remastered |
|
||||
| PAC-MAN World Re-PAC | Tomb Raider V Remastered |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Status
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ The GUI shows 14 variables on its **Options** and **Game options** pages. This l
|
|||
`SHARPEMU_LOG_NP`, `SHARPEMU_PROFILE_PERFORMANCE`, `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE`,
|
||||
`SHARPEMU_RENDERDOC`, `SHARPEMU_VK_DISABLE_IMPLICITS`, `SHARPEMU_STRICT_COMPUTE`, `SHARPEMU_VK_VALIDATION`, `SHARPEMU_WRITABLE_APP0`.
|
||||
|
||||
The list contains 269 variables. The source was examined on 2026-09-19, branch `dev`.
|
||||
The list contains 268 variables. The source was examined on 2026-09-19, branch `dev`.
|
||||
The descriptions come from the code that reads each variable. The emulator was not started for this list.
|
||||
|
||||
## How to use a variable
|
||||
|
|
@ -30,7 +30,7 @@ The descriptions come from the code that reads each variable. The emulator was n
|
|||
| Group | Variables |
|
||||
| --- | --- |
|
||||
| Behavior switches | 19 |
|
||||
| GPU and Vulkan | 59 |
|
||||
| GPU and Vulkan | 58 |
|
||||
| Metal | 10 |
|
||||
| Audio and video | 6 |
|
||||
| Input | 5 |
|
||||
|
|
@ -127,7 +127,6 @@ These variables change the GPU path or the Vulkan presenter.
|
|||
| `SHARPEMU_SHADER_PREWARM` | `0` | `0` turns off the shader prewarm list. By default, each compute program that a game compiles is added at once to `shader-prewarm.bin` in `user/pipeline_cache/<title id>/`, so a stopped game keeps the list. At the next launch after a change of the shader translator or of the GPU driver, background threads translate the listed programs again and create their pipelines, so that the driver compiles them before the game asks for them. The `shader-prewarm.stamp` file names the build and driver that last did this; with the same build and driver, the launch does no prewarm work. The log shows `Shader prewarm done` and `Shader prewarm hits`. On Windows it replaces the cache file by default; on Linux it works beside the cache file. It has no effect with the per-shader cache files of `SHARPEMU_VK_PIPELINE_CACHE_SHARDS`. | `VulkanVideoPresenter.ShaderPrewarm.cs`, `ShaderPrewarmList.cs`, `ShaderProgramCache.cs` |
|
||||
| `SHARPEMU_SKIP_ALL_COMPUTE` | `1` | Set to `1` to make the Metal presenter skip all compute dispatches. The Vulkan presenter reads the value but does not use it. The default is off. | `MetalVideoPresenter.Compute.cs`, `VulkanVideoPresenter.cs` |
|
||||
| `SHARPEMU_SKIP_TALL_COMPUTE_Z` | number | Effect not clear from the code. The Vulkan presenter reads the number into a field for a minimum Z group count. No code uses that field. The default is 0. | `VulkanVideoPresenter.cs` |
|
||||
| `SHARPEMU_SRT_NATIVE` | `0` | `0` turns off the compiled shader resource (SRT) evaluator. By default, the resource plan of each shader is compiled once to .NET code (`DynamicMethod`) on a low-priority background thread named `SRT compiler`; until it is ready, the plan is evaluated by the interpreter, so the first use of a new shader does not wait for the compilation. With `0`, every plan always uses the interpreter. The compiled path is also off when the runtime does not support dynamic code, for example in a Native AOT build. | `CompiledResourceEvaluator.cs`, `ShaderResourcePlan.cs`, `RuntimeValueEvaluator.cs` |
|
||||
| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. A wait that follows its own queue's store to the same label still sees that value when the title rewrites the label for the next frame (Dead Cells). | `CommandStreamQueue.cs`, `GpuCommandInterpreter.cs` |
|
||||
| `SHARPEMU_VK_DEBUG_LABELS` | `1` | Turns on Vulkan object names and command labels for capture tools. `SHARPEMU_VK_VALIDATION=1` also turns them on. Default is off because the labels add overhead to each draw. | `VulkanVideoPresenter.Draws.Recording.cs` |
|
||||
| `SHARPEMU_VK_DEVICE` | text (part of a device name) | Selects the Vulkan device whose name contains this text. The comparison ignores case. When unset, the presenter gives a score to each device and prefers a discrete GPU. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.cs` |
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ public sealed partial class DirectExecutionBackend
|
|||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
if (LogThreadMode && state is { Name: "tbb_thead" })
|
||||
if (state is { Name: "tbb_thead" })
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
|
||||
if (n <= 12 || n % 64 == 0)
|
||||
|
|
|
|||
|
|
@ -1925,10 +1925,6 @@ 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;
|
||||
|
|
@ -3982,33 +3978,6 @@ 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)
|
||||
|
|
@ -5375,7 +5344,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryWriteGuestExceptionContext(
|
||||
private static bool TryWriteGuestExceptionContext(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
GuestCpuContinuation continuation,
|
||||
|
|
@ -5386,7 +5355,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
|
||||
|
||||
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
|
||||
const int mcontext = 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;
|
||||
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]);
|
||||
|
|
@ -5405,10 +5378,9 @@ 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 : context.Rflags);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
|
||||
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,44 +19,6 @@ 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
|
||||
{
|
||||
|
|
@ -65,7 +27,6 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetAfter,
|
||||
StackAfter,
|
||||
TooShort,
|
||||
IndirectBranch,
|
||||
}
|
||||
|
||||
public static PatchResult Patch(
|
||||
|
|
@ -101,19 +62,8 @@ 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,
|
||||
large ? new Progress(hostName, "scan", functionStarts.Length) : null);
|
||||
sites.Sort(static (left, right) => left.Address.CompareTo(right.Address));
|
||||
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
|
||||
if (sites.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -144,10 +94,8 @@ 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.
|
||||
|
|
@ -180,7 +128,6 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetRefusals = scan.BranchTargetRefusals,
|
||||
StackRefusals = scan.StackRefusals,
|
||||
TooShortRefusals = scan.TooShortRefusals,
|
||||
IndirectBranchRefusals = scan.IndirectBranchRefusals,
|
||||
TrampolineBytes = trampolineCursor - trampolineBase,
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -188,8 +135,7 @@ 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} " +
|
||||
$"indirect_branch={result.IndirectBranchRefusals}) " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
|
||||
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
|
||||
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
|
||||
return result;
|
||||
|
|
@ -203,17 +149,15 @@ internal static class GuestRedZonePatcher
|
|||
bool protectRedZone,
|
||||
bool splitVectorStores,
|
||||
bool rewriteSha,
|
||||
out PatchResult result,
|
||||
Progress? progress = null)
|
||||
out PatchResult result)
|
||||
{
|
||||
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[6];
|
||||
var refusalCounts = new int[5];
|
||||
var shaInstructionCount = 0;
|
||||
|
||||
foreach (var header in programHeaders)
|
||||
|
|
@ -246,7 +190,6 @@ 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]
|
||||
|
|
@ -279,14 +222,6 @@ internal static class GuestRedZonePatcher
|
|||
redZoneFunctionCount++;
|
||||
}
|
||||
var branchTargets = CollectBranchTargets(decoded);
|
||||
var canBorrowFollowingInstructions = TryCollectJumpTableTargets(
|
||||
memory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
decoded,
|
||||
functionStart,
|
||||
functionEnd,
|
||||
branchTargets);
|
||||
|
||||
var lastSiteEnd = 0UL;
|
||||
|
||||
|
|
@ -301,29 +236,24 @@ internal static class GuestRedZonePatcher
|
|||
continue;
|
||||
}
|
||||
|
||||
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)
|
||||
// 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))
|
||||
{
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
}
|
||||
|
||||
if (span.Address < lastSiteEnd)
|
||||
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)SpanRefusal.TooShort]++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -338,6 +268,8 @@ 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;
|
||||
|
|
@ -358,7 +290,6 @@ 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;
|
||||
|
|
@ -416,321 +347,6 @@ 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
|
||||
|
|
@ -760,7 +376,7 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
|
||||
if (siteIndex < 0 ||
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), true, out var site, out _))
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -801,10 +417,7 @@ internal static class GuestRedZonePatcher
|
|||
return false;
|
||||
}
|
||||
|
||||
var hasIndirectBranch = decoded.Any(static entry =>
|
||||
entry.Instruction.FlowControl == FlowControl.IndirectBranch);
|
||||
if (hasIndirectBranch ||
|
||||
!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -938,13 +551,12 @@ internal static class GuestRedZonePatcher
|
|||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site) =>
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, true, out site, out _);
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
|
||||
|
||||
private static bool TryBuildPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
bool canBorrowFollowingInstructions,
|
||||
out PatchSite site,
|
||||
out SpanRefusal refusal)
|
||||
{
|
||||
|
|
@ -956,7 +568,6 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
var instruction = decoded[index].Instruction;
|
||||
if (index != startIndex &&
|
||||
canBorrowFollowingInstructions &&
|
||||
!branchTargets.Contains(instruction.IP) &&
|
||||
IsRelocatableDirectBranch(instruction))
|
||||
{
|
||||
|
|
@ -967,11 +578,7 @@ internal static class GuestRedZonePatcher
|
|||
break;
|
||||
}
|
||||
|
||||
if (index != startIndex && !canBorrowFollowingInstructions)
|
||||
{
|
||||
refusal = SpanRefusal.IndirectBranch;
|
||||
}
|
||||
else if (instruction.FlowControl != FlowControl.Next)
|
||||
if (instruction.FlowControl != FlowControl.Next)
|
||||
{
|
||||
refusal = SpanRefusal.ControlFlow;
|
||||
}
|
||||
|
|
@ -1482,8 +1089,6 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
public int TooShortRefusals { get; init; }
|
||||
|
||||
public int IndirectBranchRefusals { get; init; }
|
||||
|
||||
public ulong TrampolineBytes { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,6 @@ 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,21 +50,6 @@ 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,7 +21,6 @@ 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));
|
||||
|
||||
|
|
@ -56,12 +55,6 @@ 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)
|
||||
|
|
@ -72,9 +65,7 @@ public static class HleDataSymbols
|
|||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_progNameBufferAddress == 0 ||
|
||||
_progNamePointerAddress == 0 ||
|
||||
_argumentVectorAddress == 0)
|
||||
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -86,8 +77,6 @@ public static class HleDataSymbols
|
|||
|
||||
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
|
||||
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
|
||||
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
|
||||
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ 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;
|
||||
|
|
@ -102,29 +99,6 @@ 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,6 +22,7 @@ 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;
|
||||
|
|
@ -35,17 +36,6 @@ 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).
|
||||
|
|
@ -70,7 +60,6 @@ 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;
|
||||
|
|
@ -99,11 +88,6 @@ 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();
|
||||
|
|
@ -228,7 +212,7 @@ public static class AudioOut2Exports
|
|||
public uint Ambisonics = AmbisonicsNone;
|
||||
public uint Passthrough;
|
||||
|
||||
public byte[]? PcmData;
|
||||
public int PcmPending;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +226,6 @@ 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",
|
||||
|
|
@ -269,16 +252,6 @@ 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.
|
||||
|
|
@ -332,8 +305,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 memorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memorySizeAddress == 0)
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -351,12 +324,13 @@ 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{memorySizeAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
|
||||
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -417,20 +391,8 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Shared backend lifetime is process-wide; just drop the context entry.
|
||||
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -439,38 +401,7 @@ public static class AudioOut2Exports
|
|||
ExportName = "sceAudioOut2ContextBedWrite",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
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);
|
||||
}
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aII9h5nli9U",
|
||||
|
|
@ -563,6 +494,11 @@ 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
|
||||
|
|
@ -614,41 +550,6 @@ 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);
|
||||
|
|
@ -721,38 +622,14 @@ public static class AudioOut2Exports
|
|||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
|
||||
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} " +
|
||||
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
|
||||
$"grains={port.GrainSamples}");
|
||||
$"format=0x{port.DataFormat:X} grains={port.GrainSamples}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -768,10 +645,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 = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
|
||||
var outPortAddress = ctx[CpuRegister.Rdx];
|
||||
if (outPortAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
|
|
@ -792,14 +669,6 @@ 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..]);
|
||||
|
|
@ -850,16 +719,13 @@ 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 _))
|
||||
|
|
@ -869,21 +735,15 @@ public static class AudioOut2Exports
|
|||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
||||
state[0x02] = channels;
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
||||
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)}");
|
||||
}
|
||||
|
||||
TraceAudioOut2(
|
||||
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -1112,17 +972,7 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
|
||||
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -1214,20 +1064,13 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
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";
|
||||
}
|
||||
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";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1245,20 +1088,11 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
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";
|
||||
}
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1321,6 +1155,7 @@ 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
|
||||
{
|
||||
|
|
@ -1329,42 +1164,29 @@ public static class AudioOut2Exports
|
|||
foreach (var port in Ports.Values)
|
||||
{
|
||||
if (port.ContextHandle != context.Handle ||
|
||||
!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))
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var byteLength = checked(frames * ch * bps);
|
||||
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
|
||||
if (byteLength <= 0 || byteLength > source.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pcmData.Length < byteLength)
|
||||
var sourceSpan = source.AsSpan(0, byteLength);
|
||||
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MixPortIntoStereo(
|
||||
pcmData.AsSpan(0, byteLength),
|
||||
sourceSpan,
|
||||
mix.AsSpan(0, frames * 2),
|
||||
frames,
|
||||
ch,
|
||||
|
|
@ -1416,70 +1238,12 @@ 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) =>
|
||||
|
|
@ -1534,11 +1298,6 @@ 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,
|
||||
|
|
@ -1694,19 +1453,13 @@ 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,32 +881,6 @@ 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,21 +7,6 @@ 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.
|
||||
|
|
@ -44,8 +29,6 @@ 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;
|
||||
|
||||
|
|
@ -62,12 +45,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.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
|
||||
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
SingleWriter = true,
|
||||
});
|
||||
|
||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
||||
|
|
@ -91,10 +74,6 @@ 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;
|
||||
|
|
@ -127,22 +106,18 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -161,18 +136,14 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: frame or packet allocation returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
var decoder = new Videodec2Decoder(codecContext, frame, packet);
|
||||
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
|
||||
return decoder;
|
||||
return new Videodec2Decoder(codecContext, frame, packet);
|
||||
}
|
||||
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);
|
||||
|
|
@ -203,252 +174,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool EnqueueAccessUnit(byte[] accessUnit)
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
return _workChannel.Writer.TryWrite(accessUnit);
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public bool RequestDrain()
|
||||
public void RequestDrain()
|
||||
{
|
||||
return _workChannel.Writer.TryWrite(null);
|
||||
_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)
|
||||
{
|
||||
|
|
@ -496,28 +233,15 @@ 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.
|
||||
|
|
@ -840,22 +564,4 @@ 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,7 +1,6 @@
|
|||
// 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;
|
||||
|
||||
|
|
@ -16,18 +15,10 @@ 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",
|
||||
|
|
@ -36,25 +27,24 @@ public static class Videodec2Exports
|
|||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
|
||||
if (paramAddress == 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",
|
||||
|
|
@ -62,14 +52,8 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
||||
{
|
||||
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))
|
||||
var queueAddress = ctx[CpuRegister.Rdi];
|
||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -77,18 +61,7 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
||||
[SysAbiExport(
|
||||
Nid = "qqMCwlULR+E",
|
||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
||||
|
|
@ -96,18 +69,12 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
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))
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -132,9 +99,7 @@ public static class Videodec2Exports
|
|||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
||||
var decoder = Videodec2Decoder.TryCreate();
|
||||
Decoders[handle] = decoder;
|
||||
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
|
||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
||||
|
||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
||||
{
|
||||
|
|
@ -146,6 +111,7 @@ 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",
|
||||
|
|
@ -154,25 +120,33 @@ 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 (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (Decoders.TryGetValue(handle, out var decoder) &&
|
||||
decoder is not null &&
|
||||
decoder.FlushOutput(out var decodedFrame) &&
|
||||
decodedFrame is not null)
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
_ = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// No state to reset.
|
||||
[SysAbiExport(
|
||||
Nid = "wJXikG6QFN8",
|
||||
ExportName = "sceVideodec2Reset",
|
||||
|
|
@ -180,12 +154,6 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +173,8 @@ 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",
|
||||
|
|
@ -216,9 +186,8 @@ 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 (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -226,7 +195,6 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -240,235 +208,35 @@ 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);
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
||||
decoder.EnqueueAccessUnit(auBuffer);
|
||||
|
||||
if (!decodeOk || decodedFrame is null)
|
||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
var copied = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
if (copied)
|
||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
||||
{
|
||||
Interlocked.Increment(ref _decodedPictureCount);
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
[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 readonly byte[] NoPicture = [0];
|
||||
private static readonly byte[] PictureReady = [1];
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
|
|
@ -26,8 +26,6 @@ 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;
|
||||
|
|
@ -39,10 +37,8 @@ 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;
|
||||
|
|
@ -95,7 +91,7 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _submissionCount != 0 || _controlBarriers.Count != 0;
|
||||
return _submissionCount != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +113,7 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return HasRunnableWorkLocked();
|
||||
return SelectQueueLocked() >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -204,7 +200,6 @@ 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))
|
||||
|
|
@ -213,23 +208,6 @@ 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)
|
||||
|
|
@ -279,8 +257,7 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
while (_outcome == IdleOutcome.Completed &&
|
||||
(_processing || _submissionCount != 0 || _controlBarriers.Count != 0))
|
||||
while (_outcome == IdleOutcome.Completed && (_processing || _submissionCount != 0))
|
||||
{
|
||||
Monitor.Wait(_gate);
|
||||
}
|
||||
|
|
@ -302,13 +279,13 @@ public sealed class CommandStreamQueue
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (HasRunnableWorkLocked() || _stopping)
|
||||
if (SelectQueueLocked() >= 0 || _stopping)
|
||||
{
|
||||
return HasRunnableWorkLocked();
|
||||
return SelectQueueLocked() >= 0;
|
||||
}
|
||||
|
||||
Monitor.Wait(_gate, timeoutMilliseconds);
|
||||
return HasRunnableWorkLocked();
|
||||
return SelectQueueLocked() >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +295,7 @@ public sealed class CommandStreamQueue
|
|||
lock (_gate)
|
||||
{
|
||||
var deadline = Environment.TickCount64 + intervalMilliseconds;
|
||||
while (!HasRunnableWorkLocked())
|
||||
while (SelectQueueLocked() < 0)
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
|
|
@ -336,15 +313,10 @@ 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()
|
||||
|
|
@ -392,8 +364,7 @@ public sealed class CommandStreamQueue
|
|||
// Runs one slice of the next unblocked submission on the calling thread.
|
||||
public SliceResult ProcessOne()
|
||||
{
|
||||
CommandSubmission? submission = null;
|
||||
ControlBarrier? control = null;
|
||||
CommandSubmission submission;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_processing)
|
||||
|
|
@ -401,73 +372,35 @@ public sealed class CommandStreamQueue
|
|||
throw _host.Fatal("The command stream queue is already processing a slice.");
|
||||
}
|
||||
|
||||
if (IsControlBarrierReadyLocked())
|
||||
var selected = SelectQueueLocked();
|
||||
if (selected < 0)
|
||||
{
|
||||
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;
|
||||
return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork;
|
||||
}
|
||||
|
||||
var queue = _queues[selected];
|
||||
submission = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
_submissionCount--;
|
||||
_nextQueue = (selected + 1) % QueueCount;
|
||||
_processing = true;
|
||||
_processingThread = Thread.CurrentThread;
|
||||
if (submission is { Started: false } &&
|
||||
submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
|
||||
if (!submission.Started && 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)
|
||||
{
|
||||
|
|
@ -483,7 +416,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);
|
||||
|
|
@ -664,17 +597,12 @@ 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.
|
||||
|
|
@ -741,20 +669,6 @@ 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;
|
||||
|
|
@ -767,42 +681,6 @@ 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,8 +101,6 @@ 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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Packets;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.GpuCommands;
|
||||
|
|
@ -11,7 +10,6 @@ namespace SharpEmu.Libs.Gpu.GpuCommands;
|
|||
public sealed partial class GpuCommandInterpreter
|
||||
{
|
||||
private const uint DmaSkippedDestination = 0x3022C;
|
||||
private const uint LodStatsWithoutCounters = 1;
|
||||
|
||||
// Destination selectors 1, 2, 4 and 5 are memory; registers and GDS are not supported.
|
||||
internal void WriteData(ulong destination, ReadOnlySpan<uint> source, uint writeControl)
|
||||
|
|
@ -369,12 +367,12 @@ public sealed partial class GpuCommandInterpreter
|
|||
var destination = (payload[1] & 0xFFFF_FFC0u) | ((ulong)payload[2] << 32);
|
||||
if (destination != 0 && bufferSize != 0)
|
||||
{
|
||||
var report = new byte[bufferSize];
|
||||
var hasCounters = MipStatistics.Shared.TryWriteReport(report);
|
||||
WriteBytes(destination, report);
|
||||
// The statistics buffer is zeroed and its first dword marks it as valid.
|
||||
var zeros = new byte[bufferSize];
|
||||
WriteBytes(destination, zeros);
|
||||
if (bufferSize >= sizeof(uint))
|
||||
{
|
||||
WriteDword(destination, hasCounters ? MipStatistics.FirstCounterOffset : LodStatsWithoutCounters);
|
||||
WriteDword(destination, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,11 +57,6 @@ 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 = 10;
|
||||
private const uint KindCount = 9;
|
||||
private const uint ElementSizeCount = 5;
|
||||
private const uint DirectionCount = 2;
|
||||
private const uint PipelineCount = KindCount * ElementSizeCount * DirectionCount;
|
||||
|
|
|
|||
|
|
@ -1,271 +1,13 @@
|
|||
// 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();
|
||||
|
|
@ -315,6 +57,7 @@ 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) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
|
||||
info.Data.Size < PieceHashMinimumImageSize)
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -725,20 +725,15 @@ public sealed unsafe partial class GuestImageCache
|
|||
}
|
||||
|
||||
var range = image.Description.Data;
|
||||
var ring = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
|
||||
GpuBuffer download = ring;
|
||||
if (ring.TryMap(range.Size, out var offset, Math.Max(image.Description.BytesPerBlock, 4u)))
|
||||
var download = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
|
||||
if (!download.TryMap(range.Size, out var offset, Math.Max(image.Description.BytesPerBlock, 4u)))
|
||||
{
|
||||
ring.Commit();
|
||||
}
|
||||
else
|
||||
{
|
||||
download = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, range.Size);
|
||||
throw SubmissionScheduler.Fatal($"The reusable download ring cannot map the image: address=0x{range.Address:X16} size=0x{range.Size:X}.");
|
||||
}
|
||||
|
||||
download.Commit();
|
||||
if (!_backing.TryReadBacking(range.Address, download.Mapped.Slice((int)offset, (int)range.Size)))
|
||||
{
|
||||
if (download != ring) download.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -766,8 +761,6 @@ public sealed unsafe partial class GuestImageCache
|
|||
throw SubmissionScheduler.Fatal($"The image readback could not be written to guest memory: address=0x{range.Address:X16} size=0x{range.Size:X}.");
|
||||
}
|
||||
});
|
||||
// Completion actions wait for the priority readback before freeing spill buffers.
|
||||
if (download != ring) _scheduler.QueueCompletionAction(download.Dispose);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,48 +123,17 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
return GetNullImage(request);
|
||||
}
|
||||
|
||||
var metadataBaseLayer = request.View.BaseLayer;
|
||||
var result = LookUpImage(ref request, exactFormat);
|
||||
|
||||
var found = LookUpImage(ref request, exactFormat);
|
||||
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
|
||||
{
|
||||
ulong metadataAddress;
|
||||
ulong sliceSize;
|
||||
using (var held = _lock.Hold())
|
||||
ref readonly var description = ref _slots[found].Description;
|
||||
if (description.DccSliceSize is var sliceSize and not 0)
|
||||
{
|
||||
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);
|
||||
SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return found;
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
|
||||
|
|
@ -251,6 +220,17 @@ 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 };
|
||||
|
|
@ -362,7 +342,8 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
image.MarkGpuModified();
|
||||
image.Uses.RenderTarget = true;
|
||||
RefreshFromGuest(imageIdentifier, request);
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
|
||||
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
var address = request.Description.Metadata.Range.Address;
|
||||
|
|
|
|||
|
|
@ -28,11 +28,8 @@ 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>
|
||||
|
|
@ -369,8 +366,7 @@ public struct ImageDescription
|
|||
{
|
||||
case MetadataKind.None:
|
||||
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
|
||||
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
|
||||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
|
||||
}
|
||||
|
|
@ -382,11 +378,6 @@ 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 ||
|
||||
|
|
@ -516,87 +507,6 @@ 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,38 +43,24 @@ 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 && PackedClearValue.SupportsDccFixedColor(format);
|
||||
var fixedSupported = hasDcc && SupportsDccFixedClear(format);
|
||||
return (supported, fixedSupported, supported ? value : default);
|
||||
}
|
||||
|
||||
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
|
||||
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,
|
||||
};
|
||||
|
||||
// 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)
|
||||
|
|
@ -338,14 +324,8 @@ 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, metadataSize.Size);
|
||||
description.Metadata.DccClearWord = words.ClearWord0;
|
||||
description.Metadata.DccClearRegisterValid = true;
|
||||
description.Metadata.DccAlphaMsb = DccAlphaOnMsb(words);
|
||||
description.Metadata.Range = new GuestSpan(words.DccAddress, 0);
|
||||
}
|
||||
|
||||
for (var level = 0; level < levels; level++)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ 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);
|
||||
|
|
@ -125,7 +127,7 @@ public static partial class ImageRequestBuilders
|
|||
|
||||
if (hasStencil)
|
||||
{
|
||||
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt ||
|
||||
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) ||
|
||||
stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
|
|
|
|||
|
|
@ -300,17 +300,6 @@ public static partial class ImageRequestBuilders
|
|||
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
|
||||
description.Samples = samples;
|
||||
description.TileMode = tile;
|
||||
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(descriptor.MetadataAddress << 8, metadataSize.Size);
|
||||
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
|
||||
}
|
||||
|
||||
if (samples > 1)
|
||||
{
|
||||
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size.Size, Pitch = pitch, Height = height };
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
public sealed class MipStatistics
|
||||
{
|
||||
public const int SlotCount = 256;
|
||||
public const uint FirstCounterOffset = 64;
|
||||
public const int ReportBytes = (int)FirstCounterOffset + SlotCount * CounterBytes;
|
||||
public const uint NoMipSampled = 15;
|
||||
|
||||
private const int CounterBytes = 2 * sizeof(uint);
|
||||
private const uint FinestMip = 0;
|
||||
private const uint SamplesPerBinding = 0x10000;
|
||||
private const uint MaxSamples = 0xFFFFFF;
|
||||
|
||||
public static MipStatistics Shared { get; } = new();
|
||||
|
||||
private readonly uint[] _bindingsBySlot = new uint[SlotCount];
|
||||
|
||||
public void NoteSampled(in TextureDescriptorWords texture)
|
||||
{
|
||||
if (texture.MipStatsEnabled && !texture.IsNull)
|
||||
{
|
||||
_bindingsBySlot[texture.MipStatsSlot]++;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWriteReport(Span<byte> report)
|
||||
{
|
||||
if (report.Length < ReportBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var slot = 0; slot < SlotCount; slot++)
|
||||
{
|
||||
var samples = (uint)Math.Min((ulong)_bindingsBySlot[slot] * SamplesPerBinding, MaxSamples);
|
||||
var finestMipSampled = samples == 0 ? NoMipSampled : FinestMip;
|
||||
var clampedSamplesAndSlot = samples | ((uint)slot << 24);
|
||||
var unclampedSamplesAndFinestMip = samples | (finestMipSampled << 24);
|
||||
var counter = report.Slice((int)FirstCounterOffset + slot * CounterBytes, CounterBytes);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(counter, clampedSamplesAndSlot);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(counter[sizeof(uint)..], unclampedSamplesAndFinestMip);
|
||||
}
|
||||
|
||||
Array.Clear(_bindingsBySlot);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -68,18 +68,12 @@ public readonly struct TextureDescriptorWords
|
|||
|
||||
public uint MinLodWarning => (Fields[5] >> 8) & 0xFFF;
|
||||
|
||||
public bool MipStatsEnabled => ((Fields[5] >> 25) & 0x1) == 1;
|
||||
|
||||
public uint MipStatsSlot => (Fields[6] >> 0) & 0xFF;
|
||||
|
||||
public bool MsaaDepth => ((Fields[6] >> 10) & 0x1) == 1;
|
||||
|
||||
public bool WriteCompress => ((Fields[6] >> 20) & 0x1) == 1;
|
||||
|
||||
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,9 +452,7 @@ 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.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
|
||||
? region.ImageSubresource.BaseArrayLayer
|
||||
: 0;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB 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,75 +230,6 @@ 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);
|
||||
|
|
@ -388,14 +319,6 @@ 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)
|
||||
{
|
||||
|
|
@ -441,19 +364,17 @@ public static partial class TileGeometry
|
|||
|
||||
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
|
||||
}
|
||||
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
|
||||
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
|
||||
{
|
||||
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
|
||||
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
|
||||
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
}
|
||||
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
|
||||
{
|
||||
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ 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)
|
||||
|
|
@ -110,7 +109,6 @@ 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 =
|
||||
|
|
@ -249,7 +247,6 @@ 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:
|
||||
|
|
@ -300,7 +297,7 @@ public static partial class TileGeometry
|
|||
kind = TileBlockKind.Depth64KB;
|
||||
break;
|
||||
case GuestTileMode.RenderTarget:
|
||||
kind = TileBlockKind.RenderTarget64KBGen5;
|
||||
kind = TileBlockKind.RenderTarget64KB;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -311,7 +308,7 @@ public static partial class TileGeometry
|
|||
return false;
|
||||
}
|
||||
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB &&
|
||||
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -608,39 +605,6 @@ 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,57 +124,6 @@ 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,11 +41,9 @@ 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;
|
||||
bool ClipDistanceEnabled => false;
|
||||
|
||||
RenderHostLimits Limits { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -593,7 +593,6 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
|
||||
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
|
||||
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
|
||||
switch (source.Stage)
|
||||
{
|
||||
case ShaderStage.Vertex:
|
||||
|
|
@ -606,11 +605,9 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
SupportsClipDistance = _host.ClipDistanceEnabled,
|
||||
ClipSpace = new ShaderClipSpaceTransform(
|
||||
info.ClipSpace.Enabled,
|
||||
info.ClipSpace.ScaleX,
|
||||
|
|
@ -634,7 +631,6 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
|
|
@ -646,7 +642,7 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
default:
|
||||
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -662,8 +658,7 @@ internal sealed class ShaderProgramCache
|
|||
usesDispatchThreadLimits: usesDispatchThreadLimits);
|
||||
|
||||
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
|
||||
bool signedZeroInfNanPreserve) =>
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision) =>
|
||||
new(plan, resources, layout)
|
||||
{
|
||||
WaveSize = info.WaveSize,
|
||||
|
|
@ -671,7 +666,6 @@ 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),
|
||||
|
|
@ -680,8 +674,7 @@ internal sealed class ShaderProgramCache
|
|||
};
|
||||
|
||||
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled,
|
||||
out BindingLayout? layout, out string error)
|
||||
bool sharedInt64Atomics, bool execGuardElision, out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
|
||||
{
|
||||
compiled = null;
|
||||
layout = null;
|
||||
|
|
@ -698,7 +691,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, signedZeroInfNanPreserve);
|
||||
sharedInt64Atomics, execGuardElision);
|
||||
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ public static class VertexAttributeFormats
|
|||
56 => (Format.R8G8B8A8Unorm, 4u),
|
||||
50 => (Format.A2B10G10R10UnormPack32, 4u),
|
||||
51 => (Format.A2B10G10R10SNormPack32, 4u),
|
||||
36 => (Format.B10G11R11UfloatPack32, 3u),
|
||||
29 => (Format.R16G16Sfloat, 2u),
|
||||
28 => (Format.R16G16Sint, 2u),
|
||||
27 => (Format.R16G16Uint, 2u),
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ public sealed partial class RenderExecutor
|
|||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
var bindings = PrepareBindings(input.Stage);
|
||||
var bindings = _host.PrepareBindings(input.Stage);
|
||||
if (program.UsesDeviceAddresses)
|
||||
{
|
||||
_host.PrepareDeviceAddresses();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Silk.NET.Vulkan;
|
||||
|
|
@ -171,30 +170,6 @@ public sealed partial class RenderExecutor
|
|||
private void SetDrawDebugPhase(ulong submitId, in DrawCall draw, uint phase) =>
|
||||
_host.SetDebugInformation(draw.Operation, submitId, phase, draw.Count, 0, draw.InstanceCount, draw.FirstInstance);
|
||||
|
||||
private IPreparedBindings PrepareBindings(in ShaderStageResources stage)
|
||||
{
|
||||
var bindings = _host.PrepareBindings(stage);
|
||||
NoteSampledTextures(stage);
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private static void NoteSampledTextures(in ShaderStageResources stage)
|
||||
{
|
||||
if (stage.Program is not { } program)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var descriptors = stage.Resources.Images;
|
||||
for (var index = 0; index < program.Images.Length && index < descriptors.Length; index++)
|
||||
{
|
||||
if (program.Images[index].Class == ImageResourceClass.Sampled)
|
||||
{
|
||||
MipStatistics.Shared.NoteSampled(new TextureDescriptorWords(descriptors[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Binds everything the draw needs inside one preparation scope, then records it.
|
||||
private void RecordDraw(
|
||||
ulong submitId,
|
||||
|
|
@ -217,8 +192,8 @@ public sealed partial class RenderExecutor
|
|||
IPreparedBindings? pixelBindings;
|
||||
try
|
||||
{
|
||||
vertexBindings = PrepareBindings(vertexInput.Stage);
|
||||
pixelBindings = state.PixelActive ? PrepareBindings(pixelInput.Stage) : null;
|
||||
vertexBindings = _host.PrepareBindings(vertexInput.Stage);
|
||||
pixelBindings = state.PixelActive ? _host.PrepareBindings(pixelInput.Stage) : null;
|
||||
}
|
||||
catch (DrawImageTypeMismatchException rejection)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,60 +14,16 @@ 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, Func<Action, Action, bool>? tryEnqueueAfterPendingWork = null)
|
||||
public GpuWorkerRelay(Action wake)
|
||||
{
|
||||
_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;
|
||||
|
|
|
|||
|
|
@ -66,9 +66,6 @@ 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,25 +141,6 @@ 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",
|
||||
|
|
@ -187,14 +168,6 @@ 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",
|
||||
|
|
@ -220,14 +193,6 @@ 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",
|
||||
|
|
@ -245,30 +210,6 @@ 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,10 +87,6 @@ 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 _);
|
||||
|
|
@ -110,7 +106,5 @@ internal static class JsonObjectHeap
|
|||
Strings.Clear();
|
||||
GlobalNullAccessCallback = 0;
|
||||
GlobalNullAccessCallbackContext = 0;
|
||||
GlobalSpecialFloatHandler = 0;
|
||||
GlobalSpecialFloatHandlerContext = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,43 +161,20 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
|
||||
public bool Remove(ulong ident, short filter)
|
||||
=> RemoveAll(ident, filter) != 0;
|
||||
|
||||
public int RemoveAll(ulong ident, short filter)
|
||||
{
|
||||
var originalCount = Count;
|
||||
var writeIndex = 0;
|
||||
for (var readIndex = 0; readIndex < originalCount; readIndex++)
|
||||
var index = FindIndex(ident, filter);
|
||||
if (index < 0)
|
||||
{
|
||||
var candidate = this[readIndex];
|
||||
if (candidate.Ident == ident && candidate.Filter == filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this[writeIndex++] = candidate;
|
||||
return false;
|
||||
}
|
||||
|
||||
Count = writeIndex;
|
||||
return originalCount - writeIndex;
|
||||
}
|
||||
|
||||
public int UpdateUserData(ulong ident, short filter, ulong userData)
|
||||
{
|
||||
var updatedCount = 0;
|
||||
for (var index = 0; index < Count; index++)
|
||||
for (var i = index; i + 1 < Count; i++)
|
||||
{
|
||||
var candidate = this[index];
|
||||
if (candidate.Ident != ident || candidate.Filter != filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this[index] = candidate with { UserData = userData };
|
||||
updatedCount++;
|
||||
this[i] = this[i + 1];
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
Count--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -847,55 +824,52 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
|
||||
var waiterId = Interlocked.Increment(ref _nextEventQueueWaiterId);
|
||||
var blockDeadlineTimestamp = timeoutAddress == 0
|
||||
? 0
|
||||
: GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||
TimeSpan.FromMicroseconds(timeoutUsec));
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
state.WakeKey,
|
||||
new EqueueWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
WaiterId = waiterId,
|
||||
},
|
||||
blockDeadlineTimestamp);
|
||||
if (requestedBlock)
|
||||
if (timeoutAddress == 0)
|
||||
{
|
||||
var wakeAfterRegistration = false;
|
||||
lock (_eventQueueGate)
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
state.WakeKey,
|
||||
new EqueueWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
WaiterId = waiterId,
|
||||
});
|
||||
if (requestedBlock)
|
||||
{
|
||||
wakeAfterRegistration =
|
||||
!IsLiveEventQueueLocked(state) ||
|
||||
HasPendingEventsLocked(state.Handle);
|
||||
}
|
||||
var wakeAfterRegistration = false;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
wakeAfterRegistration =
|
||||
!IsLiveEventQueueLocked(state) ||
|
||||
HasPendingEventsLocked(state.Handle);
|
||||
}
|
||||
|
||||
if (wakeAfterRegistration)
|
||||
{
|
||||
WakeEventQueue(
|
||||
state,
|
||||
_logEqueue
|
||||
? "source=post-registration-state-check"
|
||||
: null);
|
||||
}
|
||||
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}");
|
||||
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;
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
|
|
@ -1167,10 +1141,9 @@ public static class KernelEventQueueCompatExports
|
|||
}
|
||||
|
||||
var key = (ident, filter);
|
||||
var isExistingRegistration = events.ContainsKey(key);
|
||||
var isNewReadRegistration =
|
||||
filter == KernelEventFilterRead &&
|
||||
!isExistingRegistration;
|
||||
!events.ContainsKey(key);
|
||||
events[(ident, filter)] = new KernelEventRegistration(
|
||||
ident,
|
||||
filter,
|
||||
|
|
@ -1179,11 +1152,6 @@ 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++;
|
||||
|
|
@ -1328,7 +1296,7 @@ public static class KernelEventQueueCompatExports
|
|||
|
||||
if (_pendingEvents.TryGetValue(handle, out var pending))
|
||||
{
|
||||
_ = pending.RemoveAll(ident, filter);
|
||||
_ = pending.Remove(ident, filter);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -1419,13 +1387,22 @@ public static class KernelEventQueueCompatExports
|
|||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
queue.AddLast(new KernelQueuedEvent(
|
||||
// Keep each interrupt as a separate event. Limit growth when the queue is not read.
|
||||
var queuedEvent = new KernelQueuedEvent(
|
||||
registration.Ident,
|
||||
registration.Filter,
|
||||
registration.Flags,
|
||||
1,
|
||||
data,
|
||||
registration.UserData));
|
||||
registration.UserData);
|
||||
if (CountPendingEvents(queue, registration.Ident, registration.Filter) < 256)
|
||||
{
|
||||
queue.AddLast(queuedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueOrUpdateEvent(queue, queuedEvent);
|
||||
}
|
||||
|
||||
(wakeQueues ??= []).Add(state);
|
||||
triggeredCount++;
|
||||
|
|
@ -1653,6 +1630,24 @@ 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.KernelOpenCore(ctx);
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqQMh1zPPT8",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, GuestFileShare);
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||
stream.SetLength(length);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ 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];
|
||||
|
|
@ -98,7 +96,6 @@ 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();
|
||||
|
|
@ -1442,9 +1439,7 @@ public static partial class KernelMemoryCompatExports
|
|||
ExportName = "_open",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpenUnderscore(CpuContext ctx) => PosixOpen(ctx);
|
||||
|
||||
internal static int KernelOpenCore(CpuContext ctx)
|
||||
public static int KernelOpenUnderscore(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
|
@ -1455,19 +1450,6 @@ 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);
|
||||
|
|
@ -1537,7 +1519,7 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
|
||||
var stream = new FileStream(hostPath, mode, access, GuestFileShare);
|
||||
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
|
||||
if ((flags & O_APPEND) != 0)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1695,11 +1677,11 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
// POSIX open(2): translates a failed raw open into -1/errno. On success
|
||||
// KernelOpenCore already writes the fd into RAX (the import bridge
|
||||
// KernelOpenUnderscore 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 = KernelOpenCore(ctx);
|
||||
var result = KernelOpenUnderscore(ctx);
|
||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
? 0
|
||||
: PosixFailure(ctx, result);
|
||||
|
|
@ -1752,8 +1734,7 @@ public static partial class KernelMemoryCompatExports
|
|||
{
|
||||
// Stop at the first miss and report its index.
|
||||
// The caller can then use its normal file-open fallback.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelAprResolveFilepathsToIdsAndFileSizes: '{guestPath}' -> '{hostPath}' not found (index {i} of {count})");
|
||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
||||
if (sizesAddress != 0 &&
|
||||
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
||||
{
|
||||
|
|
@ -2273,12 +2254,7 @@ public static partial class KernelMemoryCompatExports
|
|||
string? observedBinkPath = null;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_randomDeviceDescriptors.Remove(fd))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
else if (_openFiles.Remove(fd, out stream))
|
||||
if (_openFiles.Remove(fd, out stream))
|
||||
{
|
||||
_binkGuestCompletionShims.Remove(fd);
|
||||
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
|
||||
|
|
@ -2331,25 +2307,6 @@ 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,
|
||||
|
|
@ -2885,21 +2842,13 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
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);
|
||||
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
|
||||
var searchEnd = searchEndRaw <= 0
|
||||
? GuestMemoryLayout.DirectBytes
|
||||
: Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
|
||||
if (searchStart >= searchEnd)
|
||||
{
|
||||
return MemoryNoSpace;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
bool foundSpan;
|
||||
|
|
@ -2917,7 +2866,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
if (!foundSpan)
|
||||
{
|
||||
return MemoryNoSpace;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
|
||||
|
|
@ -6952,14 +6901,9 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
string? hostPath = null;
|
||||
bool isDirectory = false;
|
||||
bool isRandomDevice = false;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_randomDeviceDescriptors.Contains(fd))
|
||||
{
|
||||
isRandomDevice = true;
|
||||
}
|
||||
else if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
{
|
||||
hostPath = directory.Path;
|
||||
isDirectory = true;
|
||||
|
|
@ -6970,14 +6914,6 @@ 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;
|
||||
|
|
@ -6999,10 +6935,6 @@ 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,38 +548,23 @@ 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);
|
||||
|
||||
if (hasStackBounds)
|
||||
// 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))
|
||||
{
|
||||
threadState.Attributes = threadState.Attributes with
|
||||
{
|
||||
StackAddress = stackAddress,
|
||||
StackSize = stackSize,
|
||||
StackSize = NativeGuestStackSize,
|
||||
};
|
||||
}
|
||||
_attrStates[outAttrAddress] = threadState.Attributes;
|
||||
|
|
@ -589,11 +574,6 @@ 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,13 +80,6 @@ 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 (!ctx.TryWriteUInt64(semaphoreAddress, handle))
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -113,27 +113,6 @@ 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)
|
||||
|
|
@ -146,7 +125,7 @@ public static class KernelSemaphoreCompatExports
|
|||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")}");
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
|
@ -158,7 +137,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 = hasTimeout
|
||||
var deadline = timeoutAddress != 0
|
||||
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||
: 0;
|
||||
|
||||
|
|
@ -208,7 +187,7 @@ public static class KernelSemaphoreCompatExports
|
|||
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
operationName,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
ResumeWait,
|
||||
WakePredicate,
|
||||
|
|
@ -237,21 +216,14 @@ public static class KernelSemaphoreCompatExports
|
|||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
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)}");
|
||||
}
|
||||
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,
|
||||
hasTimeout,
|
||||
timeoutAddress,
|
||||
timeoutUsec);
|
||||
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
|
||||
}
|
||||
|
||||
private static int WaitSemaphoreOnHostThread(
|
||||
|
|
@ -259,62 +231,46 @@ public static class KernelSemaphoreCompatExports
|
|||
KernelSemaphoreState semaphore,
|
||||
uint handle,
|
||||
int needCount,
|
||||
bool hasTimeout,
|
||||
ulong timeoutAddress,
|
||||
uint timeoutUsec)
|
||||
{
|
||||
var deadlineMs = hasTimeout
|
||||
var deadlineMs = timeoutAddress != 0
|
||||
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
|
||||
: long.MaxValue;
|
||||
if (_traceSema)
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore(
|
||||
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
|
||||
$"count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} {FormatCallSite(ctx)}");
|
||||
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
|
||||
}
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
while (semaphore.Count < needCount)
|
||||
{
|
||||
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 (hasTimeout && remaining <= 0)
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var waitMilliseconds = hasTimeout
|
||||
? (int)Math.Max(1L, Math.Min(remaining, 10L))
|
||||
: 10;
|
||||
Monitor.Wait(semaphore.Gate, waitMilliseconds);
|
||||
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(ctx);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,7 +328,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);
|
||||
|
|
@ -576,35 +532,6 @@ 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,44 +10,23 @@ 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,
|
||||
bool usePosixResult,
|
||||
long deadlineTimestamp)
|
||||
public SyncWaiter(CpuContext context, ulong address, ulong expected, bool is64Bit)
|
||||
{
|
||||
_context = context;
|
||||
Address = address;
|
||||
_expected = expected;
|
||||
_is64Bit = is64Bit;
|
||||
_usePosixResult = usePosixResult;
|
||||
_deadlineTimestamp = deadlineTimestamp;
|
||||
_registeredTicks = KernelSyncOnAddressProfile.Enabled
|
||||
? Stopwatch.GetTimestamp()
|
||||
: 0L;
|
||||
|
|
@ -71,13 +50,13 @@ public static class KernelSyncOnAddressCompatExports
|
|||
if (Volatile.Read(ref _wakeRequested) != 0)
|
||||
{
|
||||
RecordResume(explicitWake: true, valueChanged: false, timedOut: false, faulted: false);
|
||||
return Complete((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryReadValue(_context, Address, _is64Bit, out var value))
|
||||
{
|
||||
RecordResume(explicitWake: false, valueChanged: false, timedOut: false, faulted: true);
|
||||
return Complete((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var valueChanged = value != _expected;
|
||||
|
|
@ -86,21 +65,15 @@ public static class KernelSyncOnAddressCompatExports
|
|||
valueChanged,
|
||||
timedOut: !valueChanged,
|
||||
faulted: false);
|
||||
var result = valueChanged
|
||||
return 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;
|
||||
|
|
@ -114,50 +87,42 @@ public static class KernelSyncOnAddressCompatExports
|
|||
return true;
|
||||
}
|
||||
|
||||
public int WaitOnHost()
|
||||
public int WaitOnHost(TimeSpan? timeout)
|
||||
{
|
||||
while (Volatile.Read(ref _wakeRequested) == 0)
|
||||
if (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
if (!TryReadValue(_context, Address, _is64Bit, out var current) ||
|
||||
current != _expected)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
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)
|
||||
if (timeout is null)
|
||||
{
|
||||
Monitor.Wait(_hostGate, waitSlice);
|
||||
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;
|
||||
}
|
||||
|
||||
var remaining = TimeSpan.FromSeconds(
|
||||
remainingTicks / (double)Stopwatch.Frequency);
|
||||
Monitor.Wait(_hostGate, remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(_context);
|
||||
}
|
||||
|
||||
return Resume();
|
||||
}
|
||||
|
||||
private int Complete(int result) => _usePosixResult
|
||||
? SetPosixReturn(_context, result)
|
||||
: result;
|
||||
|
||||
private void RecordResume(
|
||||
bool explicitWake,
|
||||
bool valueChanged,
|
||||
|
|
@ -206,80 +171,6 @@ 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",
|
||||
|
|
@ -289,29 +180,14 @@ 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 CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
usePosixResult);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (requested == 0)
|
||||
{
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
List<SyncWaiter>? selected = null;
|
||||
|
|
@ -383,10 +259,7 @@ public static class KernelSyncOnAddressCompatExports
|
|||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
Environment.CurrentManagedThreadId);
|
||||
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static void CaptureCallStack(
|
||||
|
|
@ -431,15 +304,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);
|
||||
|
|
@ -461,92 +334,28 @@ 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);
|
||||
}
|
||||
|
||||
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);
|
||||
var waiter = new SyncWaiter(ctx, address, expected, is64Bit);
|
||||
Register(address, waiter);
|
||||
if (!TryReadValue(ctx, address, is64Bit, out current))
|
||||
{
|
||||
Unregister(waiter);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
usePosixResult);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (current != expected || waiter.TryWake())
|
||||
{
|
||||
Unregister(waiter);
|
||||
KernelSyncOnAddressProfile.RecordImmediate(address);
|
||||
return CompleteResult(
|
||||
ctx,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
usePosixResult);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var returnRip = 0UL;
|
||||
|
|
@ -574,7 +383,7 @@ public static class KernelSyncOnAddressCompatExports
|
|||
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
reason,
|
||||
is64Bit ? "sceKernelSyncOnAddressWait64" : "sceKernelSyncOnAddressWait32",
|
||||
waiter.WakeKey,
|
||||
waiter,
|
||||
deadline))
|
||||
|
|
@ -584,24 +393,7 @@ public static class KernelSyncOnAddressCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
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);
|
||||
return SetReturn(ctx, waiter.WaitOnHost(timeout));
|
||||
}
|
||||
|
||||
private static void Register(ulong address, SyncWaiter waiter)
|
||||
|
|
@ -666,31 +458,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
// 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,20 +69,6 @@ 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",
|
||||
|
|
@ -147,7 +133,7 @@ public static class LibcStdioExports
|
|||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1063,7 +1049,7 @@ public static class LibcStdioExports
|
|||
}
|
||||
}
|
||||
|
||||
var replacement = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
|
||||
var replacement = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
lock (file.Gate)
|
||||
{
|
||||
if (file.IsClosed)
|
||||
|
|
|
|||
|
|
@ -12,82 +12,29 @@ 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 Http2CookieBox;
|
||||
private sealed record Http2Template(int ContextId, string UserAgent, int HttpVersion, bool AutoProxyConfig);
|
||||
|
||||
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);
|
||||
private sealed record Http2Request(int TemplateId, string Method, string Url, ulong ContentLength);
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Templates.Clear();
|
||||
Requests.Clear();
|
||||
CookieBoxes.Clear();
|
||||
_nextContextId = 0;
|
||||
_nextTemplateId = 0x1000;
|
||||
_nextRequestId = 0x2000;
|
||||
_nextCookieBoxId = 0x3000;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -172,12 +119,7 @@ public static class Http2Exports
|
|||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextTemplateId);
|
||||
Templates[id] = new Http2Template(
|
||||
contextId,
|
||||
userAgent,
|
||||
httpVersion,
|
||||
autoProxyConfig != 0,
|
||||
Http2Options.Default);
|
||||
Templates[id] = new Http2Template(contextId, userAgent, httpVersion, autoProxyConfig != 0);
|
||||
TraceHttp2("create_template", id, unchecked((ulong)contextId), ctx[CpuRegister.Rsi], unchecked((ulong)httpVersion), unchecked((ulong)autoProxyConfig));
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
|
@ -198,175 +140,6 @@ 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,
|
||||
|
|
@ -374,7 +147,7 @@ public static class Http2Exports
|
|||
public static int Http2CreateRequestWithUrl(CpuContext ctx)
|
||||
{
|
||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Templates.TryGetValue(templateId, out var template))
|
||||
if (!Templates.ContainsKey(templateId))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
|
@ -386,73 +159,11 @@ public static class Http2Exports
|
|||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
Requests[id] = new Http2Request(
|
||||
templateId,
|
||||
method,
|
||||
url,
|
||||
ctx[CpuRegister.Rcx],
|
||||
template.Options,
|
||||
[]);
|
||||
Requests[id] = new Http2Request(templateId, method, url, ctx[CpuRegister.Rcx]);
|
||||
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,
|
||||
|
|
@ -491,100 +202,6 @@ 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,42 +14,28 @@ 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;
|
||||
|
|
@ -66,26 +52,6 @@ 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,
|
||||
|
|
@ -150,11 +116,6 @@ 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();
|
||||
|
|
@ -234,12 +195,6 @@ 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);
|
||||
|
|
@ -760,291 +715,6 @@ 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",
|
||||
|
|
@ -1146,53 +816,6 @@ 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",
|
||||
|
|
@ -1402,39 +1025,6 @@ 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",
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
// 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,22 +13,15 @@ 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 NpAuthErrorInvalidArgument = unchecked((int)0x80550301);
|
||||
private const int NpErrorSignedOut = unchecked((int)0x80550006);
|
||||
private const int AuthorizationCodeSize = 136;
|
||||
private const int IdTokenSize = 4104;
|
||||
private const int NpAuthErrorServiceDown = unchecked((int)0x80550401);
|
||||
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(bool async)
|
||||
private sealed class AuthRequest
|
||||
{
|
||||
public bool Async { get; } = async;
|
||||
|
||||
public bool Completed { get; set; }
|
||||
|
||||
public bool Aborted { get; set; }
|
||||
|
||||
public int Result { get; set; }
|
||||
|
|
@ -54,7 +47,7 @@ public static class NpAuthExports
|
|||
ExportName = "sceNpAuthCreateRequest",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx, async: false);
|
||||
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthCreateAsyncRequest",
|
||||
|
|
@ -70,7 +63,7 @@ public static class NpAuthExports
|
|||
return ctx.SetReturn(NpAuthErrorInvalidSize);
|
||||
}
|
||||
|
||||
return CreateRequest(ctx, async: true);
|
||||
return CreateRequest(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -141,23 +134,21 @@ public static class NpAuthExports
|
|||
ExportName = "sceNpAuthGetAuthorizationCodeV3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) =>
|
||||
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], AuthorizationCodeSize, ctx[CpuRegister.Rcx]);
|
||||
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthGetIdTokenV3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetIdTokenV3(CpuContext ctx) =>
|
||||
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], IdTokenSize, 0);
|
||||
public static int NpAuthGetIdTokenV3(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthGetAuthorizedAppCode",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => CompleteSignedOut(ctx, 0, 0, 0);
|
||||
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
|
||||
private static int CreateRequest(CpuContext ctx, bool async)
|
||||
private static int CreateRequest(CpuContext ctx)
|
||||
{
|
||||
lock (RequestGate)
|
||||
{
|
||||
|
|
@ -169,7 +160,7 @@ public static class NpAuthExports
|
|||
while (true)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
if (Requests.TryAdd(id, new AuthRequest(async)))
|
||||
if (Requests.TryAdd(id, new AuthRequest()))
|
||||
{
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
|
@ -177,20 +168,8 @@ public static class NpAuthExports
|
|||
}
|
||||
}
|
||||
|
||||
private static int CompleteSignedOut(CpuContext ctx, ulong outputAddress, int outputSize, ulong issuerAddress)
|
||||
private static int RejectOnlineOperation(CpuContext ctx)
|
||||
{
|
||||
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);
|
||||
|
|
@ -198,21 +177,8 @@ public static class NpAuthExports
|
|||
|
||||
lock (request)
|
||||
{
|
||||
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);
|
||||
request.Result = request.Aborted ? NpAuthErrorAborted : NpAuthErrorServiceDown;
|
||||
return ctx.SetReturn(request.Result);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,26 +95,6 @@ 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,8 +11,6 @@ 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,14 +64,9 @@ internal static class NpUniversalDataSystemState
|
|||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
ResetLocked();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
ResetLocked();
|
||||
_initialized = true;
|
||||
_poolSize = poolSize;
|
||||
UpdateMaximumInUseLocked();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +85,8 @@ internal static class NpUniversalDataSystemState
|
|||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_initialized = true;
|
||||
context = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
context = NextPositiveId(ref _nextContext);
|
||||
|
|
@ -484,7 +480,7 @@ internal static class NpUniversalDataSystemState
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetRuntimeState()
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
|
|
@ -492,8 +488,6 @@ internal static class NpUniversalDataSystemState
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
|
||||
private static ulong CreateObjectLocked(UdsObjectNode node)
|
||||
{
|
||||
var handle = ObjectHandleBase | ++_nextObject;
|
||||
|
|
|
|||
|
|
@ -12,27 +12,9 @@ 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",
|
||||
|
|
@ -69,11 +51,6 @@ 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);
|
||||
}
|
||||
|
|
@ -97,20 +74,6 @@ 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",
|
||||
|
|
@ -134,34 +97,9 @@ 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",
|
||||
|
|
@ -233,25 +171,10 @@ 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,8 +11,6 @@ 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);
|
||||
|
|
@ -21,10 +19,8 @@ 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 SpecialPortType = 2;
|
||||
private const int RemotePortType = 16;
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
private const float StandardGravity = 9.80665f;
|
||||
|
|
@ -35,32 +31,11 @@ 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 readonly record struct PadOpenKey(int UserId, int Type, int Index);
|
||||
private static bool IsPrimaryPadHandle(int handle) =>
|
||||
handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(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;
|
||||
}
|
||||
}
|
||||
internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0);
|
||||
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||
|
||||
[ThreadStatic]
|
||||
|
|
@ -70,9 +45,6 @@ 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;
|
||||
|
|
@ -90,18 +62,6 @@ 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",
|
||||
|
|
@ -116,6 +76,12 @@ 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",
|
||||
|
|
@ -131,24 +97,22 @@ public static class PadExports
|
|||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId == -1)
|
||||
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
return PadHandleByKey.TryGetValue(new PadOpenKey(userId, type, index), out var handle)
|
||||
? ctx.SetReturn(handle)
|
||||
: ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -159,7 +123,8 @@ public static class PadExports
|
|||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
if (!IsSupportedOpenTuple(userId, type, index, extended))
|
||||
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
|
@ -173,42 +138,8 @@ 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.");
|
||||
}
|
||||
|
||||
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;
|
||||
Volatile.Write(ref _padOpened, 1);
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -219,16 +150,9 @@ public static class PadExports
|
|||
public static int PadClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
if (!PadKeyByHandle.Remove(handle, out var key))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
PadHandleByKey.Remove(key);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -239,7 +163,7 @@ public static class PadExports
|
|||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -256,7 +180,7 @@ public static class PadExports
|
|||
public static int PadSetTiltCorrectionState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsOpenPadHandle(handle)
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -269,7 +193,7 @@ public static class PadExports
|
|||
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsOpenPadHandle(handle)
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -283,7 +207,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -334,7 +258,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -348,7 +272,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[0x14];
|
||||
Span<byte> information = stackalloc byte[0x20];
|
||||
information.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
|
||||
|
||||
|
|
@ -357,57 +281,6 @@ 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",
|
||||
|
|
@ -417,7 +290,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -442,7 +315,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 (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -476,7 +349,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -514,7 +387,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -702,7 +575,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -732,7 +605,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -761,7 +634,7 @@ public static class PadExports
|
|||
public static int PadResetLightBar(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsOpenPadHandle(handle))
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -778,7 +651,7 @@ public static class PadExports
|
|||
public static int PadResetOrientation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsOpenPadHandle(handle)
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -791,7 +664,7 @@ public static class PadExports
|
|||
public static int PadIsRemoteController(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsOpenPadHandle(handle)
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -892,17 +765,6 @@ 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,7 +19,6 @@ 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;
|
||||
|
|
@ -30,10 +29,6 @@ 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;
|
||||
|
||||
|
|
@ -236,7 +231,7 @@ public static class PlayGoExports
|
|||
chunkIds = _metadata.ChunkIds;
|
||||
}
|
||||
|
||||
var availableEntries = (uint)chunkIds.Length;
|
||||
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
|
||||
if (outChunkIdList == 0)
|
||||
{
|
||||
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
||||
|
|
@ -254,7 +249,7 @@ public static class PlayGoExports
|
|||
|
||||
for (uint i = 0; i < entriesToWrite; i++)
|
||||
{
|
||||
var chunkId = chunkIds[i];
|
||||
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
||||
if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
|
|
@ -267,91 +262,6 @@ 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",
|
||||
|
|
@ -775,7 +685,10 @@ 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 CreateBaseChunkMetadata(PlayGoChunkIdKnowledge.Authoritative);
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
[(ushort)0],
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
|
|
@ -805,27 +718,14 @@ 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,
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
chunkIds.Length == 0
|
||||
? PlayGoChunkIdKnowledge.Unknown
|
||||
: 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
|
||||
|
|
@ -931,7 +831,7 @@ public static class PlayGoExports
|
|||
}
|
||||
}
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
|
|
@ -946,8 +846,6 @@ public static class PlayGoExports
|
|||
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
|
||||
private enum PlayGoChunkIdKnowledge
|
||||
{
|
||||
Unknown,
|
||||
|
|
|
|||
|
|
@ -74,11 +74,9 @@ 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 SaveDataTitleIdStructSize = 0x10;
|
||||
private const int SaveDataEventSize = 0x68;
|
||||
private const int MountInfoSize = 0x30;
|
||||
private const int SaveDataEventSize = 0x60;
|
||||
private const int MountInfoSize = 0x40;
|
||||
private const uint SaveDataBlockSize = 65536;
|
||||
private const ulong SaveDataBlocksMax = 16384;
|
||||
|
||||
|
|
@ -88,79 +86,16 @@ 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 TitleId,
|
||||
string DirName);
|
||||
private readonly record struct SaveDataEvent(uint Type, int ErrorCode, int UserId, string DirName);
|
||||
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
|
||||
|
||||
private static void EnqueueEvent(
|
||||
uint type,
|
||||
int userId,
|
||||
string titleId,
|
||||
string dirName,
|
||||
int errorCode = 0)
|
||||
private static void EnqueueEvent(uint type, int userId, string dirName, int errorCode = 0)
|
||||
{
|
||||
lock (_eventGate)
|
||||
{
|
||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, titleId, dirName));
|
||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, dirName));
|
||||
}
|
||||
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);
|
||||
TraceSaveData($"event.enqueue type={type} user={userId} dir='{dirName}' err=0x{errorCode:X}");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -196,14 +131,7 @@ public static class SaveDataExports
|
|||
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
|
||||
WriteFixedAscii(
|
||||
ev.Slice(0x10, SaveDataTitleIdStructSize),
|
||||
pending.TitleId,
|
||||
SaveDataTitleIdSize);
|
||||
WriteFixedAscii(
|
||||
ev.Slice(0x20, SaveDataDirNameSize),
|
||||
pending.DirName,
|
||||
SaveDataDirNameSize);
|
||||
WriteAscii(ev.Slice(0x10, SaveDataDirNameSize), pending.DirName);
|
||||
if (!ctx.Memory.TryWrite(eventAddress, ev))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -689,11 +617,6 @@ 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];
|
||||
|
|
@ -1318,19 +1241,6 @@ 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)];
|
||||
|
|
@ -1576,11 +1486,7 @@ 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,
|
||||
ResolveConfiguredTitleId(),
|
||||
"sce_sdmemory");
|
||||
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
// 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,6 +64,14 @@ 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);
|
||||
|
|
@ -106,14 +114,6 @@ 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,17 +34,6 @@ 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",
|
||||
|
|
@ -155,30 +144,6 @@ 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,18 +398,15 @@ public static partial class VideoOutExports
|
|||
public static int VideoOutClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
GuestGpu.Current.RunAfterPendingCommandStreams(() =>
|
||||
lock (_stateGate)
|
||||
{
|
||||
lock (_stateGate)
|
||||
foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray())
|
||||
{
|
||||
foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray())
|
||||
{
|
||||
CancelFlipLocked(request);
|
||||
}
|
||||
_ports.Remove(handle);
|
||||
Monitor.PulseAll(_stateGate);
|
||||
CancelFlipLocked(request);
|
||||
}
|
||||
});
|
||||
_ports.Remove(handle);
|
||||
Monitor.PulseAll(_stateGate);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
|
@ -1249,42 +1246,36 @@ 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;
|
||||
}
|
||||
|
||||
var result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
GuestGpu.Current.RunAfterPendingCommandStreams(() =>
|
||||
lock (_stateGate)
|
||||
{
|
||||
lock (_stateGate)
|
||||
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||
{
|
||||
if (!_ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
result = OrbisVideoOutErrorInvalidHandle;
|
||||
return;
|
||||
}
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||
port.Groups[attributeIndex] = null;
|
||||
foreach (var slot in port.BufferSlots)
|
||||
{
|
||||
if (slot.GroupIndex == attributeIndex)
|
||||
{
|
||||
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;
|
||||
}
|
||||
slot.GroupIndex = -1;
|
||||
slot.AddressLeft = 0;
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
[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 || target.Request.Description.Metadata.Range.Size != 0)
|
||||
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,26 +72,6 @@ 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,8 +110,6 @@ 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)
|
||||
{
|
||||
|
|
@ -129,12 +127,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
|
||||
BindImage(imageIdentifier, storage);
|
||||
var descriptor = new TextureDescriptorWords(words);
|
||||
if (ShouldTraceTextureBindings() &&
|
||||
(!_traceVolumeTextureBindingsOnly || image.Dimension == ImageDimension.Dim3D))
|
||||
if (ShouldTraceTextureBindings())
|
||||
{
|
||||
var cached = _imageCache.GetImage(imageIdentifier);
|
||||
var description = cached.Description;
|
||||
var line =
|
||||
Console.Error.WriteLine(
|
||||
$"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} " +
|
||||
|
|
@ -142,14 +139,7 @@ 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} " +
|
||||
$"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);
|
||||
}
|
||||
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format}");
|
||||
}
|
||||
return new TextureResource
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ 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;
|
||||
|
|
@ -584,15 +583,10 @@ 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 = &floatControls,
|
||||
PNext = &pushDescriptorProperties,
|
||||
};
|
||||
var subgroup = new PhysicalDeviceSubgroupProperties
|
||||
{
|
||||
|
|
@ -605,8 +599,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
PNext = &subgroup,
|
||||
};
|
||||
_vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2);
|
||||
_supportsShaderSignedZeroInfNanPreserveFloat32 =
|
||||
floatControls.ShaderSignedZeroInfNanPreserveFloat32;
|
||||
SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages);
|
||||
_canRequireComputeSubgroup32 =
|
||||
subgroup.SubgroupSize != RdnaSubgroupSize &&
|
||||
|
|
@ -665,9 +657,6 @@ 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} " +
|
||||
|
|
@ -753,7 +742,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
|
||||
_supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp;
|
||||
_supportsDepthBounds = supportedFeatures.DepthBounds;
|
||||
_supportsShaderClipDistance = supportedFeatures.ShaderClipDistance;
|
||||
_supportsFillRectangle = IsDeviceExtensionAvailable(FillRectangleExtensionName);
|
||||
var enabledFeatures = new PhysicalDeviceFeatures
|
||||
{
|
||||
|
|
@ -763,8 +751,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
|
||||
ShaderInt64 = supportedFeatures.ShaderInt64,
|
||||
ShaderFloat64 = supportedFeatures.ShaderFloat64,
|
||||
ShaderClipDistance = supportedFeatures.ShaderClipDistance,
|
||||
ShaderCullDistance = supportedFeatures.ShaderCullDistance,
|
||||
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
|
||||
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
|
||||
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
|
||||
|
|
|
|||
|
|
@ -91,9 +91,10 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceTextureBindingsEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") is "1" or "volume";
|
||||
private static readonly bool _traceVolumeTextureBindingsOnly =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") == "volume";
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly uint _traceGuestImageWidth =
|
||||
uint.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_WIDTH"),
|
||||
|
|
|
|||
|
|
@ -87,12 +87,9 @@ 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;
|
||||
bool IShaderPipelineHost.ClipDistanceEnabled => _supportsShaderClipDistance;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private bool _supportsDepthClipControl;
|
||||
private bool _supportsDepthClipEnable;
|
||||
private bool _supportsDepthBounds;
|
||||
private bool _supportsShaderClipDistance;
|
||||
private bool _supportsFillRectangle;
|
||||
private RenderHostLimits _renderHostLimits;
|
||||
private IGuestBackedSpace _guestBacking = null!;
|
||||
|
|
@ -574,8 +573,7 @@ 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 ||
|
||||
image.Description.Metadata.Range.Size != 0)
|
||||
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -604,7 +602,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_vk.CmdClearColorImage(command, image.Backing.Handle, ImageLayout.TransferDstOptimal, &clearValue, 1, &vkRange);
|
||||
if (!tracked)
|
||||
{
|
||||
_bufferCache.FillDccMetadata(guestSlice, sliceSize, uint.MaxValue);
|
||||
_bufferCache.FillBuffer(guestSlice, sliceSize, uint.MaxValue, false);
|
||||
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
|
||||
{
|
||||
RenderTrace.Write(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
private readonly GpuWorkerRelay _relay = new(WakeRenderThread);
|
||||
|
||||
internal GpuWorkerRelay Relay => _relay;
|
||||
private readonly SubmissionContext _submissionContext = new();
|
||||
|
|
|
|||
|
|
@ -162,8 +162,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
bool execGuardElision)
|
||||
{
|
||||
if (!ShaderProgramCache.TryCompilePrewarm(
|
||||
record, code, compiler, sharedInt64Atomics, execGuardElision,
|
||||
_supportsShaderSignedZeroInfNanPreserveFloat32, out var compiled, out var layout, out var error))
|
||||
record, code, compiler, sharedInt64Atomics, execGuardElision, out var compiled, out var layout, out var error))
|
||||
{
|
||||
NoteShaderPrewarmFailure(record, error);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ 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;
|
||||
|
|
@ -256,7 +255,6 @@ 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,15 +442,6 @@ 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,
|
||||
|
|
@ -463,11 +454,7 @@ public static partial class Gen5SpirvTranslator
|
|||
case "VLogF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
30,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
Ext(30, _floatType, GetFloatSource(instruction, 0)));
|
||||
break;
|
||||
case "VLdexpF32":
|
||||
result = EmitFloatResult(
|
||||
|
|
@ -481,20 +468,12 @@ public static partial class Gen5SpirvTranslator
|
|||
case "VExpF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
29,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
Ext(29, _floatType, GetFloatSource(instruction, 0)));
|
||||
break;
|
||||
case "VRsqF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
32,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
Ext(32, _floatType, GetFloatSource(instruction, 0)));
|
||||
break;
|
||||
case "VRcpF16":
|
||||
result = EmitFloat16Result(instruction, destination, _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1), GetFloat16Source(instruction, 0)));
|
||||
|
|
@ -563,11 +542,7 @@ public static partial class Gen5SpirvTranslator
|
|||
case "VSqrtF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
31,
|
||||
_floatType,
|
||||
EmitFlushF32DenormToSignedZero(
|
||||
GetFloatSource(instruction, 0))));
|
||||
Ext(31, _floatType, GetFloatSource(instruction, 0)));
|
||||
break;
|
||||
case "VSinF32":
|
||||
result = EmitFloatResult(
|
||||
|
|
@ -578,9 +553,7 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
EmitTrigCycleF32(
|
||||
GetFloatSource(instruction, 0),
|
||||
preserveSignedZero: true),
|
||||
GetFloatSource(instruction, 0),
|
||||
Float(MathF.Tau))));
|
||||
break;
|
||||
case "VCosF32":
|
||||
|
|
@ -592,9 +565,7 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
EmitTrigCycleF32(
|
||||
GetFloatSource(instruction, 0),
|
||||
preserveSignedZero: false),
|
||||
GetFloatSource(instruction, 0),
|
||||
Float(MathF.Tau))));
|
||||
break;
|
||||
case "VAddF32":
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,7 +185,6 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint _layerOutput;
|
||||
private uint _viewportIndexOutput;
|
||||
private uint _clipDistanceCount;
|
||||
private uint _invalidPositionClipDistance = uint.MaxValue;
|
||||
private uint _cullDistanceCount;
|
||||
private uint _vertexIndexInput;
|
||||
private uint _instanceIndexInput;
|
||||
|
|
@ -518,13 +517,6 @@ 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);
|
||||
|
|
@ -562,11 +554,6 @@ 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())
|
||||
|
|
@ -6601,20 +6588,6 @@ public static partial class Gen5SpirvTranslator
|
|||
outputValue,
|
||||
Load(_vec4Type, outputVariable));
|
||||
Store(outputVariable, outputValue);
|
||||
if (export.Target == 12 && _invalidPositionClipDistance != uint.MaxValue)
|
||||
{
|
||||
var equal = _module.AddInstruction(
|
||||
SpirvOp.FOrdEqual,
|
||||
_module.TypeVector(_boolType, 4),
|
||||
outputValue,
|
||||
_module.ConstantNull(_vec4Type));
|
||||
var invalid = _module.AddInstruction(SpirvOp.All, _boolType, equal);
|
||||
// A zero position has an undefined perspective divide. Collapse its primitive
|
||||
// to the remaining edge, as the guest's clipping-error cull does.
|
||||
var distance = _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, invalid, Float(-1f), Float(0f));
|
||||
StoreDistanceConditional(_clipDistanceOutput, _invalidPositionClipDistance, distance);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -6723,14 +6696,6 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
if (_request.SupportsClipDistance && clipCount + cullCount < 8 &&
|
||||
_request.Program.Instructions.Any(static instruction =>
|
||||
instruction.Control is Gen5ExportControl { Target: 12, EnableMask: not 0 }))
|
||||
{
|
||||
// Use a separate plane so auxiliary position exports keep their own distances.
|
||||
_invalidPositionClipDistance = clipCount++;
|
||||
}
|
||||
|
||||
if (needPointSize)
|
||||
{
|
||||
_pointSizeOutput = DeclareBuiltInOutput(
|
||||
|
|
|
|||
|
|
@ -213,7 +213,6 @@ public enum SpirvCapability : uint
|
|||
ShaderViewportIndexLayerExt = 5254,
|
||||
RuntimeDescriptorArray = 5302,
|
||||
PhysicalStorageBufferAddresses = 5347,
|
||||
SignedZeroInfNanPreserve = 4466,
|
||||
}
|
||||
|
||||
public enum SpirvStorageClass : uint
|
||||
|
|
@ -243,7 +242,6 @@ public enum SpirvExecutionMode : uint
|
|||
OriginUpperLeft = 7,
|
||||
DepthReplacing = 12,
|
||||
LocalSize = 17,
|
||||
SignedZeroInfNanPreserve = 4461,
|
||||
}
|
||||
|
||||
public enum SpirvDecoration : uint
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ public enum TilerBlockShape
|
|||
Prt64KB3D,
|
||||
RenderTarget64KB,
|
||||
Depth64KB,
|
||||
RenderTarget64KBGen5,
|
||||
}
|
||||
|
||||
// Assembles the tiler compute shaders: block copies per shape and element size,
|
||||
|
|
@ -252,15 +251,6 @@ 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)],
|
||||
|
|
@ -324,7 +314,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 or TilerBlockShape.RenderTarget64KBGen5 => (ThinExtent(65536, bytes, 256, 128, 64), 65536),
|
||||
TilerBlockShape.Standard64KB or TilerBlockShape.Prt64KB or TilerBlockShape.RenderTarget64KB => (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)),
|
||||
|
|
@ -358,8 +348,6 @@ 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,14 +158,12 @@ public static class Gen5ExecFullAnalysis
|
|||
written |= Bit(sdst) | Bit(sdst + 1);
|
||||
}
|
||||
|
||||
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||
if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null)
|
||||
{
|
||||
written |= Bit(ExecLow) | Bit(ExecHigh);
|
||||
}
|
||||
else if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null)
|
||||
{
|
||||
// VOPC compares write VCC.
|
||||
written |= Bit(106) | Bit(107);
|
||||
// VOPC compares write VCC, V_CMPX writes EXEC.
|
||||
written |= opcode.StartsWith("VCmpx", StringComparison.Ordinal)
|
||||
? Bit(ExecLow) | Bit(ExecHigh)
|
||||
: Bit(106) | Bit(107);
|
||||
}
|
||||
|
||||
if (opcode.StartsWith('V') && opcode.Contains("Co", StringComparison.Ordinal) && scalarDestination is null)
|
||||
|
|
|
|||
|
|
@ -1,419 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// Compile the immutable SRT graph once, not the draw's pointers or memory contents.
|
||||
// Small methods bound JIT cost; dependency calls use constant indices and a dense cache.
|
||||
internal sealed class CompiledResourceEvaluator
|
||||
{
|
||||
private const int NodesPerMethod = 64;
|
||||
private delegate bool EvaluateFunction(RuntimeValueEvaluator evaluator, int index, out ulong result);
|
||||
private readonly Dictionary<ScalarValue, int> _indices;
|
||||
private readonly EvaluateFunction[] _functions;
|
||||
internal ScalarValue[] Values { get; }
|
||||
internal int[][] SourceWords { get; }
|
||||
internal int[] TableWords { get; }
|
||||
internal int Count => Values.Length;
|
||||
|
||||
private static readonly Dictionary<string, MethodInfo> Helpers = new[]
|
||||
{
|
||||
nameof(RuntimeValueEvaluator.BeginCompiled), nameof(RuntimeValueEvaluator.EndCompiled),
|
||||
nameof(RuntimeValueEvaluator.StoreCompiled), nameof(RuntimeValueEvaluator.EvaluateCompiled),
|
||||
nameof(RuntimeValueEvaluator.EvaluateCompiledSpecial), nameof(RuntimeValueEvaluator.IsCompiledActiveMask),
|
||||
nameof(RuntimeValueEvaluator.ReadUserData), nameof(RuntimeValueEvaluator.ReadShaderBase), nameof(RuntimeValueEvaluator.ReadRawWord),
|
||||
}.ToDictionary(name => name, name => typeof(RuntimeValueEvaluator).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance)!);
|
||||
private static readonly MethodInfo EvaluateOperation = typeof(ScalarOperationSemantics)
|
||||
.GetMethod(nameof(ScalarOperationSemantics.TryEvaluateFixed), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private CompiledResourceEvaluator(ShaderResourcePlan plan, List<ScalarValue> values, Dictionary<ScalarValue, int> indices,
|
||||
Dictionary<ScalarValue, ScalarValue?> phis)
|
||||
{
|
||||
Values = [.. values];
|
||||
_indices = indices;
|
||||
SourceWords = plan.DescriptorSources.Select(source => source.Dwords.Select(word => indices[word]).ToArray()).ToArray();
|
||||
TableWords = plan.TableReads.Select(read => indices[read.Value]).ToArray();
|
||||
_functions = new EvaluateFunction[(Count + NodesPerMethod - 1) / NodesPerMethod];
|
||||
for (var chunk = 0; chunk < _functions.Length; chunk++)
|
||||
_functions[chunk] = Compile(plan, chunk * NodesPerMethod, phis);
|
||||
}
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentQueue<ShaderResourcePlan> Pending = new();
|
||||
private static readonly SemaphoreSlim PendingSignal = new(0);
|
||||
private static int _compilerStarted;
|
||||
|
||||
internal static void Enqueue(ShaderResourcePlan plan)
|
||||
{
|
||||
Pending.Enqueue(plan);
|
||||
if (Interlocked.Exchange(ref _compilerStarted, 1) == 0)
|
||||
{
|
||||
new Thread(CompilePending) { IsBackground = true, Priority = ThreadPriority.BelowNormal, Name = "SRT compiler" }.Start();
|
||||
}
|
||||
|
||||
PendingSignal.Release();
|
||||
}
|
||||
|
||||
private static void CompilePending()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
PendingSignal.Wait();
|
||||
while (Pending.TryDequeue(out var plan)) plan.CompileEvaluatorNow();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryGetIndex(ScalarValue value, out int index) => _indices.TryGetValue(value, out index);
|
||||
internal bool Evaluate(RuntimeValueEvaluator evaluator, int index, out ulong result) =>
|
||||
_functions[index / NodesPerMethod](evaluator, index, out result);
|
||||
|
||||
internal static CompiledResourceEvaluator? Build(ShaderResourcePlan plan)
|
||||
{
|
||||
if (!RuntimeFeature.IsDynamicCodeSupported || Environment.GetEnvironmentVariable("SHARPEMU_SRT_NATIVE") == "0") return null;
|
||||
|
||||
var values = new List<ScalarValue>();
|
||||
var indices = new Dictionary<ScalarValue, int>();
|
||||
var phis = new Dictionary<ScalarValue, ScalarValue?>();
|
||||
void Add(ScalarValue value)
|
||||
{
|
||||
if (indices.TryAdd(value, values.Count)) values.Add(value);
|
||||
}
|
||||
foreach (var source in plan.DescriptorSources)
|
||||
foreach (var word in source.Dwords) Add(word);
|
||||
foreach (var read in plan.TableReads) Add(read.Value);
|
||||
foreach (var read in plan.DynamicReads) Add(read);
|
||||
foreach (var branch in plan.ResourceBranches)
|
||||
if (branch.Condition is { } condition) Add(condition);
|
||||
foreach (var range in plan.DeviceAddressRanges)
|
||||
{
|
||||
Add(range.BaseLow);
|
||||
Add(range.BaseHigh);
|
||||
}
|
||||
foreach (var image in plan.IndirectImages) Add(image.Key);
|
||||
foreach (var candidates in plan.BufferCandidateTables)
|
||||
if (candidates.Limit is { } limit) Add(limit);
|
||||
|
||||
// An iterative walk also admits cycles. At run time, an in-progress dense slot
|
||||
// fails just like the interpreter's visiting set, without recursing forever.
|
||||
for (var index = 0; index < values.Count; index++)
|
||||
{
|
||||
var value = values[index];
|
||||
if (value.Kind == ScalarValueKind.Phi)
|
||||
{
|
||||
var invariant = plan.Graph.ResolveInvariantPhi(value);
|
||||
phis[value] = invariant;
|
||||
if (invariant is not null) Add(invariant);
|
||||
}
|
||||
else if (value.Kind == ScalarValueKind.FirstLane)
|
||||
{
|
||||
Add(value.Operands[0]);
|
||||
}
|
||||
else if (value.Kind == ScalarValueKind.ResourceTableWord)
|
||||
{
|
||||
if (value.Payload < (ulong)plan.TableReads.Count) Add(plan.TableReads[(int)value.Payload].Value);
|
||||
}
|
||||
else if (value.Kind is ScalarValueKind.ScalarAddressWord or ScalarValueKind.ScalarBufferWord)
|
||||
{
|
||||
if ((uint)value.MemoryIndex >= (uint)plan.Memory.Count || value.Operands.Length < 2) continue;
|
||||
foreach (var word in value.Operands[0].Operands) Add(word);
|
||||
Add(value.Operands[1]);
|
||||
}
|
||||
else if (value.Kind == ScalarValueKind.Select ||
|
||||
(value.Kind == ScalarValueKind.Operation && RuntimeValueValidator.IsUniformOperation(value.Operation)))
|
||||
{
|
||||
foreach (var operand in value.Operands) Add(operand);
|
||||
}
|
||||
}
|
||||
return values.Count == 0 ? null : new(plan, values, indices, phis);
|
||||
}
|
||||
|
||||
private EvaluateFunction Compile(ShaderResourcePlan plan, int first, Dictionary<ScalarValue, ScalarValue?> phis)
|
||||
{
|
||||
var count = Math.Min(NodesPerMethod, Count - first);
|
||||
var method = new DynamicMethod($"Srt_{plan.Hash:X16}_{first}", typeof(bool),
|
||||
[typeof(RuntimeValueEvaluator), typeof(int), typeof(ulong).MakeByRefType()], typeof(CompiledResourceEvaluator).Module, true);
|
||||
var il = method.GetILGenerator();
|
||||
var result = il.DeclareLocal(typeof(ulong));
|
||||
var success = il.DeclareLocal(typeof(bool));
|
||||
var status = il.DeclareLocal(typeof(int));
|
||||
var operands = Enumerable.Range(0, 5).Select(_ => il.DeclareLocal(typeof(ulong))).ToArray();
|
||||
var failed = il.DefineLabel();
|
||||
var store = il.DefineLabel();
|
||||
var exit = il.DefineLabel();
|
||||
var cases = Enumerable.Range(0, count).Select(_ => il.DefineLabel()).ToArray();
|
||||
|
||||
void Call(string helper) => il.Emit(OpCodes.Call, Helpers[helper]);
|
||||
void Constant(ulong value) => il.Emit(OpCodes.Ldc_I8, unchecked((long)value));
|
||||
void Operand(ScalarValue value, LocalBuilder destination)
|
||||
{
|
||||
if (value.IsConstant)
|
||||
{
|
||||
Constant(value.Payload);
|
||||
il.Emit(OpCodes.Stloc, destination);
|
||||
return;
|
||||
}
|
||||
var index = _indices[value];
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldc_I4, index);
|
||||
il.Emit(OpCodes.Ldloca, destination);
|
||||
if (index >= first && index < first + count) il.Emit(OpCodes.Call, method);
|
||||
else Call(nameof(RuntimeValueEvaluator.EvaluateCompiled));
|
||||
il.Emit(OpCodes.Brfalse, failed);
|
||||
}
|
||||
void Load(int index, bool narrow = false)
|
||||
{
|
||||
il.Emit(OpCodes.Ldloc, operands[index]);
|
||||
if (narrow) il.Emit(OpCodes.Conv_U4);
|
||||
}
|
||||
void Special(int index)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldc_I4, index);
|
||||
il.Emit(OpCodes.Ldloca, result);
|
||||
Call(nameof(RuntimeValueEvaluator.EvaluateCompiledSpecial));
|
||||
il.Emit(OpCodes.Brfalse, failed);
|
||||
}
|
||||
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
il.Emit(OpCodes.Ldarg_2);
|
||||
Call(nameof(RuntimeValueEvaluator.BeginCompiled));
|
||||
il.Emit(OpCodes.Stloc, status);
|
||||
var compute = il.DefineLabel();
|
||||
il.Emit(OpCodes.Ldloc, status);
|
||||
il.Emit(OpCodes.Brfalse, compute);
|
||||
il.Emit(OpCodes.Ldloc, status);
|
||||
il.Emit(OpCodes.Ldc_I4_0);
|
||||
il.Emit(OpCodes.Cgt);
|
||||
il.Emit(OpCodes.Ret);
|
||||
il.MarkLabel(compute);
|
||||
il.BeginExceptionBlock();
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
il.Emit(OpCodes.Ldc_I4, first);
|
||||
il.Emit(OpCodes.Sub);
|
||||
il.Emit(OpCodes.Switch, cases);
|
||||
il.Emit(OpCodes.Br, failed);
|
||||
|
||||
for (var offset = 0; offset < count; offset++)
|
||||
{
|
||||
var index = first + offset;
|
||||
var value = Values[index];
|
||||
il.MarkLabel(cases[offset]);
|
||||
switch (value.Kind)
|
||||
{
|
||||
case ScalarValueKind.Constant:
|
||||
Constant(value.Payload);
|
||||
il.Emit(OpCodes.Stloc, result);
|
||||
break;
|
||||
case ScalarValueKind.MemoryAperture:
|
||||
Constant(Gen5InlineConstants.DecodeAperture64((uint)value.Payload) >> 32);
|
||||
il.Emit(OpCodes.Stloc, result);
|
||||
break;
|
||||
case ScalarValueKind.UserData:
|
||||
if (value.UserDataRegister < plan.UserDataBase)
|
||||
{
|
||||
il.Emit(OpCodes.Br, failed);
|
||||
break;
|
||||
}
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldc_I4, unchecked((int)(value.UserDataRegister - plan.UserDataBase)));
|
||||
il.Emit(OpCodes.Ldloca, result);
|
||||
Call(nameof(RuntimeValueEvaluator.ReadUserData));
|
||||
il.Emit(OpCodes.Brfalse, failed);
|
||||
break;
|
||||
case ScalarValueKind.ShaderBase:
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
Call(nameof(RuntimeValueEvaluator.ReadShaderBase));
|
||||
il.Emit(OpCodes.Stloc, result);
|
||||
break;
|
||||
case ScalarValueKind.Phi:
|
||||
if (phis[value] is { } invariant) Operand(invariant, result);
|
||||
else il.Emit(OpCodes.Br, failed);
|
||||
break;
|
||||
case ScalarValueKind.FirstLane:
|
||||
case ScalarValueKind.ResourceTableWord:
|
||||
// These cross evaluator contexts: active-lane cache or clean reader.
|
||||
Special(index);
|
||||
break;
|
||||
case ScalarValueKind.Select:
|
||||
{
|
||||
var ordinary = il.DefineLabel();
|
||||
var chosen = il.DefineLabel();
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldc_I4, _indices[value.Operands[0]]);
|
||||
Call(nameof(RuntimeValueEvaluator.IsCompiledActiveMask));
|
||||
il.Emit(OpCodes.Brfalse, ordinary);
|
||||
Operand(value.Operands[1], result);
|
||||
il.Emit(OpCodes.Br, store);
|
||||
il.MarkLabel(ordinary);
|
||||
for (var operand = 0; operand < 3; operand++) Operand(value.Operands[operand], operands[operand]);
|
||||
// Ordinary selects are eager, including reads on the inactive arm.
|
||||
Load(0);
|
||||
il.Emit(OpCodes.Brfalse, chosen);
|
||||
Load(1);
|
||||
il.Emit(OpCodes.Stloc, result);
|
||||
il.Emit(OpCodes.Br, store);
|
||||
il.MarkLabel(chosen);
|
||||
Load(2);
|
||||
il.Emit(OpCodes.Stloc, result);
|
||||
break;
|
||||
}
|
||||
case ScalarValueKind.ScalarAddressWord:
|
||||
case ScalarValueKind.ScalarBufferWord:
|
||||
{
|
||||
var buffer = value.Kind == ScalarValueKind.ScalarBufferWord;
|
||||
if ((uint)value.MemoryIndex >= (uint)plan.Memory.Count || value.Operands.Length < 2 ||
|
||||
value.Operands[0].Operands.Length < 2 || (buffer && value.Operands[0].Operands.Length != 4))
|
||||
{
|
||||
il.Emit(OpCodes.Br, failed);
|
||||
break;
|
||||
}
|
||||
var handle = value.Operands[0];
|
||||
Operand(handle.Operands[0], operands[0]);
|
||||
Operand(handle.Operands[1], operands[1]);
|
||||
Operand(value.Operands[1], operands[2]);
|
||||
if (buffer)
|
||||
{
|
||||
Operand(handle.Operands[2], operands[3]);
|
||||
Operand(handle.Operands[3], operands[4]);
|
||||
}
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldc_I4, (int)value.Kind);
|
||||
Load(0);
|
||||
Load(1);
|
||||
Load(2, narrow: true);
|
||||
if (buffer) Load(3);
|
||||
else Constant(0);
|
||||
Constant(unchecked((ulong)(long)(int)plan.Memory[value.MemoryIndex].Offset));
|
||||
il.Emit(OpCodes.Ldloca, result);
|
||||
Call(nameof(RuntimeValueEvaluator.ReadRawWord));
|
||||
il.Emit(OpCodes.Brfalse, failed);
|
||||
break;
|
||||
}
|
||||
case ScalarValueKind.Operation:
|
||||
if (!RuntimeValueValidator.IsUniformOperation(value.Operation))
|
||||
{
|
||||
il.Emit(OpCodes.Br, failed);
|
||||
break;
|
||||
}
|
||||
if (value.Operands.Length > 4)
|
||||
{
|
||||
Special(index);
|
||||
break;
|
||||
}
|
||||
for (var operand = 0; operand < 4; operand++)
|
||||
{
|
||||
if (operand < value.Operands.Length) Operand(value.Operands[operand], operands[operand]);
|
||||
else
|
||||
{
|
||||
Constant(0);
|
||||
il.Emit(OpCodes.Stloc, operands[operand]);
|
||||
}
|
||||
}
|
||||
if (EmitArithmetic(il, value.Operation, operands)) il.Emit(OpCodes.Stloc, result);
|
||||
else
|
||||
{
|
||||
il.Emit(OpCodes.Ldc_I4, (int)value.Operation);
|
||||
for (var operand = 0; operand < 4; operand++) Load(operand);
|
||||
il.Emit(OpCodes.Ldloca, result);
|
||||
il.Emit(OpCodes.Call, EvaluateOperation);
|
||||
il.Emit(OpCodes.Brfalse, failed);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
il.Emit(OpCodes.Br, failed);
|
||||
break;
|
||||
}
|
||||
il.Emit(OpCodes.Br, store);
|
||||
}
|
||||
|
||||
il.MarkLabel(store);
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
il.Emit(OpCodes.Ldloc, result);
|
||||
Call(nameof(RuntimeValueEvaluator.StoreCompiled));
|
||||
il.Emit(OpCodes.Ldarg_2);
|
||||
il.Emit(OpCodes.Ldloc, result);
|
||||
il.Emit(OpCodes.Stind_I8);
|
||||
il.Emit(OpCodes.Ldc_I4_1);
|
||||
il.Emit(OpCodes.Stloc, success);
|
||||
il.Emit(OpCodes.Leave, exit);
|
||||
il.MarkLabel(failed);
|
||||
il.Emit(OpCodes.Leave, exit);
|
||||
il.BeginFinallyBlock();
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
Call(nameof(RuntimeValueEvaluator.EndCompiled));
|
||||
il.EndExceptionBlock();
|
||||
il.MarkLabel(exit);
|
||||
il.Emit(OpCodes.Ldloc, success);
|
||||
il.Emit(OpCodes.Ret);
|
||||
var function = method.CreateDelegate<EvaluateFunction>();
|
||||
RuntimeHelpers.PrepareDelegate(function);
|
||||
return function;
|
||||
}
|
||||
|
||||
private static bool EmitArithmetic(ILGenerator il, ScalarOperation operation, LocalBuilder[] operands)
|
||||
{
|
||||
if (operation is ScalarOperation.Construct64 or ScalarOperation.AddCarry32 or ScalarOperation.UMulHi32)
|
||||
{
|
||||
for (var operand = 0; operand < 2; operand++)
|
||||
{
|
||||
il.Emit(OpCodes.Ldloc, operands[operand]);
|
||||
il.Emit(OpCodes.Conv_U4);
|
||||
il.Emit(OpCodes.Conv_U8);
|
||||
if (operand == 1 && operation == ScalarOperation.Construct64)
|
||||
{
|
||||
il.Emit(OpCodes.Ldc_I4, 32);
|
||||
il.Emit(OpCodes.Shl);
|
||||
}
|
||||
}
|
||||
il.Emit(operation == ScalarOperation.Construct64 ? OpCodes.Or :
|
||||
operation == ScalarOperation.AddCarry32 ? OpCodes.Add : OpCodes.Mul);
|
||||
if (operation == ScalarOperation.UMulHi32)
|
||||
{
|
||||
il.Emit(OpCodes.Ldc_I4, 32);
|
||||
il.Emit(OpCodes.Shr_Un);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var narrow = operation is ScalarOperation.IAdd32 or ScalarOperation.ISub32 or ScalarOperation.IMul32 or
|
||||
ScalarOperation.And32 or ScalarOperation.Or32 or ScalarOperation.Xor32 or ScalarOperation.Not32 or
|
||||
ScalarOperation.ShiftLeft32 or ScalarOperation.ShiftRightLogical32 or ScalarOperation.ShiftRightArithmetic32;
|
||||
OpCode opcode;
|
||||
switch (operation)
|
||||
{
|
||||
case ScalarOperation.IAdd32: case ScalarOperation.IAdd64: opcode = OpCodes.Add; break;
|
||||
case ScalarOperation.ISub32: case ScalarOperation.ISub64: opcode = OpCodes.Sub; break;
|
||||
case ScalarOperation.IMul32: case ScalarOperation.IMul64: opcode = OpCodes.Mul; break;
|
||||
case ScalarOperation.And32: case ScalarOperation.And64: opcode = OpCodes.And; break;
|
||||
case ScalarOperation.Or32: opcode = OpCodes.Or; break;
|
||||
case ScalarOperation.Xor32: opcode = OpCodes.Xor; break;
|
||||
case ScalarOperation.Not32: opcode = OpCodes.Not; break;
|
||||
case ScalarOperation.ShiftLeft32: case ScalarOperation.ShiftLeft64: opcode = OpCodes.Shl; break;
|
||||
case ScalarOperation.ShiftRightLogical32: case ScalarOperation.ShiftRightLogical64: opcode = OpCodes.Shr_Un; break;
|
||||
case ScalarOperation.ShiftRightArithmetic32: case ScalarOperation.ShiftRightArithmetic64: opcode = OpCodes.Shr; break;
|
||||
default: return false;
|
||||
}
|
||||
il.Emit(OpCodes.Ldloc, operands[0]);
|
||||
if (narrow) il.Emit(OpCodes.Conv_U4);
|
||||
if (operation != ScalarOperation.Not32)
|
||||
{
|
||||
il.Emit(OpCodes.Ldloc, operands[1]);
|
||||
if (opcode == OpCodes.Shl || opcode == OpCodes.Shr || opcode == OpCodes.Shr_Un)
|
||||
{
|
||||
il.Emit(OpCodes.Conv_I4);
|
||||
il.Emit(OpCodes.Ldc_I4, narrow ? 31 : 63);
|
||||
il.Emit(OpCodes.And);
|
||||
}
|
||||
else if (narrow) il.Emit(OpCodes.Conv_U4);
|
||||
}
|
||||
il.Emit(opcode);
|
||||
if (narrow) il.Emit(OpCodes.Conv_U4);
|
||||
il.Emit(OpCodes.Conv_U8);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,18 +13,7 @@ internal sealed class RuntimeEvaluationScratch : IDisposable
|
|||
private bool _rented;
|
||||
|
||||
internal ScalarValueCache Values { get; } = new();
|
||||
internal CompiledValueCache CompiledValues { get; } = new();
|
||||
internal List<ScalarValue> Visiting { get; } = [];
|
||||
internal Stack<int> PendingBranches { get; } = new();
|
||||
private bool[] _visitedBranches = [];
|
||||
|
||||
internal bool[] PrepareBranches(int count)
|
||||
{
|
||||
if (_visitedBranches.Length < count) _visitedBranches = new bool[count];
|
||||
else Array.Clear(_visitedBranches, 0, count);
|
||||
PendingBranches.Clear();
|
||||
return _visitedBranches;
|
||||
}
|
||||
|
||||
internal static RuntimeEvaluationScratch Rent()
|
||||
{
|
||||
|
|
@ -43,60 +32,9 @@ internal sealed class RuntimeEvaluationScratch : IDisposable
|
|||
{
|
||||
if (!_rented) return;
|
||||
Values.Reset();
|
||||
CompiledValues.Reset();
|
||||
Visiting.Clear();
|
||||
PendingBranches.Clear();
|
||||
_rented = false;
|
||||
_nextAvailable = _available;
|
||||
_available = this;
|
||||
}
|
||||
}
|
||||
|
||||
// Plan-local indices avoid hashing graph nodes on every dependency evaluation.
|
||||
internal sealed class CompiledValueCache
|
||||
{
|
||||
private ulong[] _values = [];
|
||||
private uint[] _stamps = [];
|
||||
private uint _generation = 2;
|
||||
|
||||
internal void EnsureCapacity(int count)
|
||||
{
|
||||
if (_values.Length >= count) return;
|
||||
Array.Resize(ref _values, count);
|
||||
Array.Resize(ref _stamps, count);
|
||||
}
|
||||
|
||||
internal int Begin(int index, out ulong result)
|
||||
{
|
||||
result = 0;
|
||||
if (_stamps[index] == _generation)
|
||||
{
|
||||
result = _values[index];
|
||||
return 1;
|
||||
}
|
||||
if (_stamps[index] == _generation + 1) return -1;
|
||||
_stamps[index] = _generation + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
internal bool Contains(int index) => _stamps[index] == _generation;
|
||||
|
||||
internal void Store(int index, ulong value)
|
||||
{
|
||||
_values[index] = value;
|
||||
_stamps[index] = _generation;
|
||||
}
|
||||
|
||||
internal void End(int index)
|
||||
{
|
||||
if (_stamps[index] == _generation + 1) _stamps[index] = 0;
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
_generation = unchecked(_generation + 2);
|
||||
if (_generation != 0) return;
|
||||
Array.Clear(_stamps);
|
||||
_generation = 2;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ public sealed class RuntimeValueEvaluator
|
|||
private readonly ScalarValue? _activeMask;
|
||||
private readonly ScalarValueCache _cache;
|
||||
private readonly List<ScalarValue> _visiting;
|
||||
private readonly CompiledResourceEvaluator? _compiled;
|
||||
private readonly CompiledValueCache _compiledValues;
|
||||
|
||||
public RuntimeValueEvaluator(
|
||||
ShaderResourcePlan plan,
|
||||
|
|
@ -27,7 +25,7 @@ public sealed class RuntimeValueEvaluator
|
|||
IReadOnlyList<byte>? cleanFlatSlots = null,
|
||||
RuntimeValueEvaluator? cleanEvaluator = null,
|
||||
ScalarValue? activeMask = null)
|
||||
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, new ScalarValueCache(), [], new CompiledValueCache(), true)
|
||||
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, new ScalarValueCache(), [])
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -37,9 +35,8 @@ public sealed class RuntimeValueEvaluator
|
|||
ResourceRuntimeInputs inputs,
|
||||
IReadOnlyList<byte>? cleanFlatSlots = null,
|
||||
RuntimeValueEvaluator? cleanEvaluator = null,
|
||||
ScalarValue? activeMask = null,
|
||||
bool useCompiled = true)
|
||||
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, scratch.Values, scratch.Visiting, scratch.CompiledValues, useCompiled)
|
||||
ScalarValue? activeMask = null)
|
||||
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, scratch.Values, scratch.Visiting)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -50,9 +47,7 @@ public sealed class RuntimeValueEvaluator
|
|||
RuntimeValueEvaluator? cleanEvaluator,
|
||||
ScalarValue? activeMask,
|
||||
ScalarValueCache cache,
|
||||
List<ScalarValue> visiting,
|
||||
CompiledValueCache compiledValues,
|
||||
bool useCompiled)
|
||||
List<ScalarValue> visiting)
|
||||
{
|
||||
_cache = cache;
|
||||
_visiting = visiting;
|
||||
|
|
@ -61,9 +56,6 @@ public sealed class RuntimeValueEvaluator
|
|||
_cleanFlatSlots = cleanFlatSlots ?? [];
|
||||
_cleanEvaluator = cleanEvaluator;
|
||||
_activeMask = activeMask;
|
||||
_compiled = useCompiled ? plan.CompiledEvaluator : null;
|
||||
_compiledValues = compiledValues;
|
||||
if (_compiled is not null) _compiledValues.EnsureCapacity(_compiled.Count);
|
||||
}
|
||||
|
||||
public bool Evaluate(ScalarValue value, out uint result)
|
||||
|
|
@ -92,11 +84,6 @@ public sealed class RuntimeValueEvaluator
|
|||
return EvaluateWide(value.Operands[1], out result);
|
||||
}
|
||||
|
||||
if (_compiled is not null && _compiled.TryGetIndex(value, out var compiledIndex))
|
||||
{
|
||||
return EvaluateCompiled(compiledIndex, out result);
|
||||
}
|
||||
|
||||
if (_cache.TryGetValue(value, out result))
|
||||
{
|
||||
return true;
|
||||
|
|
@ -154,11 +141,24 @@ public sealed class RuntimeValueEvaluator
|
|||
case ScalarValueKind.FirstLane:
|
||||
{
|
||||
using var scratch = RuntimeEvaluationScratch.Rent();
|
||||
return new RuntimeValueEvaluator(scratch, _plan, _inputs, _cleanFlatSlots, _cleanEvaluator, value.Operands[1], useCompiled: _compiled is not null)
|
||||
return new RuntimeValueEvaluator(scratch, _plan, _inputs, _cleanFlatSlots, _cleanEvaluator, value.Operands[1])
|
||||
.EvaluateWide(value.Operands[0], out result);
|
||||
}
|
||||
case ScalarValueKind.ResourceTableWord:
|
||||
return EvaluateTableReference((int)value.Payload, out result);
|
||||
{
|
||||
var slot = (int)value.Payload;
|
||||
if (slot >= _plan.TableReads.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot < _cleanFlatSlots.Count && _cleanFlatSlots[slot] != 0 && _cleanEvaluator is not null)
|
||||
{
|
||||
return _cleanEvaluator.EvaluateWide(_plan.TableReads[slot].Value, out result);
|
||||
}
|
||||
|
||||
return EvaluateWide(_plan.TableReads[slot].Value, out result);
|
||||
}
|
||||
case ScalarValueKind.ScalarAddressWord:
|
||||
case ScalarValueKind.ScalarBufferWord:
|
||||
return EvaluateRawRead(value, out result);
|
||||
|
|
@ -215,6 +215,7 @@ public sealed class RuntimeValueEvaluator
|
|||
return false;
|
||||
}
|
||||
|
||||
var baseAddress = ((high << 32) | (uint)low) & AddressMask;
|
||||
ulong records = 0;
|
||||
if (value.Kind == ScalarValueKind.ScalarBufferWord &&
|
||||
(handle.Operands.Length != 4 ||
|
||||
|
|
@ -224,14 +225,7 @@ public sealed class RuntimeValueEvaluator
|
|||
return false;
|
||||
}
|
||||
|
||||
return ReadRawWord(value.Kind, low, high, (uint)offset, records, (int)memory.Offset, out result);
|
||||
}
|
||||
|
||||
internal bool ReadRawWord(ScalarValueKind kind, ulong low, ulong high, uint offset, ulong records, long immediate, out ulong result)
|
||||
{
|
||||
result = 0;
|
||||
var baseAddress = ((high << 32) | (uint)low) & AddressMask;
|
||||
switch (ResolveRawAddress(kind, baseAddress, high, records, immediate, offset, out var address))
|
||||
switch (ResolveRawAddress(value.Kind, baseAddress, high, records, (long)(int)memory.Offset, (uint)offset, out var address))
|
||||
{
|
||||
case RawAddress.Failed:
|
||||
return false;
|
||||
|
|
@ -318,59 +312,9 @@ public sealed class RuntimeValueEvaluator
|
|||
return true;
|
||||
}
|
||||
|
||||
internal bool IsEvaluated(ScalarValue value) =>
|
||||
_compiled is not null && _compiled.TryGetIndex(value, out var index)
|
||||
? _compiledValues.Contains(index) : _cache.TryGetValue(value, out _);
|
||||
internal bool IsEvaluated(ScalarValue value) => _cache.TryGetValue(value, out _);
|
||||
|
||||
internal void Seed(ScalarValue value, ulong result)
|
||||
{
|
||||
if (_compiled is not null && _compiled.TryGetIndex(value, out var index)) _compiledValues.Store(index, result);
|
||||
else _cache[value] = result;
|
||||
}
|
||||
|
||||
internal int BeginCompiled(int index, out ulong result) => _compiledValues.Begin(index, out result);
|
||||
internal void StoreCompiled(int index, ulong result) => _compiledValues.Store(index, result);
|
||||
internal void EndCompiled(int index) => _compiledValues.End(index);
|
||||
internal bool EvaluateCompiled(int index, out ulong result) => _compiled!.Evaluate(this, index, out result);
|
||||
internal bool EvaluateCompiledSpecial(int index, out ulong result) => EvaluateNode(_compiled!.Values[index], out result);
|
||||
internal bool IsCompiledActiveMask(int index) => ReferenceEquals(_activeMask, _compiled!.Values[index]);
|
||||
|
||||
internal bool ReadUserData(int index, out ulong result)
|
||||
{
|
||||
result = 0;
|
||||
if ((uint)index >= (uint)_inputs.UserData.Count) return false;
|
||||
result = _inputs.UserData[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
internal ulong ReadShaderBase() => _inputs.ShaderBase;
|
||||
|
||||
private bool EvaluateTableReference(int slot, out ulong result)
|
||||
{
|
||||
result = 0;
|
||||
if ((uint)slot >= (uint)_plan.TableReads.Count) return false;
|
||||
var selected = slot < _cleanFlatSlots.Count && _cleanFlatSlots[slot] != 0 && _cleanEvaluator is not null ? _cleanEvaluator : this;
|
||||
return selected._compiled is { } compiled
|
||||
? selected.EvaluateCompiled(compiled.TableWords[slot], out result)
|
||||
: selected.EvaluateWide(_plan.TableReads[slot].Value, out result);
|
||||
}
|
||||
|
||||
private bool EvaluateSourceWord(int source, int word, out uint result)
|
||||
{
|
||||
var value = _plan.DescriptorSources[source].Dwords[word];
|
||||
if (_compiled is null || value.IsConstant) return Evaluate(value, out result);
|
||||
var succeeded = EvaluateCompiled(_compiled.SourceWords[source][word], out var wide);
|
||||
result = (uint)wide;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
private bool EvaluateTableRead(int index, out uint result)
|
||||
{
|
||||
if (_compiled is null) return Evaluate(_plan.TableReads[index].Value, out result);
|
||||
var succeeded = EvaluateCompiled(_compiled.TableWords[index], out var wide);
|
||||
result = (uint)wide;
|
||||
return succeeded;
|
||||
}
|
||||
internal void Seed(ScalarValue value, ulong result) => _cache[value] = result;
|
||||
|
||||
internal ResourceRuntimeInputs Inputs => _inputs;
|
||||
|
||||
|
|
@ -446,7 +390,7 @@ public sealed class RuntimeValueEvaluator
|
|||
var cleanEvaluator = new RuntimeValueEvaluator(cleanScratch, plan, inputs.WithReader(inputs.ReadCleanMemory));
|
||||
var evaluator = new RuntimeValueEvaluator(scratch, plan, inputs, cleanFlatSlots, cleanEvaluator);
|
||||
if (evaluateTable && plan.ResourceBranches.Count != 0)
|
||||
activeSources = EvaluateActiveSources(plan, inputs, cleanEvaluator, cleanScratch);
|
||||
activeSources = EvaluateActiveSources(plan, inputs, cleanEvaluator);
|
||||
var evaluated = new List<DescriptorWords>(sources.Count);
|
||||
RawReadPrefetch.PrefetchSources(plan, sources, activeSources, evaluator);
|
||||
foreach (var sourceIndex in sources)
|
||||
|
|
@ -462,7 +406,7 @@ public sealed class RuntimeValueEvaluator
|
|||
{
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
if (!evaluator.EvaluateSourceWord((int)sourceIndex, index, out words[index]))
|
||||
if (!evaluator.Evaluate(source.Dwords[index], out words[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -480,12 +424,11 @@ public sealed class RuntimeValueEvaluator
|
|||
try
|
||||
{
|
||||
RawReadPrefetch.PrefetchTable(plan, evaluator, cleanEvaluator, cleanFlatSlots);
|
||||
for (var index = 0; index < plan.TableReads.Count; index++)
|
||||
foreach (var read in plan.TableReads)
|
||||
{
|
||||
var read = plan.TableReads[index];
|
||||
var clean = read.FlatOffset < cleanFlatSlots.Count && cleanFlatSlots[(int)read.FlatOffset] != 0;
|
||||
var selected = clean ? cleanEvaluator : evaluator;
|
||||
if (read.FlatOffset >= plan.TableReads.Count || !selected.EvaluateTableRead(index, out var word))
|
||||
if (read.FlatOffset >= plan.TableReads.Count || !selected.Evaluate(read.Value, out var word))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -504,16 +447,15 @@ public sealed class RuntimeValueEvaluator
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool[] EvaluateActiveSources(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, RuntimeValueEvaluator cleanEvaluator,
|
||||
RuntimeEvaluationScratch scratch)
|
||||
private static bool[] EvaluateActiveSources(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, RuntimeValueEvaluator cleanEvaluator)
|
||||
{
|
||||
var activeSources = new bool[plan.DescriptorSources.Count];
|
||||
Array.Fill(activeSources, true);
|
||||
foreach (var block in plan.ResourceBranches)
|
||||
foreach (var source in block.Sources) activeSources[source] = false;
|
||||
|
||||
var visited = scratch.PrepareBranches(plan.ResourceBranches.Count);
|
||||
var pending = scratch.PendingBranches;
|
||||
var visited = new bool[plan.ResourceBranches.Count];
|
||||
var pending = new Stack<int>();
|
||||
pending.Push(0);
|
||||
while (pending.TryPop(out var blockIndex))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ namespace SharpEmu.ShaderCompiler.Resources;
|
|||
// values use the low dword, F32 values are the bits of the low dword.
|
||||
public static class ScalarOperationSemantics
|
||||
{
|
||||
internal static bool TryEvaluateFixed(ScalarOperation operation, ulong first, ulong second, ulong third, ulong fourth, out ulong result) =>
|
||||
TryEvaluate(operation, [first, second, third, fourth], out result);
|
||||
|
||||
public static bool TryEvaluate(ScalarOperation operation, ReadOnlySpan<ulong> operands, out ulong result)
|
||||
{
|
||||
result = 0;
|
||||
|
|
|
|||
|
|
@ -43,40 +43,6 @@ public sealed class ShaderResourcePlan
|
|||
public ShaderResourceInfo Info { get; private set; } = new();
|
||||
|
||||
internal RawReadPrefetchPlan? RawReadPrefetch { get; set; }
|
||||
private readonly object _compileGate = new();
|
||||
private CompiledResourceEvaluator? _compiledEvaluator;
|
||||
private int _compiledEvaluatorState;
|
||||
|
||||
internal CompiledResourceEvaluator? CompiledEvaluator
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Volatile.Read(ref _compiledEvaluatorState) == 2) return _compiledEvaluator;
|
||||
if (Interlocked.CompareExchange(ref _compiledEvaluatorState, 1, 0) == 0) CompiledResourceEvaluator.Enqueue(this);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal CompiledResourceEvaluator? CompileEvaluatorNow()
|
||||
{
|
||||
lock (_compileGate)
|
||||
{
|
||||
if (Volatile.Read(ref _compiledEvaluatorState) == 2) return _compiledEvaluator;
|
||||
CompiledResourceEvaluator? compiled;
|
||||
try
|
||||
{
|
||||
compiled = CompiledResourceEvaluator.Build(this);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OutOfMemoryException)
|
||||
{
|
||||
compiled = null;
|
||||
}
|
||||
|
||||
_compiledEvaluator = compiled;
|
||||
Volatile.Write(ref _compiledEvaluatorState, 2);
|
||||
return compiled;
|
||||
}
|
||||
}
|
||||
|
||||
// The handles each memory access reads, after flattened reads were replaced.
|
||||
public MemoryAccessBinding?[] Accesses { get; private set; } = [];
|
||||
|
|
|
|||
|
|
@ -171,7 +171,6 @@ 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; } = [];
|
||||
|
|
@ -187,7 +186,6 @@ public sealed class ShaderCompileRequest
|
|||
public int RequiredVertexOutputCount { get; init; }
|
||||
public IReadOnlyList<ShaderVertexInput> VertexInputs { get; init; } = [];
|
||||
public uint PositionExportControl { get; init; }
|
||||
public bool SupportsClipDistance { get; init; } = true;
|
||||
public ShaderClipSpaceTransform ClipSpace { get; init; }
|
||||
|
||||
// The LDS the dispatch allocates (COMPUTE_PGM_RSRC2.LDS_SIZE), 0 when unknown.
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.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,12 +42,9 @@ public sealed class AudioOut2PortGetStateExportsTests
|
|||
// SceAudioOut2PortState: output@0, numChannels@2, volume@4.
|
||||
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
|
||||
Assert.Equal(2, state[2]);
|
||||
Assert.Equal(0, state[3]);
|
||||
Assert.Equal(127, BinaryPrimitives.ReadInt16LittleEndian(state[4..]));
|
||||
Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(state[6..]));
|
||||
Assert.Equal(-1, BinaryPrimitives.ReadInt16LittleEndian(state[4..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[8..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[0x0C..]));
|
||||
Assert.All(state[0x10..0x40].ToArray(), value => Assert.Equal(0, value));
|
||||
// sizeof(SceAudioOut2PortState) is 0x40; nothing past it may be touched.
|
||||
Assert.Equal(0xAB, state[0x40]);
|
||||
Assert.Equal(0xAB, state[0x7F]);
|
||||
}
|
||||
|
|
@ -105,19 +102,15 @@ 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(0, info[1]);
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[2..]));
|
||||
Assert.Equal(0x3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
|
||||
Assert.Equal(3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[8..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[0x0C..]));
|
||||
// aSpeakerAngle[0..1] = { -30, 0 }, { +30, 0 } degrees.
|
||||
Assert.Equal(-30, BinaryPrimitives.ReadInt16LittleEndian(info[0x10..]));
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x12..]));
|
||||
Assert.Equal(30, BinaryPrimitives.ReadInt16LittleEndian(info[0x14..]));
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x16..]));
|
||||
Assert.All(info[0x18..0x50].ToArray(), value => Assert.Equal(0, value));
|
||||
// sizeof(SceAudioOut2SpeakerInfo) is 0x50.
|
||||
Assert.Equal(0xCD, info[0x50]);
|
||||
Assert.Equal(0xCD, info[0x7F]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// 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)));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue