Compare commits

...

5 Commits

Author SHA1 Message Date
ParantezTech 6bb75a4049 [SharpEmu] Add bundled demo game 2026-10-09 17:22:05 +03:00
ParantezTech 0af8dce081 [GUI] improve library tile collection and acceleration fixes 2026-10-09 14:55:12 +03:00
qwe1qwe 289f090b50
[GPU] Align linear color target pitch like linear textures (#1063) 2026-10-09 14:07:12 +03:00
Berk 47d4c07a8d
chore: bump version to 0.0.5-nexus-release.2 (#1070) 2026-10-09 01:53:47 +03:00
Berk 1811cef944
Ports another set of compatibility and GPU fixes from PR #1016 (#1069)
* [Cpu] Prefer LLE for libc FILE-object exports

* [Libc] Add file-lock compatibility exports

* [Kernel] Expose the process argument count and vector

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [AppContent] Report missing add-on content entitlements

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Videodec2] Decode guest access units into pitched NV12 frames

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Network] Add the RUDP context lifecycle

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [SharePlay] Report inactive share-play state

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Np] Complete offline authentication requests as signed out

* [Kernel] Warn when APR cannot resolve a resource path

* [GPU] Skip piece hashing for stencil association proxies

* [VideoOut] Add deduplicated volume texture binding traces

* [SpirvTranslator] Match guest F32 denormal and trigonometric semantics

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Network] Add socket epoll registration and readiness waits

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Network] Close POSIX socket descriptors through sceNetSocketClose

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Network] Add Ethernet address string conversion

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Json] Track the global special-float handler

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Json] Add the special-float format setter

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Pad] Add the n3kSX62fgNo compatibility export

* [Pad] Trace input state changes when requested

* [PlayGo] Keep unknown chunk lists empty

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [PlayGo] Keep chunk metadata parsing in the metadata loader

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [PlayGo] Expose runtime state reset

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [PlayGo] Add install and optional chunk queries

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Loader] Preserve switch targets while patching guest red-zone accesses

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Loader] Report patching progress for large executables

* [SpirvTranslator] Preserve F32 signed zero, infinity and NaN when supported

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [GPU] Use Gen5 render-target tile equations on the CPU and GPU

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [GPU] Decode DCC fixed and register clear values

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [GPU] Track DCC metadata coverage for color targets and volume textures

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [GPU] Materialize DCC slice clears from guest metadata

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Tests] Cover Gen5 tiling, DCC clears and offline authentication

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Tests] Match red-zone coverage to forward-only patching

* [Pad] Track independent standard, special and remote port handles

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [UserService] Add user number and termination exports

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [NP] Expose the game trials flag

* [Kernel] Allow renaming and unlinking open guest files

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Tests] Cover controller ports, user exports, trials and open-file renames

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Clear the outputs and report no space when no direct memory span is free

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Return POSIX results from _open and keep sceKernelOpen raw

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Open /dev/random and /dev/urandom as host RNG descriptors

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Read random device descriptors from the host RNG

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Close random device descriptors

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Report random device descriptors in fstat

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Cpu] Place the guest exception mcontext at the start of the context

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Cpu] Report the flags and Sony stack alias in guest exception contexts

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Cpu] Expose the guest exception context writer to tests

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [NP] Keep Universal Data System handles across repeated initialization

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [NP] Create Universal Data System contexts before explicit initialization

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [NP] Reset Universal Data System state with the runtime

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Pad] Write the device class extended information at its guest size

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Pad] Add scePadDeviceClassParseData

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Tests] Cover random devices, POSIX open, UDS initialization, pad class data and exception contexts

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [SaveData] Write the mount info structure at its 48-byte guest size

* [Stubs] Add sceTextToSpeech2Cancel and sceTextToSpeech2GetSpeechStatus

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Kernel] Add sceKernelSync

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>

* [Loader] Patch red-zone sites from the previous instruction when every indirect branch is resolved

* [Tests] Cover backward red-zone spans, direct memory no-space results and the mount info size

---------

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-09 01:44:45 +03:00
79 changed files with 4392 additions and 335 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
[Aa]rtifacts/
logs/
.tmp/
src/SharpEmu.Demo/
x86/
x64/

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
<SharpEmuVersion>0.0.5-nexus-release.2</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>

View File

@ -180,4 +180,71 @@ SPDX-License-Identifier: GPL-2.0-or-later
SkipUnchangedFiles="true" />
</Target>
<PropertyGroup>
<SharpEmuDemoTag>e3a6dd3</SharpEmuDemoTag>
<SharpEmuDemoFolderName>demo</SharpEmuDemoFolderName>
<SharpEmuDemoCacheDir>$(BaseIntermediateOutputPath)sharpemu-demo/$(SharpEmuDemoTag)</SharpEmuDemoCacheDir>
<SharpEmuDemoArchive>$(SharpEmuDemoCacheDir)/sharpemu-demo.zip</SharpEmuDemoArchive>
<SharpEmuDemoDir>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '..', 'SharpEmu.Demo'))</SharpEmuDemoDir>
<SharpEmuDemoStamp>$(SharpEmuDemoDir).tag</SharpEmuDemoStamp>
</PropertyGroup>
<Target Name="FetchSharpEmuDemo" BeforeTargets="Publish;Build">
<ReadLinesFromFile File="$(SharpEmuDemoStamp)" Condition="Exists('$(SharpEmuDemoStamp)')">
<Output TaskParameter="Lines" PropertyName="_SharpEmuDemoExtractedTag" />
</ReadLinesFromFile>
<PropertyGroup>
<_SharpEmuDemoCurrent Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoExtractedTag)' == '$(SharpEmuDemoTag)'">true</_SharpEmuDemoCurrent>
</PropertyGroup>
<DownloadFile
SourceUrl="https://github.com/sharpemu/sharpemu-demo/releases/download/$(SharpEmuDemoTag)/sharpemu-demo.zip"
DestinationFolder="$(SharpEmuDemoCacheDir)"
Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoCurrent)' != 'true' And !Exists('$(SharpEmuDemoArchive)')"
ContinueOnError="WarnAndContinue" />
<RemoveDir Directories="$(SharpEmuDemoDir)" Condition="'$(_SharpEmuDemoCurrent)' != 'true'" />
<Unzip
SourceFiles="$(SharpEmuDemoArchive)"
DestinationFolder="$(SharpEmuDemoDir)"
Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoCurrent)' != 'true' And Exists('$(SharpEmuDemoArchive)')"
ContinueOnError="WarnAndContinue" />
<PropertyGroup>
<SharpEmuDemoComplete Condition="Exists('$(SharpEmuDemoDir)eboot.bin') And Exists('$(SharpEmuDemoDir)sce_sys/param.json') And Exists('$(SharpEmuDemoDir)sce_sys/icon0.png') And Exists('$(SharpEmuDemoDir)sce_sys/pic0.png')">true</SharpEmuDemoComplete>
</PropertyGroup>
<WriteLinesToFile
File="$(SharpEmuDemoStamp)"
Lines="$(SharpEmuDemoTag)"
Overwrite="true"
Condition="'$(SharpEmuDemoComplete)' == 'true' And '$(_SharpEmuDemoCurrent)' != 'true'" />
<RemoveDir Directories="$(SharpEmuDemoDir)" Condition="'$(SharpEmuDemoComplete)' != 'true'" />
<Delete Files="$(SharpEmuDemoArchive)" Condition="'$(SharpEmuDemoComplete)' != 'true' And Exists('$(SharpEmuDemoArchive)')" />
</Target>
<Target Name="BuildSharpEmuDemo" AfterTargets="Build" DependsOnTargets="FetchSharpEmuDemo">
<ItemGroup>
<_SharpEmuDemoFiles Include="$(SharpEmuDemoDir)**/*" Exclude="$(SharpEmuDemoStamp)" Condition="'$(SharpEmuDemoComplete)' == 'true'" />
</ItemGroup>
<RemoveDir
Directories="$(OutDir)$(SharpEmuDemoFolderName)"
Condition="'$(SharpEmuDemoComplete)' != 'true'"
ContinueOnError="WarnAndContinue" />
<Copy
SourceFiles="@(_SharpEmuDemoFiles)"
DestinationFiles="@(_SharpEmuDemoFiles->'$(OutDir)$(SharpEmuDemoFolderName)/%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
</Target>
<Target Name="PublishSharpEmuDemo" AfterTargets="Publish" DependsOnTargets="FetchSharpEmuDemo">
<ItemGroup>
<_SharpEmuDemoFiles Include="$(SharpEmuDemoDir)**/*" Exclude="$(SharpEmuDemoStamp)" Condition="'$(SharpEmuDemoComplete)' == 'true'" />
</ItemGroup>
<RemoveDir
Directories="$(PublishDir)$(SharpEmuDemoFolderName)"
Condition="'$(SharpEmuDemoComplete)' != 'true'"
ContinueOnError="WarnAndContinue" />
<Copy
SourceFiles="@(_SharpEmuDemoFiles)"
DestinationFiles="@(_SharpEmuDemoFiles->'$(PublishDir)$(SharpEmuDemoFolderName)/%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@ -1925,6 +1925,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
return HasUsableLleLibcExport("tcVi5SivF7Q", "sprintf");
}
if (IsLibcFileObjectExport(exportName))
{
return HasUsableLleLibcExport(ComputePsNid(exportName), exportName);
}
if (string.Equals(value, "0", StringComparison.Ordinal))
{
return true;
@ -5371,7 +5375,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true;
}
private static bool TryWriteGuestExceptionContext(
internal static bool TryWriteGuestExceptionContext(
CpuContext context,
ulong address,
GuestCpuContinuation continuation,
@ -5382,11 +5386,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
// Orbis ucontext_t has a 0x10-byte signal mask and 0x30 bytes of
// private fields before its amd64 mcontext. These offsets match the
// platform ABI used by libScePs5Util and Unity's Boehm GC. Supplying a
// bare mcontext here makes the collector miss live register roots.
const int mcontext = 0x40;
const int mcontext = 0;
Write64(mcontext + 0x08, hasContinuation ? continuation.Rdi : context[CpuRegister.Rdi]);
Write64(mcontext + 0x10, hasContinuation ? continuation.Rsi : context[CpuRegister.Rsi]);
Write64(mcontext + 0x18, hasContinuation ? continuation.Rdx : context[CpuRegister.Rdx]);
@ -5405,9 +5405,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
var rip = hasContinuation ? continuation.Rip : context.Rip;
var rsp = hasContinuation ? continuation.Rsp : context[CpuRegister.Rsp];
Write64(mcontext + 0xA0, rip);
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : context.Rflags);
Write64(mcontext + 0xB8, rsp);
Write64(mcontext + 0xC8, 0x480); // sizeof(Orbis mcontext_t)
Write64(mcontext + 0xF8, rsp);
Write64(mcontext + 0x440, hasContinuation ? continuation.FsBase : context.FsBase);
Write64(mcontext + 0x448, hasContinuation ? continuation.GsBase : context.GsBase);
return context.Memory.TryWrite(address, bytes);

View File

@ -19,6 +19,44 @@ internal static class GuestRedZonePatcher
private const ulong PageSize = 0x1000;
private const ulong AllocationAlignment = 0x10000;
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
private const int MaximumJumpTableEntries = 4096;
private const int MaximumJumpTableSetupInstructions = 6;
private const ulong LargeExecutableBytes = 32UL << 20;
private static readonly TimeSpan ProgressInterval = TimeSpan.FromSeconds(2);
private sealed class Progress(string hostName, string phase, int total)
{
private readonly long _started = System.Diagnostics.Stopwatch.GetTimestamp();
private long _lastReport = System.Diagnostics.Stopwatch.GetTimestamp();
public void Report(int done)
{
var now = System.Diagnostics.Stopwatch.GetTimestamp();
if (System.Diagnostics.Stopwatch.GetElapsedTime(_lastReport, now) < ProgressInterval)
{
return;
}
_lastReport = now;
Console.Error.WriteLine(
$"[LOADER] {hostName} red-zone {phase}: {done}/{total} ({(total == 0 ? 100 : done * 100L / total)}%) " +
$"after {System.Diagnostics.Stopwatch.GetElapsedTime(_started, now).TotalSeconds:F0} s");
}
}
private static ulong ExecutableBytes(IReadOnlyList<ProgramHeader> programHeaders)
{
var bytes = 0UL;
foreach (var header in programHeaders)
{
if (header.HeaderType == ProgramHeaderType.Load && (header.Flags & ProgramHeaderFlags.Execute) != 0)
{
bytes += header.FileSize;
}
}
return bytes;
}
internal enum SpanRefusal
{
@ -27,6 +65,7 @@ internal static class GuestRedZonePatcher
BranchTargetAfter,
StackAfter,
TooShort,
IndirectBranch,
}
public static PatchResult Patch(
@ -62,8 +101,19 @@ internal static class GuestRedZonePatcher
return default;
}
var executableBytes = ExecutableBytes(programHeaders);
var large = executableBytes >= LargeExecutableBytes;
if (large)
{
Console.Error.WriteLine(
$"[LOADER] {hostName} red-zone patch: scanning {functionStarts.Length} functions in {executableBytes >> 20} MB of code; " +
"a large executable takes a while before the game window opens.");
}
var sites = CollectPatchSites(
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan,
large ? new Progress(hostName, "scan", functionStarts.Length) : null);
sites.Sort(static (left, right) => left.Address.CompareTo(right.Address));
if (sites.Count == 0)
{
Console.Error.WriteLine(
@ -94,8 +144,10 @@ internal static class GuestRedZonePatcher
var patched = 0;
var failed = 0;
var previousEnd = 0UL;
var patchProgress = large ? new Progress(hostName, "patch", sites.Count) : null;
foreach (var site in sites)
{
patchProgress?.Report(patched + failed);
// Sites are produced in increasing address order. Patching two that
// overlap would write one jump over another and send the trampoline
// into whatever follows, so refuse rather than corrupt guest code.
@ -128,6 +180,7 @@ internal static class GuestRedZonePatcher
BranchTargetRefusals = scan.BranchTargetRefusals,
StackRefusals = scan.StackRefusals,
TooShortRefusals = scan.TooShortRefusals,
IndirectBranchRefusals = scan.IndirectBranchRefusals,
TrampolineBytes = trampolineCursor - trampolineBase,
};
Console.Error.WriteLine(
@ -135,7 +188,8 @@ internal static class GuestRedZonePatcher
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
$"unrelocatable={result.UnrelocatableSites} " +
$"(control_flow={result.ControlFlowRefusals} branch_target_after={result.BranchTargetRefusals} " +
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals} " +
$"indirect_branch={result.IndirectBranchRefusals}) " +
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
return result;
@ -149,15 +203,17 @@ internal static class GuestRedZonePatcher
bool protectRedZone,
bool splitVectorStores,
bool rewriteSha,
out PatchResult result)
out PatchResult result,
Progress? progress = null)
{
var sites = new List<PatchSite>();
var scannedFunctions = 0;
var functionCount = 0;
var redZoneFunctionCount = 0;
var instructionCount = 0;
var vectorStoreCount = 0;
var unrelocatableSites = 0;
var refusalCounts = new int[5];
var refusalCounts = new int[6];
var shaInstructionCount = 0;
foreach (var header in programHeaders)
@ -190,6 +246,7 @@ internal static class GuestRedZonePatcher
for (var functionIndex = 0; functionIndex < segmentFunctions.Length; functionIndex++)
{
progress?.Report(scannedFunctions++);
var functionStart = segmentFunctions[functionIndex];
var functionEnd = functionIndex + 1 < segmentFunctions.Length
? segmentFunctions[functionIndex + 1]
@ -222,6 +279,14 @@ internal static class GuestRedZonePatcher
redZoneFunctionCount++;
}
var branchTargets = CollectBranchTargets(decoded);
var canBorrowFollowingInstructions = TryCollectJumpTableTargets(
memory,
programHeaders,
imageBase,
decoded,
functionStart,
functionEnd,
branchTargets);
var lastSiteEnd = 0UL;
@ -236,27 +301,32 @@ internal static class GuestRedZonePatcher
continue;
}
// A backward span reaches instructions that an earlier site
// may already have replaced with its own jump. Two jumps
// written over each other corrupt control flow, so a span is
// only usable when it starts at or after the end of the last
// one. Forward spans cannot overlap by construction; the
// check costs nothing and documents the invariant.
var backward = false;
if (!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span, out var refusal))
var span = default(PatchSite);
var refusal = SpanRefusal.TooShort;
var forward = TryBuildPatchSpan(
decoded,
instructionIndex,
branchTargets,
canBorrowFollowingInstructions,
out span,
out refusal);
var backward = !forward &&
canBorrowFollowingInstructions &&
TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
if (!forward && !backward)
{
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
}
if ((!backward && refusal != SpanRefusal.None) || span.Address < lastSiteEnd)
{
// Refused spans never enter the site list, so they are
// invisible in FailedSites. Count them separately.
unrelocatableSites++;
refusalCounts[(int)refusal]++;
continue;
}
if (span.Address < lastSiteEnd)
{
unrelocatableSites++;
refusalCounts[(int)SpanRefusal.TooShort]++;
continue;
}
sites.Add(span);
lastSiteEnd = span.Address + (ulong)span.ByteLength;
if (splitVectorStores)
@ -268,8 +338,6 @@ internal static class GuestRedZonePatcher
shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
}
// A backward span already ends at the current instruction, so
// only a forward one consumes the instructions that follow.
if (!backward)
{
instructionIndex += span.Instructions.Count - 1;
@ -290,6 +358,7 @@ internal static class GuestRedZonePatcher
BranchTargetRefusals = refusalCounts[(int)SpanRefusal.BranchTargetAfter],
StackRefusals = refusalCounts[(int)SpanRefusal.StackAfter],
TooShortRefusals = refusalCounts[(int)SpanRefusal.TooShort],
IndirectBranchRefusals = refusalCounts[(int)SpanRefusal.IndirectBranch],
ShaInstructionCount = shaInstructionCount,
};
return sites;
@ -347,6 +416,321 @@ internal static class GuestRedZonePatcher
return targets;
}
private static bool TryCollectJumpTableTargets(
IVirtualMemory memory,
IReadOnlyList<ProgramHeader> programHeaders,
ulong imageBase,
IReadOnlyList<DecodedInstruction> decoded,
ulong functionStart,
ulong functionEnd,
HashSet<ulong> branchTargets)
{
List<ulong>? resolvedTargets = null;
HashSet<ulong>? instructionBoundaries = null;
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
{
if (decoded[instructionIndex].Instruction.FlowControl != FlowControl.IndirectBranch)
{
continue;
}
instructionBoundaries ??= decoded
.Select(static entry => entry.Instruction.IP)
.ToHashSet();
resolvedTargets ??= new List<ulong>();
if (!TryResolveJumpTable(
memory,
programHeaders,
imageBase,
decoded,
instructionIndex,
functionStart,
functionEnd,
instructionBoundaries,
branchTargets,
resolvedTargets))
{
return false;
}
}
if (resolvedTargets is not null)
{
branchTargets.UnionWith(resolvedTargets);
}
return true;
}
private static bool TryResolveJumpTable(
IVirtualMemory memory,
IReadOnlyList<ProgramHeader> programHeaders,
ulong imageBase,
IReadOnlyList<DecodedInstruction> decoded,
int jumpIndex,
ulong functionStart,
ulong functionEnd,
IReadOnlySet<ulong> instructionBoundaries,
IReadOnlySet<ulong> knownBranchTargets,
List<ulong> targets)
{
if (jumpIndex < 4)
{
return false;
}
var jump = decoded[jumpIndex].Instruction;
var add = decoded[jumpIndex - 1].Instruction;
var load = decoded[jumpIndex - 2].Instruction;
if (jump.OpCount != 1 || jump.GetOpKind(0) != OpKind.Register ||
add.Mnemonic != Mnemonic.Add || add.OpCount != 2 ||
add.GetOpKind(0) != OpKind.Register || add.GetOpKind(1) != OpKind.Register ||
load.Mnemonic != Mnemonic.Movsxd || load.OpCount != 2 ||
load.GetOpKind(0) != OpKind.Register || load.GetOpKind(1) != OpKind.Memory ||
load.MemorySize.GetSize() != sizeof(int) || load.MemoryIndexScale != sizeof(int) ||
load.MemoryBase == Register.None || load.MemoryIndex == Register.None ||
load.MemoryDisplacement64 != 0)
{
return false;
}
var targetRegister = jump.GetOpRegister(0).GetFullRegister();
var tableRegister = add.GetOpRegister(1).GetFullRegister();
var selectorRegister = load.MemoryIndex.GetFullRegister();
if (targetRegister != add.GetOpRegister(0).GetFullRegister() ||
targetRegister != load.GetOpRegister(0).GetFullRegister() ||
tableRegister != load.MemoryBase.GetFullRegister() ||
targetRegister.GetSize() != sizeof(ulong) ||
tableRegister.GetSize() != sizeof(ulong) ||
selectorRegister.GetSize() != sizeof(ulong) ||
tableRegister == selectorRegister)
{
return false;
}
var branchIndex = -1;
for (var index = jumpIndex - 3;
index >= 1 && index >= jumpIndex - MaximumJumpTableSetupInstructions;
index--)
{
var candidate = decoded[index].Instruction;
if (candidate.Mnemonic == Mnemonic.Ja)
{
branchIndex = index;
break;
}
if (candidate.Mnemonic != Mnemonic.Lea)
{
return false;
}
}
if (branchIndex < 1)
{
return false;
}
for (var index = branchIndex + 1; index <= jumpIndex; index++)
{
if (knownBranchTargets.Contains(decoded[index].Instruction.IP))
{
return false;
}
}
var boundsBranch = decoded[branchIndex].Instruction;
var boundsCompare = decoded[branchIndex - 1].Instruction;
if (boundsBranch.FlowControl != FlowControl.ConditionalBranch ||
!TryGetNearBranchTarget(boundsBranch, out var defaultTarget) ||
defaultTarget < functionStart || defaultTarget >= functionEnd ||
boundsCompare.Mnemonic != Mnemonic.Cmp || boundsCompare.OpCount != 2 ||
boundsCompare.GetOpKind(0) != OpKind.Register ||
boundsCompare.GetOpRegister(0).GetFullRegister() != selectorRegister ||
boundsCompare.GetOpRegister(0).GetSize() != sizeof(int) ||
!TryGetNonNegativeImmediate(boundsCompare, 1, out var maximumIndex) ||
maximumIndex >= MaximumJumpTableEntries)
{
return false;
}
var tableAddress = 0UL;
var tableDefinitionFound = false;
for (var index = branchIndex + 1; index <= jumpIndex - 3; index++)
{
var candidate = decoded[index].Instruction;
if (candidate.OpCount < 1 || candidate.GetOpKind(0) != OpKind.Register)
{
return false;
}
var writtenRegister = candidate.GetOpRegister(0).GetFullRegister();
if (writtenRegister == selectorRegister)
{
return false;
}
if (writtenRegister != tableRegister)
{
continue;
}
if (tableDefinitionFound || candidate.OpCount != 2 ||
candidate.GetOpKind(1) != OpKind.Memory || !candidate.IsIPRelativeMemoryOperand)
{
return false;
}
tableAddress = candidate.IPRelativeMemoryAddress;
tableDefinitionFound = true;
}
if (!tableDefinitionFound)
{
return false;
}
var entryCount = checked((int)maximumIndex + 1);
var tableBytes = checked(entryCount * sizeof(int));
if (!IsRangeInReadableLoad(programHeaders, imageBase, tableAddress, (ulong)tableBytes))
{
return false;
}
var entries = GC.AllocateUninitializedArray<byte>(tableBytes);
if (!memory.TryRead(tableAddress, entries))
{
return false;
}
var firstTarget = targets.Count;
for (var index = 0; index < entryCount; index++)
{
var relative = BinaryPrimitives.ReadInt32LittleEndian(entries.AsSpan(index * sizeof(int)));
if (!TryAddSignedOffset(tableAddress, relative, out var target) ||
target < functionStart || target >= functionEnd ||
!instructionBoundaries.Contains(target))
{
targets.RemoveRange(firstTarget, targets.Count - firstTarget);
return false;
}
targets.Add(target);
}
return true;
}
private static bool IsRangeInReadableLoad(
IReadOnlyList<ProgramHeader> programHeaders,
ulong imageBase,
ulong address,
ulong length)
{
foreach (var header in programHeaders)
{
if (header.HeaderType != ProgramHeaderType.Load ||
(header.Flags & ProgramHeaderFlags.Read) == 0 ||
header.FileSize == 0 ||
header.VirtualAddress > ulong.MaxValue - imageBase)
{
continue;
}
var start = imageBase + header.VirtualAddress;
if (address < start || address - start > header.FileSize ||
length > header.FileSize - (address - start))
{
continue;
}
return true;
}
return false;
}
private static bool TryAddSignedOffset(ulong address, int offset, out ulong result)
{
if (offset >= 0)
{
var positive = (ulong)offset;
if (positive <= ulong.MaxValue - address)
{
result = address + positive;
return true;
}
}
else
{
var negative = (ulong)-(long)offset;
if (negative <= address)
{
result = address - negative;
return true;
}
}
result = 0;
return false;
}
private static bool TryGetNearBranchTarget(in Instruction instruction, out ulong target)
{
for (var operand = 0; operand < instruction.OpCount; operand++)
{
switch (instruction.GetOpKind(operand))
{
case OpKind.NearBranch16:
target = instruction.NearBranch16;
return true;
case OpKind.NearBranch32:
target = instruction.NearBranch32;
return true;
case OpKind.NearBranch64:
target = instruction.NearBranch64;
return true;
}
}
target = 0;
return false;
}
private static bool TryGetNonNegativeImmediate(in Instruction instruction, int operand, out ulong value)
{
switch (instruction.GetOpKind(operand))
{
case OpKind.Immediate8:
value = instruction.Immediate8;
return true;
case OpKind.Immediate16:
value = instruction.Immediate16;
return true;
case OpKind.Immediate32:
value = instruction.Immediate32;
return true;
case OpKind.Immediate64:
value = instruction.Immediate64;
return true;
case OpKind.Immediate8to16 when instruction.Immediate8to16 >= 0:
value = (ulong)instruction.Immediate8to16;
return true;
case OpKind.Immediate8to32 when instruction.Immediate8to32 >= 0:
value = (ulong)instruction.Immediate8to32;
return true;
case OpKind.Immediate8to64 when instruction.Immediate8to64 >= 0:
value = (ulong)instruction.Immediate8to64;
return true;
case OpKind.Immediate32to64 when instruction.Immediate32to64 >= 0:
value = (ulong)instruction.Immediate32to64;
return true;
default:
value = 0;
return false;
}
}
/// <summary>
/// Runs the enclosing-span search over a raw instruction range, so the
/// behaviour can be pinned on the byte sequences that occur in real guest
@ -376,7 +760,7 @@ internal static class GuestRedZonePatcher
}
if (siteIndex < 0 ||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), true, out var site, out _))
{
return false;
}
@ -417,7 +801,10 @@ internal static class GuestRedZonePatcher
return false;
}
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
var hasIndirectBranch = decoded.Any(static entry =>
entry.Instruction.FlowControl == FlowControl.IndirectBranch);
if (hasIndirectBranch ||
!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
{
return false;
}
@ -551,12 +938,13 @@ internal static class GuestRedZonePatcher
int startIndex,
IReadOnlySet<ulong> branchTargets,
out PatchSite site) =>
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
TryBuildPatchSpan(decoded, startIndex, branchTargets, true, out site, out _);
private static bool TryBuildPatchSpan(
IReadOnlyList<DecodedInstruction> decoded,
int startIndex,
IReadOnlySet<ulong> branchTargets,
bool canBorrowFollowingInstructions,
out PatchSite site,
out SpanRefusal refusal)
{
@ -568,6 +956,7 @@ internal static class GuestRedZonePatcher
{
var instruction = decoded[index].Instruction;
if (index != startIndex &&
canBorrowFollowingInstructions &&
!branchTargets.Contains(instruction.IP) &&
IsRelocatableDirectBranch(instruction))
{
@ -578,7 +967,11 @@ internal static class GuestRedZonePatcher
break;
}
if (instruction.FlowControl != FlowControl.Next)
if (index != startIndex && !canBorrowFollowingInstructions)
{
refusal = SpanRefusal.IndirectBranch;
}
else if (instruction.FlowControl != FlowControl.Next)
{
refusal = SpanRefusal.ControlFlow;
}
@ -1089,6 +1482,8 @@ internal static class GuestRedZonePatcher
public int TooShortRefusals { get; init; }
public int IndirectBranchRefusals { get; init; }
public ulong TrampolineBytes { get; init; }
}

View File

@ -159,6 +159,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
FiberExports.ResetRuntimeState();
Http2Exports.ResetRuntimeState();
NpAuthExports.ResetRuntimeState();
NpUniversalDataSystemExports.ResetRuntimeState();
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);

View File

@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.IO;
namespace SharpEmu.GUI;
internal static class BundledDemo
{
internal const string FolderName = "demo";
private static readonly string[] RequiredFiles =
[
"eboot.bin",
Path.Combine("sce_sys", "param.json"),
Path.Combine("sce_sys", "icon0.png"),
Path.Combine("sce_sys", "pic0.png"),
Path.Combine("sce_sys", "snd0.at9"),
];
internal static string? FindEboot(string baseDirectory)
{
var root = Path.Combine(baseDirectory, FolderName);
foreach (var file in RequiredFiles)
{
if (!File.Exists(Path.Combine(root, file)))
{
return null;
}
}
return Path.GetFullPath(Path.Combine(root, "eboot.bin"));
}
}

View File

@ -15,6 +15,7 @@ namespace SharpEmu.GUI;
public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, INotifyCollectionChanged
{
private readonly ObservableCollection<GameEntry> _games;
private bool _includeAddFolderTile = true;
public LibraryTileCollection(ObservableCollection<GameEntry> games)
{
@ -24,7 +25,22 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public int Count => _games.Count + 1;
public bool IncludeAddFolderTile
{
get => _includeAddFolderTile;
set
{
if (_includeAddFolderTile == value)
{
return;
}
_includeAddFolderTile = value;
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public int Count => _games.Count + (_includeAddFolderTile ? 1 : 0);
bool IList.IsFixedSize => false;
@ -43,7 +59,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
return _games[index];
}
if (index == _games.Count)
if (_includeAddFolderTile && index == _games.Count)
{
return AddFolderTile.Instance;
}
@ -65,7 +81,10 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
yield return game;
}
yield return AddFolderTile.Instance;
if (_includeAddFolderTile)
{
yield return AddFolderTile.Instance;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@ -79,7 +98,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
int IList.IndexOf(object? value) => value switch
{
GameEntry game => _games.IndexOf(game),
AddFolderTile => _games.Count,
AddFolderTile when _includeAddFolderTile => _games.Count,
_ => -1,
};

View File

@ -299,6 +299,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
<Button x:Name="CarouselAddFolderButton"
Classes="ghost"
HorizontalAlignment="Left"
VerticalAlignment="Top"
IsVisible="False"
AutomationProperties.Name="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="+"
FontSize="16"
FontWeight="Light"
VerticalAlignment="Center" />
<TextBlock Text="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Panel>
<!-- Selected-game identity and actions share one detail surface so

View File

@ -178,6 +178,19 @@ public partial class MainWindow : Window
private GameEntry? _lastSelectedGame;
private double _libraryRailRowHeight = 188;
private double _libraryGridRowHeight = 216;
private const double CarouselLeadingPadding = 20;
private const double CarouselItemMargin = 12;
private bool _carouselAnimating;
private readonly TranslateTransform _carouselTransform = new();
private double _carouselOffset;
private double _carouselScrollFrom;
private double _carouselScrollTo;
private long _carouselScrollStartedAt;
private bool _carouselPointerDown;
private bool _carouselDragging;
private Point _carouselDragStart;
private double _carouselDragStartOffset;
private double _carouselWheelAccumulator;
// Bundled key art shown whenever no game-specific backdrop applies; the
// plain window color remains the fallback when the asset fails to load.
@ -246,6 +259,27 @@ public partial class MainWindow : Window
_libraryLayoutTimer.Stop();
UpdateLibraryLayoutMetrics();
};
CarouselAddFolderButton.Click += (_, _) => StartAddFolderFromTile();
GameList.AddHandler(PointerPressedEvent, OnCarouselPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.AddHandler(PointerMovedEvent, OnCarouselPointerMoved, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.AddHandler(PointerReleasedEvent, OnCarouselPointerReleased, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.AddHandler(
PointerCaptureLostEvent,
(_, e) =>
{
if (ReferenceEquals(e.Source, GameList))
{
EndCarouselDrag(select: false);
}
},
RoutingStrategies.Tunnel | RoutingStrategies.Bubble,
handledEventsToo: true);
LibraryPage.AddHandler(PointerWheelChangedEvent, OnCarouselPointerWheel, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.SizeChanged += (_, _) =>
{
UpdateCarouselPadding();
Dispatcher.UIThread.Post(() => AnchorCarouselSelection(animate: false), DispatcherPriority.Loaded);
};
TitleBar.PointerPressed += OnTitleBarPointerPressed;
TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
@ -376,13 +410,7 @@ public partial class MainWindow : Window
Closing += (_, _) => BeginWindowClosing();
Closed += (_, _) => CompleteWindowClosing();
SdlLauncherGamepad.EnsureStarted();
_gamepadTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50),
};
EnvRenderDocToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle(
"SHARPEMU_RENDERDOC",
EnvRenderDocToggle.IsChecked == true);
@ -390,24 +418,6 @@ public partial class MainWindow : Window
SetEnvironmentToggle(
"SHARPEMU_VK_DISABLE_IMPLICITS",
EnvDisableVkImplicitLayersToggle.IsChecked == true);
DefaultProfileBox.TextChanged += (_, _) =>
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
CtxLaunch.Click += (_, _) => LaunchSelected();
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
CtxCopyPath.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
CtxCopyTitleId.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => BeginWindowClosing();
Closed += (_, _) => CompleteWindowClosing();
SdlLauncherGamepad.EnsureStarted();
_gamepadTimer = new DispatcherTimer
@ -492,12 +502,25 @@ public partial class MainWindow : Window
() => SetOptionsNavigationIndicator(_optionsSectionIndex, animate: false),
DispatcherPriority.Loaded);
}
else
{
FocusLibrarySelection();
}
}
private void SetLibraryLayout(bool grid)
{
_isLibraryGridLayout = grid;
var selectedTile = GameList.SelectedItem;
_libraryTiles.IncludeAddFolderTile = grid;
if (selectedTile is GameEntry && !ReferenceEquals(GameList.SelectedItem, selectedTile))
{
GameList.SelectedItem = selectedTile;
}
CarouselAddFolderButton.IsVisible = !grid;
SetClass(GameList, "gridLayout", grid);
UpdateCarouselPadding();
SetClass(LibrarySelectedDetails, "gridLayout", grid);
LibraryPage.Margin = new Thickness(0, 6, 0, 0);
UpdateLibraryLayoutMetrics();
@ -506,7 +529,11 @@ public partial class MainWindow : Window
if (GameList.SelectedItem is { } selected)
{
Dispatcher.UIThread.Post(
() => GameList.ScrollIntoView(selected),
() =>
{
GameList.ScrollIntoView(selected);
AnchorCarouselSelection(animate: false);
},
DispatcherPriority.Loaded);
}
}
@ -518,8 +545,17 @@ public partial class MainWindow : Window
Resources["LibraryTileWidth"] = metrics.ItemWidth;
Resources["LibraryRailItemHeight"] = metrics.RailItemHeight;
Resources["LibraryGridItemHeight"] = metrics.GridItemHeight;
_libraryRailRowHeight = metrics.RailItemHeight + 16;
_libraryRailRowHeight = metrics.RailItemHeight + 16 + 48;
CarouselAddFolderButton.Margin = new Thickness(0, metrics.RailItemHeight + 24, 0, 0);
_libraryGridRowHeight = metrics.GridItemHeight + 16;
UpdateCarouselPadding();
Dispatcher.UIThread.Post(
() =>
{
UpdateCarouselPadding();
AnchorCarouselSelection(animate: false);
},
DispatcherPriority.Loaded);
GameList.MaxHeight = double.PositiveInfinity;
UpdateLibraryMinimumHeight();
@ -850,11 +886,28 @@ public partial class MainWindow : Window
if (ShouldNavigate(left, ref _navLeftNextAt, now))
{
MoveSelection(-1);
ReturnFocusFromCarouselAddFolder();
}
if (ShouldNavigate(right, ref _navRightNextAt, now))
{
MoveSelection(1);
ReturnFocusFromCarouselAddFolder();
}
if (!_isLibraryGridLayout)
{
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
if (ShouldNavigate(down, ref _navDownNextAt, now))
{
CarouselAddFolderButton.Focus(NavigationMethod.Directional);
}
if (ShouldNavigate(up, ref _navUpNextAt, now))
{
ReturnFocusFromCarouselAddFolder();
}
}
if (_isLibraryGridLayout)
@ -877,7 +930,14 @@ public partial class MainWindow : Window
var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & HostGamepadButtons.Cross) != 0)
{
LaunchSelected();
if (!_isLibraryGridLayout && CarouselAddFolderButton.IsFocused)
{
StartAddFolderFromTile();
}
else
{
LaunchSelected();
}
}
_previousPadButtons = pad.Buttons;
@ -940,6 +1000,10 @@ public partial class MainWindow : Window
: Math.Clamp(GameList.SelectedIndex + delta, 0, _libraryTiles.Count - 1);
GameList.SelectedIndex = index;
GameList.ScrollIntoView(index);
if (GameList.IsKeyboardFocusWithin && GameList.ContainerFromIndex(index) is { } container)
{
container.Focus(NavigationMethod.Unspecified);
}
}
private async Task OnOpenedAsync()
@ -953,6 +1017,7 @@ public partial class MainWindow : Window
: $" · UNOFFICIAL {BuildInfo.CommitSha}";
VersionText.Text = display;
Title = $"SharpEmu {display}";
ReportGuiRenderer();
ToolTip.SetTip(VersionText, BuildInfo.Banner);
_settings = GuiSettings.Load();
@ -970,7 +1035,66 @@ public partial class MainWindow : Window
}
SeedLibraryFromCache();
FocusLibrarySelection();
await RescanLibraryAsync();
FocusLibrarySelection();
}
private void ReturnFocusFromCarouselAddFolder()
{
if (CarouselAddFolderButton.IsFocused)
{
FocusLibrarySelection();
}
}
private void ReportGuiRenderer()
{
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
var angle = Process.GetCurrentProcess().Modules
.Cast<ProcessModule>()
.Any(static module => string.Equals(module.ModuleName, "av_libglesv2.dll", StringComparison.OrdinalIgnoreCase));
AppendConsoleLine(
angle ? "[GUI] Renderer: GPU (ANGLE/Direct3D)" : "[GUI] Renderer: software (ANGLE not loaded)",
angle ? DimLineBrush : WarningLineBrush);
}
catch (Exception)
{
}
}
private void FocusLibrarySelection()
{
Dispatcher.UIThread.Post(
() =>
{
if (_activePageIndex != 0 || _isGameSettingsOpen)
{
return;
}
if (GameList.SelectedIndex < 0 && _visibleGames.Count > 0)
{
GameList.SelectedIndex = 0;
}
AnchorCarouselSelection(animate: false);
if (GameList.SelectedIndex >= 0 && GameList.ContainerFromIndex(GameList.SelectedIndex) is { } container)
{
container.Focus(NavigationMethod.Unspecified);
}
else
{
GameList.Focus(NavigationMethod.Unspecified);
}
},
DispatcherPriority.Loaded);
}
private void PopulateLanguageBox()
@ -1884,21 +2008,7 @@ public partial class MainWindow : Window
continue;
}
long size = 0;
try
{
size = new FileInfo(fullPath).Length;
}
catch (IOException exception)
{
Console.Error.WriteLine(
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
}
var (title, titleId, version) = TryReadParamJson(fullPath);
games.Add(new GameEntry(
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
games.Add(CreateGameEntry(fullPath));
}
}
catch (Exception exception)
@ -1909,9 +2019,41 @@ public partial class MainWindow : Window
}
games.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
AddBundledDemo(games, excludedPaths);
return games;
}
private static void AddBundledDemo(List<GameEntry> games, IReadOnlySet<string> excludedPaths)
{
var demo = BundledDemo.FindEboot(AppContext.BaseDirectory);
if (demo is null || excludedPaths.Contains(demo))
{
return;
}
games.RemoveAll(game => string.Equals(game.Path, demo, GameLibraryPath.Comparison));
games.Insert(0, CreateGameEntry(demo));
}
private static GameEntry CreateGameEntry(string fullPath)
{
long size = 0;
try
{
size = new FileInfo(fullPath).Length;
}
catch (IOException exception)
{
Console.Error.WriteLine(
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
}
var (title, titleId, version) = TryReadParamJson(fullPath);
return new GameEntry(
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
FindCoverFor(fullPath), FindBackgroundFor(fullPath));
}
/// <summary>
/// Reads the game title, title id and content version from
/// sce_sys/param.json next to the executable, when present.
@ -2083,6 +2225,195 @@ public partial class MainWindow : Window
_lastSelectedGame = GameList.SelectedItem as GameEntry;
UpdateSelectedGame();
Dispatcher.UIThread.Post(() => AnchorCarouselSelection(animate: true), DispatcherPriority.Loaded);
}
private double CarouselItemStride() =>
(Resources.TryGetValue("LibraryTileWidth", out var width) && width is double tileWidth ? tileWidth : 0) +
CarouselItemMargin;
private void UpdateCarouselPadding()
{
if (GameList.ItemsPanelRoot is not { } panel)
{
return;
}
if (_isLibraryGridLayout)
{
panel.ClearValue(MarginProperty);
if (ReferenceEquals(panel.RenderTransform, _carouselTransform))
{
panel.RenderTransform = null;
}
return;
}
panel.Margin = new Thickness(CarouselLeadingPadding, 0, 0, 0);
if (!ReferenceEquals(panel.RenderTransform, _carouselTransform))
{
panel.RenderTransform = _carouselTransform;
}
SetCarouselOffset(_carouselOffset);
}
private double MaximumCarouselOffset() =>
Math.Max(0, (_visibleGames.Count - 1) * CarouselItemStride());
private void SetCarouselOffset(double offset)
{
_carouselOffset = Math.Clamp(offset, 0, MaximumCarouselOffset());
_carouselTransform.X = -_carouselOffset;
}
private void AnchorCarouselSelection(bool animate)
{
if (_isLibraryGridLayout || GameList.SelectedIndex < 0 || _carouselPointerDown)
{
return;
}
var target = Math.Clamp(GameList.SelectedIndex * CarouselItemStride(), 0, MaximumCarouselOffset());
if (!animate)
{
_carouselAnimating = false;
SetCarouselOffset(target);
return;
}
_carouselScrollFrom = _carouselOffset;
_carouselScrollTo = target;
_carouselScrollStartedAt = Stopwatch.GetTimestamp();
if (!_carouselAnimating)
{
_carouselAnimating = true;
RequestAnimationFrame(OnCarouselAnimationFrame);
}
}
private void OnCarouselPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isLibraryGridLayout || !e.GetCurrentPoint(GameList).Properties.IsLeftButtonPressed)
{
return;
}
_carouselPointerDown = true;
_carouselDragging = false;
_carouselDragStart = e.GetPosition(GameList);
_carouselDragStartOffset = _carouselOffset;
}
private void OnCarouselPointerMoved(object? sender, PointerEventArgs e)
{
if (!_carouselPointerDown)
{
return;
}
var deltaX = e.GetPosition(GameList).X - _carouselDragStart.X;
if (!_carouselDragging)
{
if (Math.Abs(deltaX) < 8)
{
return;
}
_carouselDragging = true;
_carouselAnimating = false;
_carouselDragStartOffset = _carouselOffset;
_carouselDragStart = e.GetPosition(GameList);
deltaX = 0;
e.Pointer.Capture(GameList);
}
SetCarouselOffset(_carouselDragStartOffset - deltaX);
e.Handled = true;
}
private void OnCarouselPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_carouselPointerDown)
{
return;
}
var dragged = _carouselDragging;
EndCarouselDrag(select: true);
if (dragged)
{
e.Pointer.Capture(null);
e.Handled = true;
}
}
private void EndCarouselDrag(bool select)
{
var dragged = _carouselDragging;
_carouselPointerDown = false;
_carouselDragging = false;
if (!dragged || !select || _visibleGames.Count == 0)
{
if (!dragged)
{
AnchorCarouselSelection(animate: true);
}
return;
}
var stride = CarouselItemStride();
var index = stride <= 0 ? 0 : (int)Math.Round(_carouselOffset / stride);
index = Math.Clamp(index, 0, _visibleGames.Count - 1);
if (GameList.SelectedIndex == index)
{
AnchorCarouselSelection(animate: true);
}
else
{
GameList.SelectedIndex = index;
}
}
private void OnCarouselPointerWheel(object? sender, PointerWheelEventArgs e)
{
if (_isLibraryGridLayout || _isGameSettingsOpen || _activePageIndex != 0 || _visibleGames.Count == 0)
{
return;
}
var delta = Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y) ? -e.Delta.X : e.Delta.Y;
_carouselWheelAccumulator += delta;
while (Math.Abs(_carouselWheelAccumulator) >= 1)
{
var step = _carouselWheelAccumulator > 0 ? -1 : 1;
_carouselWheelAccumulator += step;
MoveSelection(step);
}
e.Handled = true;
}
private void OnCarouselAnimationFrame(TimeSpan frameTime)
{
if (!_carouselAnimating || _carouselDragging)
{
_carouselAnimating = false;
return;
}
var progress = Math.Min(1.0, Stopwatch.GetElapsedTime(_carouselScrollStartedAt).TotalMilliseconds / 260.0);
var eased = 1 - Math.Pow(1 - progress, 3);
SetCarouselOffset(_carouselScrollFrom + (_carouselScrollTo - _carouselScrollFrom) * eased);
if (progress >= 1)
{
_carouselAnimating = false;
return;
}
RequestAnimationFrame(OnCarouselAnimationFrame);
}
private void OnGameListDoubleTapped(object? sender, TappedEventArgs e)

View File

@ -9,17 +9,21 @@ Cover-art library item states and motion.
<Style Selector="ListBox.tileGrid">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="4,0,8,0" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="-32,0,-32,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<WrapPanel />
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter>
</Style>
<Style Selector="ListBox.tileGrid.gridLayout">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="4,0,8,0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
@ -30,6 +34,7 @@ Cover-art library item states and motion.
</Setter>
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="Width" Value="{DynamicResource LibraryTileWidth}" />
<Setter Property="Height" Value="{DynamicResource LibraryRailItemHeight}" />
<Setter Property="Padding" Value="4" />

View File

@ -21,6 +21,7 @@ public static class HleDataSymbols
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
private static readonly nint _progNameBufferAddress = Allocate(ProgNameMaxBytes + 1);
private static readonly nint _progNamePointerAddress = Allocate(nint.Size);
private static readonly nint _argumentVectorAddress = Allocate(nint.Size * 2);
private static readonly nint _libcNeedFlagAddress = Allocate(sizeof(uint));
private static readonly nint _libcInternalNeedFlagAddress = Allocate(sizeof(uint));
@ -55,6 +56,12 @@ public static class HleDataSymbols
yield return LibcInternalNeedFlagNid;
}
public static int ProcessArgumentCount =>
_progNameBufferAddress != 0 && _argumentVectorAddress != 0 ? 1 : 0;
public static ulong ProcessArgumentVectorAddress =>
unchecked((ulong)_argumentVectorAddress);
public static void ConfigureProcessImageName(string? processImageName)
{
var effectiveName = string.IsNullOrWhiteSpace(processImageName)
@ -65,7 +72,9 @@ public static class HleDataSymbols
lock (_gate)
{
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
if (_progNameBufferAddress == 0 ||
_progNamePointerAddress == 0 ||
_argumentVectorAddress == 0)
{
return;
}
@ -77,6 +86,8 @@ public static class HleDataSymbols
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
}
}

View File

@ -12,6 +12,9 @@ namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
private const ulong FallbackAvailableSpaceKb = 1024UL * 1024UL; // 1 GiB
private const int AppContentErrorParameter = unchecked((int)0x80D90002);
private const int AppContentErrorDrmNoEntitlement = unchecked((int)0x80D90007);
private const int MountPointSize = 16;
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
private const uint AppParamSkuFlag = 0;
@ -99,6 +102,29 @@ public static class AppContentExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "VANhIWcqYak",
ExportName = "sceAppContentAddcontMount",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentAddcontMount(CpuContext ctx)
{
var entitlementLabelAddress = ctx[CpuRegister.Rsi];
var mountPointAddress = ctx[CpuRegister.Rdx];
if (entitlementLabelAddress == 0 || mountPointAddress == 0)
{
return ctx.SetReturn(AppContentErrorParameter);
}
Span<byte> emptyMountPoint = stackalloc byte[MountPointSize];
if (!ctx.Memory.TryWrite(mountPointAddress, emptyMountPoint))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(AppContentErrorDrmNoEntitlement);
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",

View File

@ -7,6 +7,21 @@ using SharpEmu.Libs.VideoOut;
namespace SharpEmu.Libs.Codec;
internal sealed record Videodec2DecodedFrame(
byte[] Nv12,
uint Width,
uint Height,
uint Pitch,
bool ErrorFrame)
{
public ulong Pts { get; init; } = ulong.MaxValue;
public ulong Dts { get; init; } = ulong.MaxValue;
public ulong AttachedData { get; init; }
public bool KeyFrame { get; init; }
public uint Profile { get; init; }
public uint Level { get; init; }
}
/// <summary>
/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder
/// handle, feeding it pre-demuxed Annex-B access units from guest memory.
@ -29,6 +44,8 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
// Enough lookahead to absorb decode jitter without adding visible latency.
private const int FrameQueueCapacity = 4;
private const int WorkQueueCapacity = 64;
// Fallback when the stream doesn't declare a usable framerate.
private const double FallbackFps = 30.0;
@ -45,12 +62,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
private bool _disposed;
// Unbounded: access units are small, backpressure lives on the frame queue below.
private readonly Channel<byte[]?> _workChannel =
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
Channel.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = true,
SingleWriter = false,
});
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
@ -74,6 +91,10 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
private uint _lastWidth;
private uint _lastHeight;
private long _workerInputCount;
private long _workerFrameCount;
private static readonly bool TraceEnabled = IsTraceEnabled();
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
{
_codecContext = codecContext;
@ -106,18 +127,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
if (codec == null)
{
Trace("create failed: H.264 decoder not found");
return null;
}
codecContext = ffmpeg.avcodec_alloc_context3(codec);
if (codecContext == null)
{
Trace("create failed: avcodec_alloc_context3 returned null");
return null;
}
codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE;
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
{
ffmpeg.avcodec_free_context(&codecContext);
Trace("create failed: avcodec_open2 rejected H.264 decoder");
return null;
}
@ -136,14 +161,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
}
ffmpeg.avcodec_free_context(&codecContext);
Trace("create failed: frame or packet allocation returned null");
return null;
}
return new Videodec2Decoder(codecContext, frame, packet);
var decoder = new Videodec2Decoder(codecContext, frame, packet);
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
return decoder;
}
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
{
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
Trace($"create failed: {ex.GetType().Name}: {ex.Message}");
if (codecContext != null)
{
ffmpeg.avcodec_free_context(&codecContext);
@ -174,18 +203,252 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
}
}
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
public void EnqueueAccessUnit(byte[] accessUnit)
public bool EnqueueAccessUnit(byte[] accessUnit)
{
_workChannel.Writer.TryWrite(accessUnit);
return _workChannel.Writer.TryWrite(accessUnit);
}
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
public void RequestDrain()
public bool RequestDrain()
{
_workChannel.Writer.TryWrite(null);
return _workChannel.Writer.TryWrite(null);
}
public bool DecodeAccessUnit(
byte[] accessUnit,
ulong pts,
ulong dts,
ulong attachedData,
out Videodec2DecodedFrame? decodedFrame)
{
decodedFrame = null;
lock (_gate)
{
if (_disposed || accessUnit.Length == 0)
{
return false;
}
ffmpeg.av_packet_unref(_packet);
ffmpeg.av_frame_unref(_frame);
var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length);
if (packetResult < 0)
{
Trace($"decode failed: av_new_packet={packetResult}");
return false;
}
try
{
fixed (byte* source = accessUnit)
{
Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length);
}
_packet->pts = ToAvTimestamp(pts);
_packet->dts = ToAvTimestamp(dts);
var havePendingFrame = false;
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
if (pendingResult < 0)
{
Trace($"decode failed: receive-before-resend={pendingResult}");
return false;
}
havePendingFrame = true;
sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
}
if (sendResult < 0)
{
Trace($"decode failed: avcodec_send_packet={sendResult}");
return false;
}
if (!havePendingFrame)
{
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
receiveResult == ffmpeg.AVERROR_EOF)
{
return true;
}
if (receiveResult < 0)
{
Trace($"decode failed: avcodec_receive_frame={receiveResult}");
return false;
}
}
decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData);
return decodedFrame is not null;
}
finally
{
ffmpeg.av_frame_unref(_frame);
ffmpeg.av_packet_unref(_packet);
}
}
}
public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame)
{
decodedFrame = null;
lock (_gate)
{
if (_disposed)
{
return false;
}
ffmpeg.av_frame_unref(_frame);
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
receiveResult == ffmpeg.AVERROR_EOF)
{
return true;
}
if (receiveResult < 0)
{
Trace($"flush failed: avcodec_receive_frame={receiveResult}");
return false;
}
try
{
decodedFrame = ConvertFrameToNv12Locked();
return decodedFrame is not null;
}
finally
{
ffmpeg.av_frame_unref(_frame);
}
}
}
public void Reset()
{
lock (_gate)
{
if (_disposed)
{
return;
}
ffmpeg.avcodec_flush_buffers(_codecContext);
ffmpeg.av_packet_unref(_packet);
ffmpeg.av_frame_unref(_frame);
}
}
private Videodec2DecodedFrame? ConvertFrameToNv12Locked(
ulong fallbackPts = ulong.MaxValue,
ulong fallbackDts = ulong.MaxValue,
ulong attachedData = 0)
{
if (_frame->width <= 0 || _frame->height <= 0)
{
return null;
}
var width = checked((uint)_frame->width);
var height = checked((uint)_frame->height);
uint pitch;
ulong lumaBytes;
ulong requiredBytes;
try
{
pitch = checked((width + 255u) & ~255u);
lumaBytes = checked((ulong)pitch * height);
var chromaRows = ((ulong)height + 1UL) / 2UL;
requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows));
}
catch (OverflowException)
{
return null;
}
if (requiredBytes > int.MaxValue)
{
return null;
}
_swsContext = ffmpeg.sws_getCachedContext(
_swsContext,
_frame->width,
_frame->height,
(AVPixelFormat)_frame->format,
_frame->width,
_frame->height,
AVPixelFormat.AV_PIX_FMT_NV12,
ffmpeg.SWS_FAST_BILINEAR,
null,
null,
null);
if (_swsContext == null)
{
return null;
}
var nv12 = new byte[(int)requiredBytes];
fixed (byte* destination = nv12)
{
var destinationPlanes = new byte*[4]
{
destination,
destination + checked((int)lumaBytes),
null,
null,
};
var destinationStrides = new int[4]
{
checked((int)pitch),
checked((int)pitch),
0,
0,
};
var rows = ffmpeg.sws_scale(
_swsContext,
_frame->data,
_frame->linesize,
0,
_frame->height,
destinationPlanes,
destinationStrides);
if (rows != _frame->height)
{
return null;
}
}
return new Videodec2DecodedFrame(
nv12,
width,
height,
pitch,
(_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0)
{
Pts = FromAvTimestamp(_frame->pts, fallbackPts),
Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts),
AttachedData = attachedData,
KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0,
Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0,
Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0,
};
}
private static long ToAvTimestamp(ulong timestamp) =>
timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE;
private static ulong FromAvTimestamp(long timestamp, ulong fallback) =>
timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp;
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
{
@ -233,15 +496,28 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
return;
}
var inputOrdinal = Interlocked.Increment(ref _workerInputCount);
var decodedOk = item is null
? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height)
: DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height);
if (ShouldTrace(inputOrdinal))
{
Trace(
$"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " +
$"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}");
}
if (!decodedOk || !hasPicture || bgraFrame is null)
{
continue;
}
var frameOrdinal = Interlocked.Increment(ref _workerFrameCount);
if (ShouldTrace(frameOrdinal))
{
Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}");
}
try
{
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
@ -564,4 +840,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
}
}
}
private static bool IsTraceEnabled()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
return string.Equals(value, "1", StringComparison.Ordinal) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldTrace(long ordinal) =>
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
private static void Trace(string message)
{
if (TraceEnabled)
{
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using SharpEmu.HLE;
@ -15,10 +16,18 @@ public static class Videodec2Exports
{
private const int Ok = 0;
private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024;
private const uint FrameBufferAlignment = 0x100;
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> PictureInfos = new();
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
private static readonly bool TraceEnabled = IsTraceEnabled();
private static long _decodeCallCount;
private static long _decodedPictureCount;
[SysAbiExport(
Nid = "RnDibcGCPKw",
ExportName = "sceVideodec2QueryComputeMemoryInfo",
@ -27,24 +36,25 @@ public static class Videodec2Exports
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
if (paramAddress == 0 ||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
// Success needs no memory writes; the game initializes from its own fields.
return SetReturn(ctx, Ok);
}
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
private const int VideodecErrorStructSize = unchecked((int)0x811D0101);
private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F);
private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202);
// Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`.
private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024;
private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024;
// Opaque token the game hands back unmodified to later Videodec2 calls.
private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL;
[SysAbiExport(
Nid = "eD+X2SmxUt4",
ExportName = "sceVideodec2AllocateComputeQueue",
@ -52,8 +62,14 @@ public static class Videodec2Exports
LibraryName = "libSceVideodec2")]
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
{
var queueAddress = ctx[CpuRegister.Rdi];
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
var configAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
var queueAddress = ctx[CpuRegister.Rdx];
if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 ||
!ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) ||
computeMemoryAddress == 0 ||
!ctx.TryWriteUInt64(
queueAddress, computeMemoryAddress))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
@ -61,7 +77,18 @@ public static class Videodec2Exports
return SetReturn(ctx, Ok);
}
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
[SysAbiExport(
Nid = "UvtA3FAiF4Y",
ExportName = "sceVideodec2ReleaseComputeQueue",
Target = Generation.Gen5,
LibraryName = "libSceVideodec2")]
public static int Videodec2ReleaseComputeQueue(CpuContext ctx)
{
return SetReturn(
ctx,
ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId);
}
[SysAbiExport(
Nid = "qqMCwlULR+E",
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
@ -69,12 +96,18 @@ public static class Videodec2Exports
LibraryName = "libSceVideodec2")]
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
{
var configAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (memoryInfoAddress == 0 ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) ||
// Frame-slot size: must be nonzero or the game divides its arena by zero.
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000))
if (configAddress == 0 || memoryInfoAddress == 0 ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) ||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) ||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
@ -99,7 +132,9 @@ public static class Videodec2Exports
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
Decoders[handle] = Videodec2Decoder.TryCreate();
var decoder = Videodec2Decoder.TryCreate();
Decoders[handle] = decoder;
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
if (!ctx.TryWriteUInt64(decoderAddress, handle))
{
@ -111,7 +146,6 @@ public static class Videodec2Exports
return SetReturn(ctx, Ok);
}
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
[SysAbiExport(
Nid = "l1hXwscLuCY",
ExportName = "sceVideodec2Flush",
@ -120,33 +154,25 @@ public static class Videodec2Exports
public static int Videodec2Flush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var outputSlotObj = ctx[CpuRegister.Rsi];
var outputInfoAddress = ctx[CpuRegister.Rdx];
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
if (Decoders.TryGetValue(handle, out var decoder) &&
decoder is not null &&
decoder.FlushOutput(out var decodedFrame) &&
decodedFrame is not null)
{
// Drain in order: report an already-finished frame before queuing a new drain request.
if (decoder.TryConsumeProtocolReadySignal(out var width, out var height))
{
if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) &&
ctx.TryWriteUInt64(outputInfoAddress + 0x10, height))
{
_ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady);
}
}
else
{
decoder.RequestDrain();
}
_ = TryWriteDecodedFrame(
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
}
return SetReturn(ctx, Ok);
}
// No state to reset.
[SysAbiExport(
Nid = "wJXikG6QFN8",
ExportName = "sceVideodec2Reset",
@ -154,6 +180,12 @@ public static class Videodec2Exports
LibraryName = "libSceVideodec2")]
public static int Videodec2Reset(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
{
decoder.Reset();
}
return SetReturn(ctx, Ok);
}
@ -173,8 +205,6 @@ public static class Videodec2Exports
return SetReturn(ctx, Ok);
}
// rcx[0] is the picture-ready flag (1 = frame published); it lives in
// uninitialized stack and must always be written explicitly.
[SysAbiExport(
Nid = "852F5+q6+iM",
ExportName = "sceVideodec2Decode",
@ -186,8 +216,9 @@ public static class Videodec2Exports
var inputAuStruct = ctx[CpuRegister.Rsi];
var outputSlotObj = ctx[CpuRegister.Rdx];
var outputInfoAddress = ctx[CpuRegister.Rcx];
var callOrdinal = Interlocked.Increment(ref _decodeCallCount);
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
@ -195,6 +226,7 @@ public static class Videodec2Exports
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
{
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}");
return SetReturn(ctx, Ok);
}
@ -208,35 +240,235 @@ public static class Videodec2Exports
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
{
// Nothing sane to feed/fill this call; not an error.
TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor");
return SetReturn(ctx, Ok);
}
_ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts);
_ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts);
_ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData);
var auBuffer = new byte[auDataSize];
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
{
TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}");
return SetReturn(ctx, Ok);
}
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
decoder.EnqueueAccessUnit(auBuffer);
var decodeOk = decoder.DecodeAccessUnit(
auBuffer, pts, dts, attachedData, out var decodedFrame);
if (ShouldTrace(callOrdinal))
{
var prefixBytes = Math.Min(auBuffer.Length, 8);
Trace(
$"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " +
$"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " +
$"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " +
$"picture={decodedFrame is not null}");
}
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
if (!decodeOk || decodedFrame is null)
{
return SetReturn(ctx, Ok);
}
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
var copied = TryWriteDecodedFrame(
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
if (copied)
{
return SetReturn(ctx, Ok);
Interlocked.Increment(ref _decodedPictureCount);
}
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
return SetReturn(ctx, Ok);
}
private static readonly byte[] NoPicture = [0];
private static readonly byte[] PictureReady = [1];
[SysAbiExport(
Nid = "NtXRa3dRzU0",
ExportName = "sceVideodec2GetPictureInfo",
Target = Generation.Gen5,
LibraryName = "libSceVideodec2")]
public static int Videodec2GetPictureInfo(CpuContext ctx)
{
var outputInfoAddress = ctx[CpuRegister.Rdi];
var firstPictureInfoAddress = ctx[CpuRegister.Rsi];
var secondPictureInfoAddress = ctx[CpuRegister.Rdx];
if (outputInfoAddress == 0 || firstPictureInfoAddress == 0)
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) ||
(outputInfoSize != 0x30 && outputInfoSize != 0x38))
{
return SetReturn(ctx, VideodecErrorStructSize);
}
if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) ||
!TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) ||
!ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) ||
!ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) ||
isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 ||
!PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame))
{
return SetReturn(ctx, VideodecErrorOutputInfo);
}
if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) ||
(requestedSize != 104 && requestedSize != 120))
{
return SetReturn(ctx, VideodecErrorStructSize);
}
var picture = new byte[(int)requestedSize];
BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize);
picture[0x08] = 1;
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts);
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts);
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData);
picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0;
picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue);
picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue);
BinaryPrimitives.WriteUInt32LittleEndian(
picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u);
BinaryPrimitives.WriteUInt32LittleEndian(
picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u);
picture[0x34] = 1;
picture[0x4E] = 1;
picture[0x4F] = 5;
if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
if (secondPictureInfoAddress != 0 &&
!TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress))
{
return SetReturn(ctx, VideodecErrorStructSize);
}
Trace(
$"picture_info buffer=0x{pixelsAddress:X16} " +
$"pts=0x{decodedFrame.Pts:X16} size={requestedSize}");
return SetReturn(ctx, Ok);
}
private static readonly byte[] NotAccepted = [0];
private static readonly byte[] Accepted = [1];
private static readonly byte[] NoPicture = [0, 0, 0, 0];
private static readonly byte[] PictureReady = [1, 0, 1, 0];
private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0];
private static bool TryInitializeNoPicture(
CpuContext ctx,
ulong frameBufferAddress,
ulong outputInfoAddress)
{
return frameBufferAddress != 0 &&
outputInfoAddress != 0 &&
ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) &&
ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture);
}
internal static bool TryWriteDecodedFrame(
CpuContext ctx,
ulong frameBufferAddress,
ulong outputInfoAddress,
Videodec2DecodedFrame decodedFrame)
{
if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) ||
!ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) ||
!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) ||
pixelsAddress == 0)
{
return false;
}
if ((ulong)decodedFrame.Nv12.Length > slotBytes ||
!ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12))
{
return false;
}
if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) ||
!ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) ||
!ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes))
{
return false;
}
if (outputInfoBytes >= 0x38 &&
(!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch)))
{
return false;
}
if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) ||
!ctx.Memory.TryWrite(
outputInfoAddress + 0x08,
decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady))
{
return false;
}
PictureInfos[pixelsAddress] = decodedFrame;
return true;
}
private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address)
{
if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256)
{
return false;
}
var output = new byte[(int)size];
BinaryPrimitives.WriteUInt64LittleEndian(output, size);
return ctx.Memory.TryWrite(address, output);
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool IsTraceEnabled()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
return string.Equals(value, "1", StringComparison.Ordinal) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldTrace(long ordinal) =>
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
private static void TraceDecode(long ordinal, string message)
{
if (ShouldTrace(ordinal))
{
Trace($"decode call={ordinal} {message}");
}
}
private static void Trace(string message)
{
if (TraceEnabled)
{
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
}
}
private static int SetReturn(CpuContext ctx, int result)
{

View File

@ -63,7 +63,7 @@ public struct TileTransferArguments
// Runs the tiling, detiling, depth conversion and channel swap compute passes.
public sealed unsafe class GpuTiler : IDisposable
{
private const uint KindCount = 9;
private const uint KindCount = 10;
private const uint ElementSizeCount = 5;
private const uint DirectionCount = 2;
private const uint PipelineCount = KindCount * ElementSizeCount * DirectionCount;

View File

@ -1,13 +1,271 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Scheduling;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Images;
// Surface metadata (HTile, DCC, CMask, FMask) keyed by its guest address.
public sealed partial class GuestImageCache
{
private unsafe void MaterializeDccClear(
ResourceSlotIdentifier imageIdentifier,
in ImageRequest request,
uint metadataBaseLayer)
{
if (request.Description.Metadata.Kind != MetadataKind.Dcc)
{
return;
}
var description = request.Description;
var range = description.Metadata.Range;
using (var held = _lock.Hold())
{
var image = _slots.TryGet(imageIdentifier);
if (image == null)
{
return;
}
image.Description.Metadata = description.Metadata;
if (range.Size == 0 || description.Resources.Levels != 1 ||
image.Description.Resources.Levels != 1)
{
return;
}
_surfaceMetadata.Remove(range.Address);
}
var layers = description.TransferLayers;
const ulong metadataBlockSize = 0x1000;
if (!ImageDescription.IsValidRange(range) ||
range.Address % metadataBlockSize != 0 ||
layers == 0 ||
range.Size % layers != 0 ||
(range.Size / layers) % metadataBlockSize != 0)
{
throw SubmissionScheduler.Fatal(
$"DCC slices must contain aligned 4 KiB blocks: address=0x{range.Address:X16} size=0x{range.Size:X} layers={layers}.");
}
var view = request.View;
var volume = description.IsVolume && view.Type == ImageViewType.Type3D;
var first = volume ? 0u : metadataBaseLayer;
var imageFirst = volume ? 0u : view.BaseLayer;
var count = volume ? description.Extent.Depth : view.LayerCount;
if (first >= layers || count == 0 || count > layers - first)
{
throw SubmissionScheduler.Fatal(
$"The DCC view exceeds its metadata slices: first={first} count={count} layers={layers}.");
}
var sliceSize = range.Size / layers;
var synchronizedAddress = range.Address + sliceSize * first;
var synchronizedSize = sliceSize * count;
if (_bufferCache.HasGpuDirtyBytes(synchronizedAddress, synchronizedSize))
{
_bufferCache.ReadMemory(synchronizedAddress, synchronizedSize, isWrite: false);
}
const int metadataScanChunkSize = 64 * 1024;
var bytes = System.Buffers.ArrayPool<byte>.Shared.Rent(
checked((int)Math.Min(sliceSize, (ulong)metadataScanChunkSize)));
try
{
Span<byte> firstByte = stackalloc byte[1];
for (uint slice = 0; slice < count; slice++)
{
var address = range.Address + sliceSize * (first + slice);
if (!_backing.TryReadBacking(address, firstByte))
{
throw SubmissionScheduler.Fatal(
$"Could not read DCC metadata key: address=0x{address:X16}.");
}
var code = firstByte[0];
if (!PackedClearValue.TryDecodeDccColor(
request.View.Format,
code * 0x01010101u,
description.Metadata,
allowClearToRegister: request.Role == ImageRole.ColorTarget,
out var clear))
{
continue;
}
var uniform = true;
for (ulong position = 0; position < sliceSize && uniform;)
{
var length = checked((int)Math.Min(
sliceSize - position,
(ulong)Math.Min(bytes.Length, metadataScanChunkSize)));
var chunk = bytes.AsSpan(0, length);
if (!_backing.TryReadBacking(address + position, chunk))
{
throw SubmissionScheduler.Fatal(
$"Could not read DCC metadata backing: address=0x{address + position:X16} size=0x{length:X}.");
}
for (var index = 0; index < chunk.Length; index++)
{
if (chunk[index] != code)
{
uniform = false;
break;
}
}
position += (ulong)length;
}
if (!uniform)
{
continue;
}
using (var held = _lock.Hold())
{
if (_slots.TryGet(imageIdentifier) == null)
{
return;
}
ClearDccImage(
imageIdentifier,
request,
new SubresourceRange(view.BaseLevel, view.LevelCount, imageFirst + slice, 1),
clear);
}
if (request.Role != ImageRole.DisplaySurface)
{
_bufferCache.FillBuffer(address, sliceSize, uint.MaxValue, isGds: false);
}
}
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(bytes);
}
}
private unsafe void ClearDccImage(
ResourceSlotIdentifier imageIdentifier,
in ImageRequest request,
in SubresourceRange range,
in ClearColorValue clear)
{
var image = _slots[imageIdentifier];
ref readonly var description = ref image.Description;
if (range.BaseLevel >= description.Resources.Levels || range.LevelCount == 0 ||
range.LevelCount > description.Resources.Levels - range.BaseLevel)
{
throw SubmissionScheduler.Fatal(
$"The DCC clear mip range is invalid: base={range.BaseLevel} count={range.LevelCount} levels={description.Resources.Levels}.");
}
var layers = description.IsVolume
? Math.Max(description.Extent.Depth >> (int)range.BaseLevel, 1)
: image.Backing.Layers;
if (range.LayerCount == 0 || range.BaseLayer >= layers || range.LayerCount > layers - range.BaseLayer)
{
throw SubmissionScheduler.Fatal(
$"The DCC clear layer range is invalid: base={range.BaseLayer} count={range.LayerCount} layers={layers}.");
}
var fullImage = range.BaseLevel == 0 && range.LevelCount == description.Resources.Levels &&
range.BaseLayer == 0 && range.LayerCount == layers;
WatchImage(imageIdentifier);
if (!fullImage && (image.IsBufferModified || image.IsCpuDirty))
{
PopulateFromGuest(imageIdentifier, RefreshRequest(image), "before-dcc-clear");
if (image.Description.Samples == 1 && (image.IsBufferModified || image.IsCpuDirty))
{
throw SubmissionScheduler.Fatal(
$"The DCC clear left guest ownership in place: address=0x{description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty}.");
}
}
var scheduled = _scheduler.Current;
if (scheduled.IsInvalid)
{
throw SubmissionScheduler.Fatal("A DCC clear needs a command buffer that is recording.");
}
scheduled.EndRendering();
var command = new CommandBuffer(scheduled.Handle);
var needsAttachmentClear = request.View.Format != image.Backing.Format ||
(description.IsVolume && !fullImage);
if (needsAttachmentClear)
{
var viewDescription = new ImageViewDescription(
request.View.Format,
range.LayerCount == 1 ? ImageViewType.Type2D : ImageViewType.Type2DArray,
ImageAspectFlags.ColorBit,
range.BaseLevel,
range.LevelCount,
range.BaseLayer,
range.LayerCount,
default,
ImageUsageFlags.ColorAttachmentBit);
var view = image.GetOrCreateView(viewDescription);
image.Transition(
ImageLayout.ColorAttachmentOptimal,
AccessFlags.ColorAttachmentWriteBit,
range,
command);
var attachment = new RenderingAttachmentInfo
{
SType = StructureType.RenderingAttachmentInfo,
ImageView = view,
ImageLayout = ImageLayout.ColorAttachmentOptimal,
LoadOp = AttachmentLoadOp.Clear,
StoreOp = AttachmentStoreOp.Store,
ClearValue = new ClearValue { Color = clear },
};
var rendering = new RenderingInfo
{
SType = StructureType.RenderingInfo,
RenderArea = new Rect2D(
new Offset2D(0, 0),
new Extent2D(
Math.Max(description.Extent.Width >> (int)range.BaseLevel, 1),
Math.Max(description.Extent.Height >> (int)range.BaseLevel, 1))),
LayerCount = range.LayerCount,
ColorAttachmentCount = 1,
PColorAttachments = &attachment,
};
_device.Vk.CmdBeginRendering(command, &rendering);
_device.Vk.CmdEndRendering(command);
}
else
{
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
var nativeRange = new ImageSubresourceRange(
ImageAspectFlags.ColorBit,
range.BaseLevel,
range.LevelCount,
description.IsVolume ? 0u : range.BaseLayer,
description.IsVolume ? 1u : range.LayerCount);
var nativeClear = clear;
_device.Vk.CmdClearColorImage(
command,
image.Backing.Handle,
ImageLayout.TransferDstOptimal,
&nativeClear,
1,
&nativeRange);
}
TakeGpuOwnership(image);
}
public bool IsMetadata(ulong address)
{
using var held = _lock.Hold();
@ -57,7 +315,6 @@ public sealed partial class GuestImageCache
return true;
}
// True when registered DCC absorbed the fill and the guest dispatch can be skipped.
public bool TryAbsorbDccFill(ulong address, ulong size, uint fillValue)
{
if (!IsValidRange(address, size))

View File

@ -25,7 +25,7 @@ public sealed partial class GuestImageCache
private static ColorTransferPlan? PieceHashPlan(CachedImage image, in ImageRequest request)
{
ref readonly var info = ref image.Description;
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
info.Data.Size < PieceHashMinimumImageSize)
{
return null;

View File

@ -123,28 +123,48 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
return GetNullImage(request);
}
var found = LookUpImage(ref request, exactFormat);
var metadataBaseLayer = request.View.BaseLayer;
var result = LookUpImage(ref request, exactFormat);
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
{
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
ulong metadataAddress;
ulong sliceSize;
using (var held = _lock.Hold())
{
using var held = _lock.Hold();
var image = _slots[found];
if (image.Description.Metadata.Kind == MetadataKind.None)
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.Kind = MetadataKind.Dcc;
image.Description.Metadata.Range = request.Description.Metadata.Range;
image.Description.Metadata = request.Description.Metadata;
}
ref readonly var description = ref image.Description;
metadataAddress = description.Metadata.Range.Address;
sliceSize = description.DccSliceSize;
}
ref readonly var description = ref _slots[found].Description;
if (description.DccSliceSize is var sliceSize and not 0)
if (sliceSize != 0)
{
SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount);
SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount);
}
}
return found;
MaterializeDccClear(result, request, metadataBaseLayer);
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
{
using var held = _lock.Hold();
var image = _slots[result];
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
if (!nativeCurrent)
{
throw SubmissionScheduler.Fatal(
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
}
}
return result;
}
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
@ -231,17 +251,6 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
}
var image = _slots[result];
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
{
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
if (!nativeCurrent)
{
throw SubmissionScheduler.Fatal(
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
}
}
if (viewMip >= 0)
{
request.View = request.View with { BaseLevel = (uint)viewMip };
@ -353,8 +362,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
image.MarkGpuModified();
image.Uses.RenderTarget = true;
RefreshFromGuest(imageIdentifier, request);
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
{
image.Description.Metadata = request.Description.Metadata;
var address = request.Description.Metadata.Range.Address;

View File

@ -28,8 +28,11 @@ public struct MetadataDescription
public GuestSpan Range;
public MetadataKind Kind;
public uint Control;
public uint DccClearWord;
public DisplayCompression Compression;
public bool StencilCompressed;
public bool DccClearRegisterValid;
public bool DccAlphaMsb;
}
public readonly record struct SubresourceCount(uint Levels, uint Layers) : IComparable<SubresourceCount>
@ -366,7 +369,8 @@ public struct ImageDescription
{
case MetadataKind.None:
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
{
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
}
@ -378,6 +382,11 @@ public struct ImageDescription
throw SubmissionScheduler.Fatal($"The HTile metadata is invalid: address=0x{Metadata.Range.Address:X16} size=0x{Metadata.Range.Size:X16}.");
}
if (Metadata.DccClearWord != 0 || Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
{
throw SubmissionScheduler.Fatal("HTile metadata carries DCC clear state.");
}
break;
case MetadataKind.Dcc:
if (Metadata.Range.Address == 0 || Metadata.Range.Address >= TrackerLayout.SpaceBytes ||
@ -507,6 +516,87 @@ public static class DisplayFormatRule
// Decoders for the packed 32-bit clear values guest compute fills carry.
public static class PackedClearValue
{
public static bool SupportsDccFixedColor(Format format) => DccComponentCount(format) != 0;
public static bool TryDecodeDccColor(
Format format,
uint metadataFill,
in MetadataDescription metadata,
bool allowClearToRegister,
out ClearColorValue clear)
{
clear = default;
var code = (byte)metadataFill;
if (metadataFill != code * 0x01010101u)
{
return false;
}
if (code == 0x20)
{
return allowClearToRegister && metadata.DccClearRegisterValid &&
TryDecodeColor(format, metadata.DccClearWord, out clear);
}
if (code == 0x00)
{
return true;
}
var components = DccComponentCount(format);
if (components == 0 || code is not (0x40 or 0x80 or 0xc0))
{
return false;
}
var rgb = (code & 0x80) != 0 ? 1.0f : 0.0f;
var alpha = (code & 0x40) != 0 ? 1.0f : 0.0f;
Span<float> channels = stackalloc float[4] { rgb, rgb, rgb, alpha };
if (!metadata.DccAlphaMsb)
{
(channels[0], channels[3]) = (channels[3], channels[0]);
}
if (components == 1)
{
channels[0] = channels[3];
}
else if (components == 2)
{
channels[1] = channels[3];
}
switch (format)
{
case Format.B8G8R8A8Unorm:
case Format.B8G8R8A8Srgb:
case Format.A2R10G10B10UnormPack32:
case Format.A1R5G5B5UnormPack16:
case Format.R5G6B5UnormPack16:
(channels[0], channels[2]) = (channels[2], channels[0]);
break;
case Format.R4G4B4A4UnormPack16:
(channels[0], channels[3]) = (channels[3], channels[0]);
(channels[1], channels[2]) = (channels[2], channels[1]);
break;
}
clear = new ClearColorValue(channels[0], channels[1], channels[2], channels[3]);
return true;
}
private static uint DccComponentCount(Format format) => format switch
{
Format.R8Unorm or Format.R16Unorm or Format.R16Sfloat or Format.R32Sfloat => 1,
Format.R8G8Unorm or Format.R16G16Unorm or Format.R16G16Sfloat or Format.R32G32Sfloat => 2,
Format.R5G6B5UnormPack16 or Format.B10G11R11UfloatPack32 => 3,
Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.A1R5G5B5UnormPack16 or
Format.R4G4B4A4UnormPack16 or Format.R16G16B16A16Unorm or Format.R16G16B16A16Sfloat or
Format.R32G32B32A32Sfloat => 4,
_ => 0,
};
public static bool TryDecodeColor(Format format, uint packed, out ClearColorValue clear)
{
static float Unorm8(uint value) => (value & 0xff) / 255.0f;

View File

@ -43,24 +43,38 @@ public static partial class ImageRequestBuilders
public static uint SampleCount(uint encodedLog2) => encodedLog2 <= 3 ? 1u << (int)encodedLog2 : 0;
private static bool DccAlphaOnMsb(in ColorTargetWords words)
{
switch (words.Layout)
{
case ChannelLayout.Bits10_10_10_2:
case ChannelLayout.Bits10_10_10_2Float:
case ChannelLayout.Bits5_5_5_1:
return true;
case ChannelLayout.Bits2_10_10_10:
case ChannelLayout.Bits1_5_5_5:
return false;
}
var components = GuestPixelFormats.ResolveRenderTargetEncoding(words.Layout, words.NumberType).Components;
if (components == 1)
{
return words.Order != ChannelOrder.Standard;
}
return components == 3 || words.Order is ChannelOrder.Standard or ChannelOrder.Alternate;
}
// DCC clears use the target's packed clear word; the fixed-clear set is a format allow-list.
private static (bool Supported, bool FixedSupported, ClearColorValue Value) DccClearInfo(Format format, bool hasDcc, uint packedClear)
{
ClearColorValue value = default;
var supported = hasDcc && PackedClearValue.TryDecodeColor(format, packedClear, out value);
var fixedSupported = hasDcc && SupportsDccFixedClear(format);
var fixedSupported = hasDcc && PackedClearValue.SupportsDccFixedColor(format);
return (supported, fixedSupported, supported ? value : default);
}
public static bool SupportsDccFixedClear(Format format) => format switch
{
Format.R8Unorm or Format.R8G8Unorm or Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.R5G6B5UnormPack16 or Format.A1R5G5B5UnormPack16 or
Format.R4G4B4A4UnormPack16 or Format.R16Unorm or Format.R16G16Unorm or Format.R16G16B16A16Unorm or Format.R16Sfloat or
Format.R16G16Sfloat or Format.R16G16B16A16Sfloat or Format.R32Sfloat or Format.R32G32Sfloat or Format.R32G32B32A32Sfloat or
Format.B10G11R11UfloatPack32 => true,
_ => false,
};
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
// Builds the request for a bound color target. Null when the slot carries no target.
public static ColorTargetResolution? ColorTarget(in ColorTargetWords words, uint targetMask, uint drawLayerOffset, bool ignoreTargetMask)
@ -216,7 +230,9 @@ public static partial class ImageRequestBuilders
}
else
{
pitch = width;
// A linear color buffer uses the same 256-byte row alignment as a linear texture of the same memory;
// an unaligned pitch made the target and a later texture view of it two different cached images.
pitch = TileGeometry.TexturePitch(transferFormat, width, GuestTileMode.Linear);
}
var mipSpans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
@ -324,8 +340,14 @@ public static partial class ImageRequestBuilders
if (hasDcc)
{
// DCC lives in its own allocation; the address lets the cache match fills seen before the target.
_ = TileGeometry.TryGetDccSize(
width, height, volume ? depth : view.ImageLayers, bytesPerElement, levels, tileMode,
out var metadataSize, words.FragmentsLog2);
description.Metadata.Kind = MetadataKind.Dcc;
description.Metadata.Range = new GuestSpan(words.DccAddress, 0);
description.Metadata.Range = new GuestSpan(words.DccAddress, metadataSize.Size);
description.Metadata.DccClearWord = words.ClearWord0;
description.Metadata.DccClearRegisterValid = true;
description.Metadata.DccAlphaMsb = DccAlphaOnMsb(words);
}
for (var level = 0; level < levels; level++)

View File

@ -300,12 +300,15 @@ public static partial class ImageRequestBuilders
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
description.Samples = samples;
description.TileMode = tile;
var metadataAddress = descriptor.MetadataAddress << 8;
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
if (!shape.R128 && words.Length >= 8 && descriptor.MetadataCompress &&
tile != GuestTileMode.Depth && !description.IsDepth)
{
_ = TileGeometry.TryGetDccSize(
width, height, volume ? depth : imageLayers, description.BytesPerBlock, levels, tile,
out var metadataSize, (uint)System.Numerics.BitOperations.TrailingZeroCount(samples));
description.Metadata.Kind = MetadataKind.Dcc;
description.Metadata.Range = new GuestSpan(metadataAddress, 0);
description.Metadata.Range = new GuestSpan(descriptor.MetadataAddress << 8, metadataSize.Size);
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
}
if (samples > 1)

View File

@ -78,6 +78,8 @@ public readonly struct TextureDescriptorWords
public bool MetadataCompress => ((Fields[6] >> 21) & 0x1) == 1;
public bool DccAlphaMsb => ((Fields[6] >> 22) & 0x1) == 1;
public ulong MetadataAddress => ((Fields[6] >> 24) & 0xFF) | ((ulong)Fields[7] << 8);
}

View File

@ -452,7 +452,9 @@ public sealed class TextureTransferLayout
transfer.Width = Math.Max((region.ImageExtent.Width + texture.TexelWidth - 1) / texture.TexelWidth, 1);
transfer.Height = Math.Max((logicalHeight + texture.TexelHeight - 1) / texture.TexelHeight, 1);
transfer.Depth = 1;
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB ? region.ImageSubresource.BaseArrayLayer : 0;
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
? region.ImageSubresource.BaseArrayLayer
: 0;
transfer.Pitch = Math.Max((pitch + texture.TexelWidth - 1) / texture.TexelWidth, 1);
transfer.Tail = tail;
transfer.TailX = tail ? mip.TailX : 0;

View File

@ -230,6 +230,75 @@ public static partial class TileGeometry
}
}
private static uint Gen5RenderTargetOffset(uint x, uint y, uint bytesPerElement)
{
uint offset = 0;
switch (bytesPerElement)
{
case 1:
offset ^= (y << 2) & 0x0008;
offset ^= (y << 4) & 0x0010;
offset ^= (y << 3) & 0x00a0;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 6) & 0x1000;
offset ^= (y << 7) & 0x4000;
offset ^= x & 0x0007;
offset ^= (x << 3) & 0x0040;
offset ^= (x << 5) & 0x0300;
offset ^= (x << 4) & 0x0400;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 7) & 0x2000;
offset ^= (x << 8) & 0x8000;
return offset;
case 2:
offset ^= (y << 4) & 0x0070;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 8) & 0x5000;
offset ^= (x << 1) & 0x000e;
offset ^= (x << 4) & 0x0480;
offset ^= (x << 5) & 0x0300;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 7) & 0x2000;
offset ^= (x << 8) & 0x8000;
return offset;
case 4:
offset ^= (y << 4) & 0x0070;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 9) & 0x1000;
offset ^= (y << 8) & 0x4000;
offset ^= (x << 2) & 0x000c;
offset ^= (x << 5) & 0x0380;
offset ^= (x << 4) & 0x0400;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 9) & 0xa000;
return offset;
case 8:
offset ^= (y << 4) & 0x0010;
offset ^= (y << 6) & 0x0080;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 10) & 0x5000;
offset ^= (x << 3) & 0x0008;
offset ^= (x << 4) & 0x0460;
offset ^= (x << 5) & 0x0300;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 10) & 0x2000;
offset ^= (x << 9) & 0x8000;
return offset;
case 16:
offset ^= (x << 4) & 0x0410;
offset ^= (x << 5) & 0x0340;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 11) & 0xa000;
offset ^= (y << 5) & 0x0f20;
offset ^= (y << 6) & 0x0080;
offset ^= (y << 10) & 0x1000;
offset ^= (y << 11) & 0x4000;
return offset;
default:
throw SubmissionScheduler.Fatal($"The Gen5 render-target tile block does not support this element size: bytes={bytesPerElement}.");
}
}
private static uint Depth64KB8X(uint x) =>
(x & 0x0001) ^ ((x << 1) & 0x0004) ^ ((x << 2) & 0x0010) ^ ((x << 3) & 0x0040) ^ ((x << 5) & 0x0300) ^
((x << 4) & 0x0400) ^ ((x << 6) & 0x0800) ^ ((x << 7) & 0x2000) ^ ((x << 8) & 0x8000);
@ -319,6 +388,14 @@ public static partial class TileGeometry
offset = RenderTargetOffset(x, y, bytes);
break;
case TileBlockKind.RenderTarget64KBGen5:
if (bytes > 16)
{
return false;
}
offset = Gen5RenderTargetOffset(x, y, bytes);
break;
case TileBlockKind.Depth64KB:
switch (bytes)
{
@ -364,17 +441,19 @@ public static partial class TileGeometry
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
}
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
{
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
{
return false;
}
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
}
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
{
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
}

View File

@ -32,6 +32,7 @@ public enum TileBlockKind : uint
Prt64KB3D,
RenderTarget64KB,
Depth64KB,
RenderTarget64KBGen5,
}
public readonly record struct TileBlockLayout(TileBlockKind Kind, uint BytesPerElement, uint BlockSize, uint BlockWidth, uint BlockHeight, uint BlockDepth)
@ -109,6 +110,7 @@ public static partial class TileGeometry
new(65536, Thick64KB, 16),
new(65536, Thin64KB, 16),
new(65536, Thin64KB, 8),
new(65536, Thin64KB, 16),
];
private static readonly Log2BlockDimensions[][] MsaaBlocks =
@ -247,6 +249,7 @@ public static partial class TileGeometry
case TileBlockKind.Standard64KB:
case TileBlockKind.Prt64KB:
case TileBlockKind.RenderTarget64KB:
case TileBlockKind.RenderTarget64KBGen5:
rule = new MipTailRule(TailThin64KB[index], block.BlockWidth >> 1, block.BlockHeight);
return true;
default:
@ -297,7 +300,7 @@ public static partial class TileGeometry
kind = TileBlockKind.Depth64KB;
break;
case GuestTileMode.RenderTarget:
kind = TileBlockKind.RenderTarget64KB;
kind = TileBlockKind.RenderTarget64KBGen5;
break;
default:
return false;
@ -308,7 +311,7 @@ public static partial class TileGeometry
return false;
}
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB &&
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
{
return false;
@ -605,6 +608,39 @@ public static partial class TileGeometry
return true;
}
public static bool TryGetDccSize(
uint width,
uint height,
uint slices,
uint bytesPerElement,
uint levels,
GuestTileMode tile,
out TileSizeAndAlignment totalSize,
uint fragmentsLog2 = 0)
{
totalSize = default;
if (width == 0 || height == 0 || slices == 0 || levels != 1 || fragmentsLog2 != 0 ||
!BitOperations.IsPow2(bytesPerElement) || bytesPerElement > 16 ||
tile is not (GuestTileMode.RenderTarget or GuestTileMode.Depth))
{
return false;
}
var coverageBits = 20 - BitOperations.TrailingZeroCount(bytesPerElement);
var blockWidth = 1u << ((coverageBits + 1) / 2);
var blockHeight = 1u << (coverageBits / 2);
var blocksX = ((ulong)width + blockWidth - 1) / blockWidth;
var blocksY = ((ulong)height + blockHeight - 1) / blockHeight;
var blocks = blocksX * blocksY;
if (blocks == 0 || blocks > uint.MaxValue / 4096u / slices)
{
return false;
}
totalSize = new TileSizeAndAlignment((uint)(blocks * slices * 4096u), 4096);
return true;
}
public static bool TryGetRenderTargetMipLayout(uint width, uint height, uint pitch, uint bytesPerElement, uint levels, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
{
totalSize = default;

View File

@ -41,6 +41,7 @@ internal interface IShaderPipelineHost
// The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be
// emitted as real 64-bit atomics instead of a non-atomic 32-bit pair.
bool SharedInt64AtomicsEnabled { get; }
bool ShaderSignedZeroInfNanPreserveFloat32Supported => false;
bool ExecGuardElisionEnabled => true;
ShaderPrewarmList? ShaderPrewarm => null;
bool PerVertexPixelInputsSupported => true;

View File

@ -593,6 +593,7 @@ internal sealed class ShaderProgramCache
{
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
switch (source.Stage)
{
case ShaderStage.Vertex:
@ -605,6 +606,7 @@ internal sealed class ShaderProgramCache
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
VertexInputs = entry.VertexInputs,
PositionExportControl = info.PositionExportControl,
@ -632,6 +634,7 @@ internal sealed class ShaderProgramCache
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
PixelOutputs = options.PixelOutputs,
PixelInputEnable = options.PixelInputEnable,
PixelCustomInterpolationMask = info.CustomInterpolationMask,
@ -643,7 +646,7 @@ internal sealed class ShaderProgramCache
default:
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
sharedInt64Atomics, _host.ExecGuardElisionEnabled);
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
}
}
@ -659,7 +662,8 @@ internal sealed class ShaderProgramCache
usesDispatchThreadLimits: usesDispatchThreadLimits);
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision) =>
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
bool signedZeroInfNanPreserve) =>
new(plan, resources, layout)
{
WaveSize = info.WaveSize,
@ -667,6 +671,7 @@ internal sealed class ShaderProgramCache
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
ScratchDwords = info.ScratchDwords,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
ComputeSystemRegisters = systemRegisters,
LocalDataShareDwords = info.LocalDataShareDwords,
LocalSizeX = Math.Max(info.ThreadsX, 1),
@ -675,7 +680,8 @@ internal sealed class ShaderProgramCache
};
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
bool sharedInt64Atomics, bool execGuardElision, out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled,
out BindingLayout? layout, out string error)
{
compiled = null;
layout = null;
@ -692,7 +698,7 @@ internal sealed class ShaderProgramCache
layout = AllocateLayout(program, plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor,
record.Info.DispatchThreadDimensions);
var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters,
sharedInt64Atomics, execGuardElision);
sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve);
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
}
catch (Exception exception)

View File

@ -141,6 +141,25 @@ public static class JsonExports
InitializerSetGlobalNullAccessCallback(ctx);
#pragma warning restore SHEM004
[SysAbiExport(
Nid = "i1393UBWu1U",
ExportName = "_ZN3sce4Json11Initializer28setGlobalSpecialFloatHandlerEPFKNS0_5ValueENS0_12FunctionTypeEdPS3_PvPbES6_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerSetGlobalSpecialFloatHandler(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
JsonObjectHeap.GlobalSpecialFloatHandler = ctx[CpuRegister.Rsi];
JsonObjectHeap.GlobalSpecialFloatHandlerContext = ctx[CpuRegister.Rdx];
TraceJson("Initializer.setGlobalSpecialFloatHandler", thisAddress, ctx[CpuRegister.Rsi]);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
@ -226,6 +245,30 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WVZBP4IyM+E",
ExportName = "_ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE",
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2SetSpecialFloatFormatType(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> field = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(field, unchecked((uint)ctx[CpuRegister.Rsi]));
if (!ctx.Memory.TryWrite(thisAddress + 0x18, field))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",

View File

@ -87,6 +87,10 @@ internal static class JsonObjectHeap
public static ulong GlobalNullAccessCallbackContext;
public static ulong GlobalSpecialFloatHandler;
public static ulong GlobalSpecialFloatHandlerContext;
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
@ -106,5 +110,7 @@ internal static class JsonObjectHeap
Strings.Clear();
GlobalNullAccessCallback = 0;
GlobalNullAccessCallbackContext = 0;
GlobalSpecialFloatHandler = 0;
GlobalSpecialFloatHandlerContext = 0;
}
}

View File

@ -399,7 +399,7 @@ public static class KernelExports
ExportName = "sceKernelOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenCore(ctx);
[SysAbiExport(
Nid = "mqQMh1zPPT8",

View File

@ -274,7 +274,7 @@ public static partial class KernelMemoryCompatExports
try
{
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, GuestFileShare);
stream.SetLength(length);
}
catch (FileNotFoundException)

View File

@ -14,12 +14,14 @@ using System.Threading;
using System.Runtime.InteropServices;
using System.Linq;
using System.Globalization;
using System.Security.Cryptography;
namespace SharpEmu.Libs.Kernel;
public static partial class KernelMemoryCompatExports
{
private const int MaxGuestStringLength = 4096;
internal const FileShare GuestFileShare = FileShare.ReadWrite | FileShare.Delete;
private const int WideCharSize = sizeof(ushort);
private const int MemsetChunkSize = 16 * 1024;
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
@ -96,6 +98,7 @@ public static partial class KernelMemoryCompatExports
private static readonly object _fdGate = new();
private static readonly Dictionary<int, FileStream> _openFiles = new();
private static readonly HashSet<int> _randomDeviceDescriptors = new();
private static readonly Dictionary<int, HostMovieBridge.BinkGuestCompletionShim>
_binkGuestCompletionShims = new();
private static readonly Dictionary<int, string> _observedBinkGuestFiles = new();
@ -1439,7 +1442,9 @@ public static partial class KernelMemoryCompatExports
ExportName = "_open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpenUnderscore(CpuContext ctx)
public static int KernelOpenUnderscore(CpuContext ctx) => PosixOpen(ctx);
internal static int KernelOpenCore(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
@ -1450,6 +1455,19 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (IsRandomDevicePath(guestPath) && (flags & O_DIRECTORY) == 0)
{
var randomFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
lock (_fdGate)
{
_randomDeviceDescriptors.Add(randomFd);
}
LogOpenTrace($"_open random-device path='{guestPath}' flags=0x{flags:X8} fd={randomFd}");
ctx[CpuRegister.Rax] = unchecked((ulong)randomFd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var hostPath = ResolveGuestPath(guestPath);
var access = ResolveOpenAccess(flags);
var mode = ResolveOpenMode(flags, access);
@ -1519,7 +1537,7 @@ public static partial class KernelMemoryCompatExports
}
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
var stream = new FileStream(hostPath, mode, access, GuestFileShare);
if ((flags & O_APPEND) != 0)
{
stream.Seek(0, SeekOrigin.End);
@ -1677,11 +1695,11 @@ public static partial class KernelMemoryCompatExports
}
// POSIX open(2): translates a failed raw open into -1/errno. On success
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
// KernelOpenCore already writes the fd into RAX (the import bridge
// prefers a written RAX over the return value), so returning 0 is correct.
public static int PosixOpen(CpuContext ctx)
{
var result = KernelOpenUnderscore(ctx);
var result = KernelOpenCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result);
@ -1734,7 +1752,8 @@ public static partial class KernelMemoryCompatExports
{
// Stop at the first miss and report its index.
// The caller can then use its normal file-open fallback.
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelAprResolveFilepathsToIdsAndFileSizes: '{guestPath}' -> '{hostPath}' not found (index {i} of {count})");
if (sizesAddress != 0 &&
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
{
@ -2254,7 +2273,12 @@ public static partial class KernelMemoryCompatExports
string? observedBinkPath = null;
lock (_fdGate)
{
if (_openFiles.Remove(fd, out stream))
if (_randomDeviceDescriptors.Remove(fd))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
else if (_openFiles.Remove(fd, out stream))
{
_binkGuestCompletionShims.Remove(fd);
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
@ -2307,6 +2331,25 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
bool isRandomDevice;
lock (_fdGate)
{
isRandomDevice = _randomDeviceDescriptors.Contains(fd);
}
if (isRandomDevice)
{
var randomBytes = GC.AllocateUninitializedArray<byte>(requested);
RandomNumberGenerator.Fill(randomBytes);
if (!ctx.Memory.TryWrite(bufferAddress, randomBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryReadSocketFd(
ctx,
fd,
@ -2842,15 +2885,23 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
var searchEnd = searchEndRaw <= 0
? GuestMemoryLayout.DirectBytes
: Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
if (searchStart >= searchEnd)
if (!ctx.TryWriteUInt64(outAddress, 0) || !ctx.TryWriteUInt64(outSize, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (searchStartRaw < 0 || searchEndRaw < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var searchStart = (ulong)searchStartRaw;
var searchEnd = Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
if (searchStart >= searchEnd)
{
return MemoryNoSpace;
}
bool foundSpan;
ulong candidate;
ulong rangeAvailable;
@ -2866,7 +2917,7 @@ public static partial class KernelMemoryCompatExports
if (!foundSpan)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return MemoryNoSpace;
}
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
@ -6901,9 +6952,14 @@ public static partial class KernelMemoryCompatExports
string? hostPath = null;
bool isDirectory = false;
bool isRandomDevice = false;
lock (_fdGate)
{
if (_openDirectories.TryGetValue(fd, out var directory))
if (_randomDeviceDescriptors.Contains(fd))
{
isRandomDevice = true;
}
else if (_openDirectories.TryGetValue(fd, out var directory))
{
hostPath = directory.Path;
isDirectory = true;
@ -6914,6 +6970,14 @@ public static partial class KernelMemoryCompatExports
}
}
if (isRandomDevice)
{
var now = DateTime.UtcNow;
LogIoTrace("fstat", "/dev/urandom", $"fd={fd} size=0 device=random");
return TryWriteKernelStat(ctx, statAddress, isDirectory: false, size: 0,
now, now, now, "/dev/urandom");
}
if (!string.IsNullOrWhiteSpace(hostPath))
{
long size = 0;
@ -6935,6 +6999,10 @@ public static partial class KernelMemoryCompatExports
return !string.IsNullOrWhiteSpace(hostPath) && TryWriteHostPathStat(ctx, statAddress, hostPath!, isDirectory);
}
private static bool IsRandomDevicePath(string path) =>
string.Equals(path, "/dev/random", StringComparison.Ordinal) ||
string.Equals(path, "/dev/urandom", StringComparison.Ordinal);
private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath)
{
var isDirectory = Directory.Exists(hostPath);

View File

@ -80,6 +80,13 @@ public static class KernelRuntimeCompatExports
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong RdtscDelegate();
[SysAbiExport(
Nid = "uvT2iYBBnkY",
ExportName = "sceKernelSync",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelSync(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "1jfXLRVzisc",
ExportName = "sceKernelUsleep",

View File

@ -0,0 +1,31 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
/// <summary>
/// Process-argument accessors exported by libKernel for the platform libc.
/// </summary>
public static class LibcProcessArgumentExports
{
[SysAbiExport(
Nid = "iKJMWrAumPE",
ExportName = "getargc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetArgc(CpuContext ctx) =>
ctx.SetReturn(HleDataSymbols.ProcessArgumentCount);
[SysAbiExport(
Nid = "FJmglmTMdr4",
ExportName = "getargv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetArgv(CpuContext ctx)
{
ctx[CpuRegister.Rax] = HleDataSymbols.ProcessArgumentVectorAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}

View File

@ -69,6 +69,20 @@ public static class LibcStdioExports
private static readonly object _ctypeTableGate = new();
private static nint _ctypeTableBase;
[SysAbiExport(
Nid = "vZkmJmvqueY",
ExportName = "_Lockfilelock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int LockFileLock(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "0x7rx8TKy2Y",
ExportName = "_Unlockfilelock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int UnlockFileLock(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "xeYO4u7uyJ0",
ExportName = "fopen",
@ -133,7 +147,7 @@ public static class LibcStdioExports
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
var stream = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
{
stream.Seek(0, SeekOrigin.End);
@ -1049,7 +1063,7 @@ public static class LibcStdioExports
}
}
var replacement = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
var replacement = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
lock (file.Gate)
{
if (file.IsClosed)

View File

@ -14,28 +14,42 @@ namespace SharpEmu.Libs.Network;
public static class NetExports
{
private const int NetErrorNoEntry = unchecked((int)0x80410102);
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
private const int NetErrorFault = unchecked((int)0x8041010E);
private const int NetErrorAlreadyExists = unchecked((int)0x80410111);
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
private const int NetErrorTooManyFiles = unchecked((int)0x80410118);
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
private const int NetErrorTooManyOpenFiles = unchecked((int)0x80410118);
private const int NetErrorResolverNoDns = unchecked((int)0x804101E1);
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
private const int NetErrnoNoEntry = 2;
private const int NetErrnoBadFileDescriptor = 9;
private const int NetErrnoFault = 14;
private const int NetErrnoAlreadyExists = 17;
private const int NetErrnoInvalidArgument = 22;
private const int NetErrnoTooManyFiles = 24;
private const int NetErrnoWouldBlock = 35;
private const int NetErrnoAddressInUse = 48;
private const int NetErrnoTooManyOpenFiles = 24;
private const int NetErrnoNotInitialized = 200;
private const int NetErrnoResolverNoDns = 225;
private const int MaxNameLength = 256;
private const int EpollIdBase = 1024;
private const int EpollCapacity = 32;
private const int NetEpollEventSize = 24;
private const uint NetEpollIn = 0x00000001;
private const uint NetEpollOut = 0x00000002;
private const uint NetEpollError = 0x00000008;
private static ReadOnlySpan<byte> OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
private static readonly object EpollGate = new();
private static readonly Dictionary<int, NetEpollContext> _epolls = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static int _nextSocketId = 255;
@ -52,6 +66,26 @@ public static class NetExports
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
private sealed class NetEpollContext(string name)
{
public string Name { get; } = name;
public List<NetEpollRegistration> Registrations { get; } = [];
}
private readonly record struct NetEpollRegistration(int SocketId, NetEpollEvent Event);
private readonly record struct NetEpollEvent(uint Events, ulong Data);
internal static void ResetEpollsForTests()
{
lock (EpollGate)
{
_epolls.Clear();
Monitor.PulseAll(EpollGate);
}
}
internal static bool TryGetReadEventState(
int socketId,
ulong lowWater,
@ -116,6 +150,11 @@ public static class NetExports
_initialized = false;
_pools.Clear();
_resolvers.Clear();
lock (EpollGate)
{
_epolls.Clear();
Monitor.PulseAll(EpollGate);
}
foreach (var socket in _sockets.Values)
{
socket.Dispose();
@ -195,6 +234,12 @@ public static class NetExports
public static int NetSocketClose(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (KernelSocketCompatExports.TryCloseSocketFd(id))
{
TraceNet("socket.close.posix", id, 0, 0, 0);
return ctx.SetReturn(0);
}
if (!_sockets.TryRemove(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
@ -715,6 +760,291 @@ public static class NetExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "SF47kB2MNTo",
ExportName = "sceNetEpollCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollCreate(CpuContext ctx)
{
if (!_initialized)
{
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
}
var nameAddress = ctx[CpuRegister.Rdi];
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
if (nameAddress == 0 || flags != 0 ||
!TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var name))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
lock (EpollGate)
{
for (var index = 0; index < EpollCapacity; index++)
{
var id = EpollIdBase + index;
if (_epolls.ContainsKey(id))
{
continue;
}
_epolls.Add(id, new NetEpollContext(name));
TraceNet("epoll.create", id, nameAddress, 0, 0);
return ctx.SetReturn(id);
}
}
return SetNetError(ctx, NetErrorTooManyOpenFiles, NetErrnoTooManyOpenFiles);
}
[SysAbiExport(
Nid = "ZVw46bsasAk",
ExportName = "sceNetEpollControl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollControl(CpuContext ctx)
{
const int EpollControlAdd = 1;
const int EpollControlModify = 2;
const int EpollControlDelete = 3;
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
var operation = unchecked((int)ctx[CpuRegister.Rsi]);
var socketId = unchecked((int)ctx[CpuRegister.Rdx]);
var eventAddress = ctx[CpuRegister.Rcx];
if (operation is < EpollControlAdd or > EpollControlDelete ||
(operation is EpollControlAdd or EpollControlModify && eventAddress == 0) ||
(operation == EpollControlDelete && eventAddress != 0))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
if (!_sockets.ContainsKey(socketId))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
NetEpollEvent requestedEvent = default;
if (eventAddress != 0 && !TryReadNetEpollEvent(ctx, eventAddress, out requestedEvent))
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
lock (EpollGate)
{
if (!_epolls.TryGetValue(epollId, out var epoll))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
var registrationIndex = epoll.Registrations.FindIndex(value => value.SocketId == socketId);
switch (operation)
{
case EpollControlAdd when registrationIndex >= 0:
return SetNetError(ctx, NetErrorAlreadyExists, NetErrnoAlreadyExists);
case EpollControlAdd:
epoll.Registrations.Add(new NetEpollRegistration(socketId, requestedEvent));
break;
case EpollControlModify when registrationIndex < 0:
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
case EpollControlModify:
epoll.Registrations[registrationIndex] =
new NetEpollRegistration(socketId, requestedEvent);
break;
case EpollControlDelete when registrationIndex < 0:
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
case EpollControlDelete:
epoll.Registrations.RemoveAt(registrationIndex);
break;
}
Monitor.PulseAll(EpollGate);
}
TraceNet(
"epoll.control",
epollId,
unchecked((uint)operation),
unchecked((uint)socketId),
requestedEvent.Events);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "drjIbDbA7UQ",
ExportName = "sceNetEpollWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollWait(CpuContext ctx)
{
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
var eventsAddress = ctx[CpuRegister.Rsi];
var maxEvents = unchecked((int)ctx[CpuRegister.Rdx]);
var timeoutMicroseconds = unchecked((int)ctx[CpuRegister.Rcx]);
if (eventsAddress == 0)
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
if (maxEvents <= 0 || timeoutMicroseconds < -1)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
NetEpollRegistration[] registrations;
lock (EpollGate)
{
if (!_epolls.TryGetValue(epollId, out var epoll))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (epoll.Registrations.Count == 0 && timeoutMicroseconds != 0)
{
if (timeoutMicroseconds < 0)
{
while (_epolls.TryGetValue(epollId, out epoll) && epoll.Registrations.Count == 0)
{
Monitor.Wait(EpollGate);
}
}
else
{
Monitor.Wait(
EpollGate,
TimeSpan.FromTicks((long)timeoutMicroseconds * TimeSpan.TicksPerMicrosecond));
}
if (!_epolls.TryGetValue(epollId, out epoll))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
}
registrations = epoll.Registrations.ToArray();
}
if (registrations.Length == 0)
{
TraceNet("epoll.wait", epollId, unchecked((uint)maxEvents), unchecked((uint)timeoutMicroseconds), 0);
return ctx.SetReturn(0);
}
var readSockets = new List<Socket>();
var writeSockets = new List<Socket>();
var errorSockets = new List<Socket>();
var hostRegistrations = new Dictionary<Socket, NetEpollRegistration>();
foreach (var registration in registrations)
{
if (!_sockets.TryGetValue(registration.SocketId, out var socket))
{
continue;
}
hostRegistrations[socket] = registration;
if ((registration.Event.Events & NetEpollIn) != 0)
{
readSockets.Add(socket);
}
if ((registration.Event.Events & NetEpollOut) != 0)
{
writeSockets.Add(socket);
}
errorSockets.Add(socket);
}
if (hostRegistrations.Count == 0)
{
return ctx.SetReturn(0);
}
try
{
Socket.Select(readSockets, writeSockets, errorSockets, timeoutMicroseconds);
}
catch (ObjectDisposedException)
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
var readyCount = 0;
foreach (var (socket, registration) in hostRegistrations)
{
uint readyEvents = 0;
if (readSockets.Contains(socket))
{
readyEvents |= NetEpollIn;
}
if (writeSockets.Contains(socket))
{
readyEvents |= NetEpollOut;
}
if (errorSockets.Contains(socket))
{
readyEvents |= NetEpollError;
}
if (readyEvents == 0)
{
continue;
}
var outputAddress = eventsAddress + (ulong)(readyCount * NetEpollEventSize);
if (!TryWriteNetEpollEvent(
ctx,
outputAddress,
readyEvents,
registration.SocketId,
registration.Event.Data))
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
readyCount++;
if (readyCount == maxEvents)
{
break;
}
}
TraceNet(
"epoll.wait",
epollId,
unchecked((uint)maxEvents),
unchecked((uint)timeoutMicroseconds),
unchecked((uint)readyCount));
return ctx.SetReturn(readyCount);
}
[SysAbiExport(
Nid = "Inp1lfL+Jdw",
ExportName = "sceNetEpollDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollDestroy(CpuContext ctx)
{
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
lock (EpollGate)
{
if (!_epolls.Remove(epollId))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
Monitor.PulseAll(EpollGate);
}
TraceNet("epoll.destroy", epollId, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "dgJBaeJnGpo",
ExportName = "sceNetPoolCreate",
@ -816,6 +1146,53 @@ public static class NetExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "v6M4txecCuo",
ExportName = "sceNetEtherNtostr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEtherNtostr(CpuContext ctx)
{
const int EtherStringLength = 18;
const string HexDigits = "0123456789abcdef";
var address = ctx[CpuRegister.Rdi];
var destination = ctx[CpuRegister.Rsi];
var destinationLength = ctx[CpuRegister.Rdx];
if (address == 0 || destination == 0 || destinationLength < EtherStringLength)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
Span<byte> macAddress = stackalloc byte[6];
if (!ctx.Memory.TryRead(address, macAddress))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
Span<byte> text = stackalloc byte[EtherStringLength];
for (var index = 0; index < macAddress.Length; index++)
{
var value = macAddress[index];
var offset = index * 3;
text[offset] = unchecked((byte)HexDigits[value >> 4]);
text[offset + 1] = unchecked((byte)HexDigits[value & 0xF]);
if (index != macAddress.Length - 1)
{
text[offset + 2] = (byte)':';
}
}
text[^1] = 0;
if (!ctx.Memory.TryWrite(destination, text))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
TraceNet("ether_ntostr", 0, address, destination, destinationLength);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
@ -1025,6 +1402,39 @@ public static class NetExports
return true;
}
private static bool TryReadNetEpollEvent(
CpuContext ctx,
ulong address,
out NetEpollEvent epollEvent)
{
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
if (!ctx.Memory.TryRead(address, bytes))
{
epollEvent = default;
return false;
}
epollEvent = new NetEpollEvent(
BinaryPrimitives.ReadUInt32LittleEndian(bytes),
BinaryPrimitives.ReadUInt64LittleEndian(bytes[16..]));
return true;
}
private static bool TryWriteNetEpollEvent(
CpuContext ctx,
ulong address,
uint events,
int socketId,
ulong data)
{
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
bytes.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(bytes, events);
BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], unchecked((uint)socketId));
BinaryPrimitives.WriteUInt64LittleEndian(bytes[16..], data);
return ctx.Memory.TryWrite(address, bytes);
}
[SysAbiExport(
Nid = "8Kcp5d-q1Uo",
ExportName = "sceNetInetPton",

View File

@ -0,0 +1,121 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
/// <summary>
/// Minimal reliable-UDP poll object support. SharpEmu does not expose a host
/// RUDP transport yet, but callers still require a distinct non-zero poll ID
/// for their offline/event-processing path.
/// </summary>
public static class RudpExports
{
private const int MaxLivePolls = 4096;
private const ulong PollEventSize = 8;
private const int RudpErrorInvalidArgument = unchecked((int)0x80770004);
private const int RudpErrorInvalidPollId = unchecked((int)0x8077001F);
private const int RudpErrorOutOfMemory = unchecked((int)0x80770003);
private static readonly object PollGate = new();
private static readonly Dictionary<int, uint> PollCapacities = [];
private static int _nextPollId;
[SysAbiExport(
Nid = "MVbmLASjn5M",
ExportName = "sceRudpPollCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRudp")]
public static int RudpPollCreate(CpuContext ctx)
{
var requestedEvents = ctx[CpuRegister.Rdi];
if (requestedEvents is 0 or > uint.MaxValue)
{
return ctx.SetReturn(RudpErrorInvalidArgument);
}
lock (PollGate)
{
if (PollCapacities.Count >= MaxLivePolls)
{
return ctx.SetReturn(RudpErrorOutOfMemory);
}
int pollId;
do
{
_nextPollId = _nextPollId == int.MaxValue ? 1 : _nextPollId + 1;
pollId = _nextPollId;
}
while (PollCapacities.ContainsKey(pollId));
PollCapacities.Add(pollId, unchecked((uint)requestedEvents));
return ctx.SetReturn(pollId);
}
}
[SysAbiExport(
Nid = "M6ggviwXpLs",
ExportName = "sceRudpPollWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRudp")]
public static int RudpPollWait(CpuContext ctx)
{
var pollId = unchecked((int)ctx[CpuRegister.Rdi]);
var events = ctx[CpuRegister.Rsi];
var maxEvents = ctx[CpuRegister.Rdx];
var timeoutMicroseconds = ctx[CpuRegister.Rcx];
if (!TryGetPollCapacity(pollId, out _))
{
return ctx.SetReturn(RudpErrorInvalidPollId);
}
if (events == 0 ||
maxEvents == 0 ||
maxEvents > uint.MaxValue ||
maxEvents > ulong.MaxValue / PollEventSize ||
!ctx.Memory.CanRead(events, maxEvents * PollEventSize))
{
return ctx.SetReturn(RudpErrorInvalidArgument);
}
ctx[CpuRegister.Rax] = 0;
if (timeoutMicroseconds == 0 ||
!GuestThreadExecution.IsGuestThread ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out _))
{
return 0;
}
var timeout = timeoutMicroseconds > (ulong)(TimeSpan.MaxValue.Ticks / 10)
? TimeSpan.MaxValue
: TimeSpan.FromTicks(checked((long)timeoutMicroseconds * 10));
_ = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceRudpPollWait",
$"rudp-poll:{pollId}",
waiter: null,
blockDeadlineTimestamp: GuestThreadExecution.ComputeDeadlineTimestamp(timeout));
return 0;
}
public static void ResetRuntimeState()
{
lock (PollGate)
{
PollCapacities.Clear();
_nextPollId = 0;
}
}
internal static bool TryGetPollCapacity(int pollId, out uint capacity)
{
lock (PollGate)
{
return PollCapacities.TryGetValue(pollId, out capacity);
}
}
internal static void ResetForTests() => ResetRuntimeState();
}

View File

@ -13,15 +13,22 @@ public static class NpAuthExports
private const int NpAuthErrorAborted = unchecked((int)0x80550304);
private const int NpAuthErrorRequestMaximum = unchecked((int)0x80550305);
private const int NpAuthErrorRequestNotFound = unchecked((int)0x80550306);
private const int NpAuthErrorServiceDown = unchecked((int)0x80550401);
private const int NpAuthErrorInvalidArgument = unchecked((int)0x80550301);
private const int NpErrorSignedOut = unchecked((int)0x80550006);
private const int AuthorizationCodeSize = 136;
private const int IdTokenSize = 4104;
private const ulong AsyncParameterSize = 24;
private static readonly ConcurrentDictionary<int, AuthRequest> Requests = new();
private static readonly object RequestGate = new();
private static int _nextRequestId;
private sealed class AuthRequest
private sealed class AuthRequest(bool async)
{
public bool Async { get; } = async;
public bool Completed { get; set; }
public bool Aborted { get; set; }
public int Result { get; set; }
@ -47,7 +54,7 @@ public static class NpAuthExports
ExportName = "sceNpAuthCreateRequest",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx);
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx, async: false);
[SysAbiExport(
ExportName = "sceNpAuthCreateAsyncRequest",
@ -63,7 +70,7 @@ public static class NpAuthExports
return ctx.SetReturn(NpAuthErrorInvalidSize);
}
return CreateRequest(ctx);
return CreateRequest(ctx, async: true);
}
[SysAbiExport(
@ -134,21 +141,23 @@ public static class NpAuthExports
ExportName = "sceNpAuthGetAuthorizationCodeV3",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) => RejectOnlineOperation(ctx);
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) =>
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], AuthorizationCodeSize, ctx[CpuRegister.Rcx]);
[SysAbiExport(
ExportName = "sceNpAuthGetIdTokenV3",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthGetIdTokenV3(CpuContext ctx) => RejectOnlineOperation(ctx);
public static int NpAuthGetIdTokenV3(CpuContext ctx) =>
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], IdTokenSize, 0);
[SysAbiExport(
ExportName = "sceNpAuthGetAuthorizedAppCode",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => RejectOnlineOperation(ctx);
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => CompleteSignedOut(ctx, 0, 0, 0);
private static int CreateRequest(CpuContext ctx)
private static int CreateRequest(CpuContext ctx, bool async)
{
lock (RequestGate)
{
@ -160,7 +169,7 @@ public static class NpAuthExports
while (true)
{
var id = Interlocked.Increment(ref _nextRequestId);
if (Requests.TryAdd(id, new AuthRequest()))
if (Requests.TryAdd(id, new AuthRequest(async)))
{
return ctx.SetReturn(id);
}
@ -168,8 +177,20 @@ public static class NpAuthExports
}
}
private static int RejectOnlineOperation(CpuContext ctx)
private static int CompleteSignedOut(CpuContext ctx, ulong outputAddress, int outputSize, ulong issuerAddress)
{
if (outputAddress != 0 && outputSize != 0)
{
Span<byte> cleared = stackalloc byte[outputSize];
cleared.Clear();
ctx.Memory.TryWrite(outputAddress, cleared);
}
if (issuerAddress != 0)
{
ctx.TryWriteUInt32(issuerAddress, 0);
}
if (!Requests.TryGetValue(unchecked((int)ctx[CpuRegister.Rdi]), out var request))
{
return ctx.SetReturn(NpAuthErrorRequestNotFound);
@ -177,8 +198,21 @@ public static class NpAuthExports
lock (request)
{
request.Result = request.Aborted ? NpAuthErrorAborted : NpAuthErrorServiceDown;
return ctx.SetReturn(request.Result);
if (request.Aborted)
{
request.Result = NpAuthErrorAborted;
return ctx.SetReturn(NpAuthErrorAborted);
}
if (request.Completed)
{
request.Result = NpAuthErrorInvalidArgument;
return ctx.SetReturn(NpAuthErrorInvalidArgument);
}
request.Completed = true;
request.Result = NpErrorSignedOut;
return ctx.SetReturn(request.Async ? 0 : NpErrorSignedOut);
}
}

View File

@ -95,6 +95,26 @@ public static class NpEntitlementAccessExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "y7JD1gfwyfA",
ExportName = "sceNpEntitlementAccessGetGameTrialsFlag",
Target = Generation.Gen5,
LibraryName = "libSceNpEntitlementAccess")]
public static int NpEntitlementAccessGetGameTrialsFlag(CpuContext ctx)
{
var flagAddress = ctx[CpuRegister.Rdi];
if (flagAddress == 0)
{
return ctx.SetReturn(NpEntitlementAccessErrorParameter);
}
Span<byte> flagBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(flagBytes, 0);
return ctx.Memory.TryWrite(flagAddress, flagBytes)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "TFyU+KFBv54",
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",

View File

@ -11,6 +11,8 @@ public static class NpUniversalDataSystemExports
private const int ErrorInvalidArgument = unchecked((int)0x80553102);
private const int MaximumGuestStringBytes = 64 * 1024;
public static void ResetRuntimeState() => NpUniversalDataSystemState.ResetRuntimeState();
[SysAbiExport(
Nid = "sjaobBgqeB4",
ExportName = "sceNpUniversalDataSystemInitialize",

View File

@ -64,9 +64,14 @@ internal static class NpUniversalDataSystemState
{
lock (Gate)
{
ResetLocked();
_initialized = true;
if (!_initialized)
{
ResetLocked();
_initialized = true;
}
_poolSize = poolSize;
UpdateMaximumInUseLocked();
return true;
}
}
@ -85,8 +90,7 @@ internal static class NpUniversalDataSystemState
{
if (!_initialized)
{
context = 0;
return false;
_initialized = true;
}
context = NextPositiveId(ref _nextContext);
@ -480,7 +484,7 @@ internal static class NpUniversalDataSystemState
}
}
internal static void ResetForTests()
internal static void ResetRuntimeState()
{
lock (Gate)
{
@ -488,6 +492,8 @@ internal static class NpUniversalDataSystemState
}
}
internal static void ResetForTests() => ResetRuntimeState();
private static ulong CreateObjectLocked(UdsObjectNode node)
{
var handle = ObjectHandleBase | ++_nextObject;

View File

@ -11,6 +11,8 @@ namespace SharpEmu.Libs.Pad;
public static class PadExports
{
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
private const int OrbisPadErrorInvalidArgument = unchecked((int)0x80920001);
private const int OrbisPadErrorAlreadyOpened = unchecked((int)0x80920004);
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
@ -19,8 +21,10 @@ public static class PadExports
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
// leaving every later keyboard/gamepad read on an invalid handle.
private const int PrimaryUserId = 0x10000000;
private const int SystemUserId = 0xFF;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int SpecialPortType = 2;
private const int RemotePortType = 16;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
private const float StandardGravity = 9.80665f;
@ -31,11 +35,32 @@ public static class PadExports
// user slots and treats every valid handle as a connected controller, so accepting
// 0 there reports phantom pads that repeat the primary pad's buttons.
private static int _padOpened;
private static readonly object PadHandleGate = new();
private static readonly Dictionary<PadOpenKey, int> PadHandleByKey = new();
private static readonly Dictionary<int, PadOpenKey> PadKeyByHandle = new();
private static int _nextPadHandle = 1;
private static bool IsPrimaryPadHandle(int handle) =>
handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(ref _padOpened) == 0);
private readonly record struct PadOpenKey(int UserId, int Type, int Index);
internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0);
private static bool IsOpenPadHandle(int handle)
{
lock (PadHandleGate)
{
return PadKeyByHandle.ContainsKey(handle) || (handle == 0 && _padOpened == 0);
}
}
internal static void ResetOpenedPadForTests()
{
lock (PadHandleGate)
{
PadHandleByKey.Clear();
PadKeyByHandle.Clear();
_nextPadHandle = 1;
_padOpened = 0;
_initialized = false;
}
}
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
[ThreadStatic]
@ -45,6 +70,9 @@ public static class PadExports
private static PadState _cachedInputState;
private static bool _initialized;
private static readonly bool LogPadState =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PAD_STATE"), "1", StringComparison.Ordinal);
private static long _loggedPadSignature = -1;
// Motion data is reported until a title turns it off: Astro Bot reads it for
// shake/tilt without ever importing scePadSetMotionSensorState.
private static int _motionSensorEnabled = 1;
@ -62,6 +90,18 @@ public static class PadExports
return ctx.SetReturn(0);
}
#pragma warning disable SHEM006
[SysAbiExport(
Nid = "n3kSX62fgNo",
ExportName = "scePadUnknownN3kSX62fgNo",
Target = Generation.Gen5,
LibraryName = "libScePad")]
public static int PadUnknownN3kSX62fgNo(CpuContext ctx)
{
return ctx.SetReturn(0);
}
#pragma warning restore SHEM006
[SysAbiExport(
Nid = "xk0AcarP3V4",
ExportName = "scePadOpen",
@ -76,12 +116,6 @@ public static class PadExports
LibraryName = "libScePad")]
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
// scePadGetHandle(userId, type, index): returns the handle of an already-open
// pad without opening a new one. Dead Cells calls it every frame to poll
// input; leaving it unresolved returned a garbage handle so the input path
// (and the game loop that drives it) misbehaved. Same validation as
// scePadOpen — the one primary pad — returning its handle or a not-connected
// error, never opening or logging.
[SysAbiExport(
Nid = "u1GRHp+oWoY",
ExportName = "scePadGetHandle",
@ -97,22 +131,24 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorNotInitialized);
}
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
if (userId == -1)
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
return ctx.SetReturn(PrimaryPadHandle);
lock (PadHandleGate)
{
return PadHandleByKey.TryGetValue(new PadOpenKey(userId, type, index), out var handle)
? ctx.SetReturn(handle)
: ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
}
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
private static int PadOpenCore(CpuContext ctx, bool extended)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized)
{
return ctx.SetReturn(OrbisPadErrorNotInitialized);
@ -123,8 +159,7 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
if (!IsSupportedOpenTuple(userId, type, index, extended))
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
@ -138,8 +173,42 @@ public static class PadExports
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
}
Volatile.Write(ref _padOpened, 1);
return ctx.SetReturn(PrimaryPadHandle);
var key = new PadOpenKey(userId, type, index);
lock (PadHandleGate)
{
if (PadHandleByKey.ContainsKey(key))
{
return ctx.SetReturn(OrbisPadErrorAlreadyOpened);
}
var handle = _nextPadHandle++;
PadHandleByKey.Add(key, handle);
PadKeyByHandle.Add(handle, key);
_padOpened = 1;
return ctx.SetReturn(handle);
}
}
private static bool IsSupportedOpenTuple(int userId, int type, int index, bool extended)
{
if (index != 0)
{
return false;
}
if (userId == SystemUserId && type == RemotePortType)
{
return true;
}
if (userId != PrimaryUserId)
{
return false;
}
return extended
? type is 0 or 1 or 2
: type is StandardPortType or SpecialPortType;
}
[SysAbiExport(
@ -150,9 +219,16 @@ public static class PadExports
public static int PadClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
lock (PadHandleGate)
{
if (!PadKeyByHandle.Remove(handle, out var key))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
PadHandleByKey.Remove(key);
return ctx.SetReturn(0);
}
}
[SysAbiExport(
@ -163,7 +239,7 @@ public static class PadExports
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -180,7 +256,7 @@ public static class PadExports
public static int PadSetTiltCorrectionState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -193,7 +269,7 @@ public static class PadExports
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -207,7 +283,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -258,7 +334,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -272,7 +348,7 @@ public static class PadExports
// (DualSense). We emulate no special peripheral (guitar/drums/wheel), so
// the class-data union stays zeroed — the guest treats it as a plain
// controller with no extended capabilities.
Span<byte> information = stackalloc byte[0x20];
Span<byte> information = stackalloc byte[0x14];
information.Clear();
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
@ -281,6 +357,57 @@ public static class PadExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "IHPqcbc0zCA",
ExportName = "scePadDeviceClassParseData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadDeviceClassParseData(CpuContext ctx)
{
const int deviceClassDataSize = 24;
const int deviceUniqueDataLengthOffset = 0x6B;
const int deviceUniqueDataOffset = 0x6C;
const int maximumDeviceUniqueDataLength = 12;
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var padDataAddress = ctx[CpuRegister.Rsi];
var classDataAddress = ctx[CpuRegister.Rdx];
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (padDataAddress == 0 || classDataAddress == 0)
{
return ctx.SetReturn(OrbisPadErrorInvalidArgument);
}
Span<byte> padData = stackalloc byte[PadDataSize];
if (!ctx.Memory.TryRead(padDataAddress, padData))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Span<byte> classData = stackalloc byte[deviceClassDataSize];
classData.Clear();
classData[0x04] = padData[0x4C] != 0 ? (byte)1 : (byte)0;
var uniqueDataLength = Math.Min(
(int)padData[deviceUniqueDataLengthOffset],
maximumDeviceUniqueDataLength);
if (uniqueDataLength > 0)
{
BinaryPrimitives.WriteInt32LittleEndian(classData[0x00..], -1);
classData[0x08] = (byte)uniqueDataLength;
padData.Slice(deviceUniqueDataOffset, uniqueDataLength)
.CopyTo(classData.Slice(0x0C, uniqueDataLength));
}
return ctx.Memory.TryWrite(classDataAddress, classData)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "YndgXqQVV7c",
ExportName = "scePadReadState",
@ -290,7 +417,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -315,7 +442,7 @@ public static class PadExports
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -349,7 +476,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -387,7 +514,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var stateAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -575,7 +702,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -605,7 +732,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -634,7 +761,7 @@ public static class PadExports
public static int PadResetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -651,7 +778,7 @@ public static class PadExports
public static int PadResetOrientation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -664,7 +791,7 @@ public static class PadExports
public static int PadIsRemoteController(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -765,6 +892,17 @@ public static class PadExports
buttons |= 0x4000;
}
if (LogPadState)
{
var signature = ((ulong)buttons << 8) | ((ulong)(acceptsKeyboardInput ? 1 : 0) << 4) | (uint)gamepadCount;
if (Interlocked.Exchange(ref _loggedPadSignature, (long)signature) != (long)signature)
{
Console.Error.WriteLine(
$"[LOADER][INFO] pad state: focused={acceptsKeyboardInput} gamepads={gamepadCount} buttons=0x{buttons:X8} " +
$"left={leftX},{leftY} thread={Environment.CurrentManagedThreadId}");
}
}
_cachedInputState = new PadState(
Connected: true,
Buttons: buttons,

View File

@ -19,6 +19,7 @@ public static class PlayGoExports
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
private const int OrbisPlayGoErrorBadOptionalType = unchecked((int)0x80B20024);
private const ulong PlayGoInitBufAddrOffset = 0;
private const ulong PlayGoInitBufSizeOffset = 8;
private const uint PlayGoMinimumInitBufferSize = 0x200000;
@ -29,6 +30,10 @@ public static class PlayGoExports
private const int PlayGoInstallSpeedSuspended = 0;
private const int PlayGoInstallSpeedTrickle = 1;
private const int PlayGoInstallSpeedFull = 2;
private const int PlayGoOptionalTypeLanguage = 0;
private const int PlayGoOptionalTypeScenario = 1;
private const ulong PlayGoLanguageMaskAll = ulong.MaxValue;
private const ulong PlayGoScenarioMaskAll = 0x1f;
private const uint MaxPlayGoQueryEntries = 0x4000;
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
@ -231,7 +236,7 @@ public static class PlayGoExports
chunkIds = _metadata.ChunkIds;
}
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
var availableEntries = (uint)chunkIds.Length;
if (outChunkIdList == 0)
{
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
@ -249,7 +254,7 @@ public static class PlayGoExports
for (uint i = 0; i < entriesToWrite; i++)
{
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
var chunkId = chunkIds[i];
if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@ -262,6 +267,91 @@ public static class PlayGoExports
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "8-e7E989rCU",
ExportName = "scePlayGoGetInstallChunkId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetInstallChunkId(CpuContext ctx)
{
return PlayGoGetChunkId(ctx);
}
[SysAbiExport(
Nid = "g4AZyxpSAlA",
ExportName = "scePlayGoGetOptionalChunk",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetOptionalChunk(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var outOption = ctx[CpuRegister.Rdx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outOption == 0)
{
return OrbisPlayGoErrorBadPointer;
}
var option = type switch
{
PlayGoOptionalTypeLanguage => PlayGoLanguageMaskAll,
PlayGoOptionalTypeScenario => PlayGoScenarioMaskAll,
_ => 0UL,
};
if (type is not PlayGoOptionalTypeLanguage and not PlayGoOptionalTypeScenario)
{
return OrbisPlayGoErrorBadOptionalType;
}
return ctx.TryWriteUInt64(outOption, option)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "IfiN+-oeVWI",
ExportName = "scePlayGoGetSupportedOptionalChunk",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetSupportedOptionalChunk(CpuContext ctx)
{
return PlayGoGetOptionalChunk(ctx);
}
[SysAbiExport(
Nid = "HVAa744ecdw",
ExportName = "scePlayGoPrefetchOptionalChunk",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoPrefetchOptionalChunk(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var option = ctx[CpuRegister.Rdx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (option == 0)
{
return OrbisPlayGoErrorBadPointer;
}
return type is PlayGoOptionalTypeLanguage or PlayGoOptionalTypeScenario
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisPlayGoErrorBadOptionalType;
}
[SysAbiExport(
Nid = "v6EZ-YWRdMs",
ExportName = "scePlayGoGetEta",
@ -685,10 +775,7 @@ public static class PlayGoExports
{
// No app0 override to probe for sidecar files: same fully-installed
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
return new PlayGoMetadata(
true,
[(ushort)0],
PlayGoChunkIdKnowledge.Authoritative);
return CreateBaseChunkMetadata(PlayGoChunkIdKnowledge.Authoritative);
}
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
@ -718,14 +805,27 @@ public static class PlayGoExports
}
var chunkIds = LoadChunkIds(chunkDefsXml);
if (chunkIds.Length == 0)
{
TracePlayGo("metadata_has_no_chunk_definitions; installed chunk ids unknown");
return new PlayGoMetadata(
true,
Array.Empty<ushort>(),
PlayGoChunkIdKnowledge.Unknown);
}
return new PlayGoMetadata(
true,
chunkIds,
chunkIds.Length == 0
? PlayGoChunkIdKnowledge.Unknown
: PlayGoChunkIdKnowledge.Authoritative);
PlayGoChunkIdKnowledge.Authoritative);
}
private static PlayGoMetadata CreateBaseChunkMetadata(PlayGoChunkIdKnowledge knowledge) =>
new(
true,
[(ushort)0],
knowledge);
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
// is the base chunk and must resolve even for a title with no pak files at
@ -831,7 +931,7 @@ public static class PlayGoExports
}
}
internal static void ResetForTests()
public static void ResetRuntimeState()
{
lock (_stateGate)
{
@ -846,6 +946,8 @@ public static class PlayGoExports
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
}
internal static void ResetForTests() => ResetRuntimeState();
private enum PlayGoChunkIdKnowledge
{
Unknown,

View File

@ -78,7 +78,7 @@ public static class SaveDataExports
private const uint EventTypeSaveDataMemorySyncEnd = 3;
private const int SaveDataTitleIdStructSize = 0x10;
private const int SaveDataEventSize = 0x68;
private const int MountInfoSize = 0x40;
private const int MountInfoSize = 0x30;
private const uint SaveDataBlockSize = 65536;
private const ulong SaveDataBlocksMax = 16384;

View File

@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Share;
public static class SharePlayExports
{
private static int _controllerProhibited;
[SysAbiExport(
Nid = "FzQS6DREDfk",
ExportName = "sceSharePlayProhibitController",
Target = Generation.Gen5,
LibraryName = "libSceSharePlay")]
public static int SharePlayProhibitController(CpuContext ctx)
{
var prohibited = ctx[CpuRegister.Rdi];
if (prohibited > 1)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Volatile.Write(ref _controllerProhibited, unchecked((int)prohibited));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
public static void ResetRuntimeState() => Volatile.Write(ref _controllerProhibited, 0);
internal static bool IsControllerProhibited => Volatile.Read(ref _controllerProhibited) != 0;
internal static void ResetForTests() => ResetRuntimeState();
}

View File

@ -106,6 +106,14 @@ public static class GameServiceStubs
public static int TextToSpeech2Open(CpuContext ctx) =>
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
[SysAbiExport(Nid = "2jiIxUmcsGo", ExportName = "sceTextToSpeech2Cancel",
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
public static int TextToSpeech2Cancel(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "08JSg9p6bgQ", ExportName = "sceTextToSpeech2GetSpeechStatus",
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
public static int TextToSpeech2GetSpeechStatus(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "kvYEw2lBndk", ExportName = "sceGameLiveStreamingInitialize",
Target = Generation.Gen5, LibraryName = "libSceGameLiveStreaming")]
public static int GameLiveStreamingInitialize(CpuContext ctx) => Ok(ctx);

View File

@ -34,6 +34,17 @@ public static class UserServiceExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bwFjS+bX9mA",
ExportName = "sceUserServiceTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceTerminate(CpuContext ctx)
{
Interlocked.Exchange(ref _loginEventDelivered, 0);
return SetReturnWithTrace(ctx, 0, "terminate");
}
[SysAbiExport(
Nid = "CdWp0oHWGr0",
ExportName = "sceUserServiceGetInitialUser",
@ -144,6 +155,30 @@ public static class UserServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "qbwy0Ub8b3M",
ExportName = "sceUserServiceGetUserNumber",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetUserNumber(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var numberAddress = ctx[CpuRegister.Rsi];
if (numberAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (userId != PrimaryUserId && userId != 1)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
return TryWriteInt32(ctx, numberAddress, 1)
? SetReturnWithTrace(ctx, 0, $"get_user_number user={userId} number=1 out=0x{numberAddress:X16}")
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
#pragma warning disable SHEM006
[SysAbiExport(

View File

@ -276,7 +276,7 @@ internal static unsafe partial class VulkanVideoPresenter
private bool ResolveDccAttachmentClear(ColorAttachment target, out ClearColorValue clearValue)
{
clearValue = default;
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc)
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc || target.Request.Description.Metadata.Range.Size != 0)
{
return false;
}

View File

@ -110,6 +110,8 @@ internal static unsafe partial class VulkanVideoPresenter
DepthCompare: image.DepthCompare,
Atomic: image.Atomic);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, byte> _tracedTextureBindings = new();
// Render-state discovery for one shader image; the view is acquired later with the draw.
private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index)
{
@ -127,11 +129,12 @@ internal static unsafe partial class VulkanVideoPresenter
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
BindImage(imageIdentifier, storage);
var descriptor = new TextureDescriptorWords(words);
if (ShouldTraceTextureBindings())
if (ShouldTraceTextureBindings() &&
(!_traceVolumeTextureBindingsOnly || image.Dimension == ImageDimension.Dim3D))
{
var cached = _imageCache.GetImage(imageIdentifier);
var description = cached.Description;
Console.Error.WriteLine(
var line =
$"TextureBinding stage={program.Stage} hash=0x{program.Hash:X16} index={index} " +
$"address=0x{new TextureDescriptorWords(words).BaseAddress:X16} " +
$"descriptor={descriptor.BaseAddress:X16} size={descriptor.Width + 1}x{descriptor.Height + 1} " +
@ -139,7 +142,14 @@ internal static unsafe partial class VulkanVideoPresenter
$"image=0x{description.Data.Address:X16} size=0x{description.Data.Size:X} " +
$"extent={description.Extent.Width}x{description.Extent.Height} pitch={description.Pitch} " +
$"guestFormat={(uint)description.GuestFormat} imageTile={(uint)description.TileMode} " +
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format}");
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format} " +
$"depth={descriptor.Depth + 1} type={descriptor.Type} dcc={descriptor.MetadataCompress} " +
$"metadata=0x{descriptor.MetadataAddress << 8:X} metaKind={description.Metadata.Kind} " +
$"words={string.Join(',', words.Select(static word => word.ToString("X8")))}";
if (!_traceVolumeTextureBindingsOnly || _tracedTextureBindings.TryAdd(line, 0))
{
Console.Error.WriteLine(line);
}
}
return new TextureResource
{

View File

@ -45,6 +45,7 @@ internal static unsafe partial class VulkanVideoPresenter
private ulong _minStorageBufferOffsetAlignment = 1;
private bool _supportsIndependentBlend;
private bool _supportsDepthBiasClamp;
private bool _supportsShaderSignedZeroInfNanPreserveFloat32;
private uint _maxColorAttachments;
private Device _device;
private PipelineCache _pipelineCache;
@ -583,10 +584,15 @@ internal static unsafe partial class VulkanVideoPresenter
{
SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr,
};
var floatControls = new PhysicalDeviceFloatControlsProperties
{
SType = StructureType.PhysicalDeviceFloatControlsProperties,
PNext = &pushDescriptorProperties,
};
var subgroupSizeControl = new PhysicalDeviceSubgroupSizeControlProperties
{
SType = StructureType.PhysicalDeviceSubgroupSizeControlProperties,
PNext = &pushDescriptorProperties,
PNext = &floatControls,
};
var subgroup = new PhysicalDeviceSubgroupProperties
{
@ -599,6 +605,8 @@ internal static unsafe partial class VulkanVideoPresenter
PNext = &subgroup,
};
_vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2);
_supportsShaderSignedZeroInfNanPreserveFloat32 =
floatControls.ShaderSignedZeroInfNanPreserveFloat32;
SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages);
_canRequireComputeSubgroup32 =
subgroup.SubgroupSize != RdnaSubgroupSize &&
@ -657,6 +665,9 @@ internal static unsafe partial class VulkanVideoPresenter
_ when GraphicsSubgroupOperationsEnabled => "auto-native-wave32",
_ => "auto-fallback",
};
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan shaderSignedZeroInfNanPreserveFloat32 " +
$"enabled={_supportsShaderSignedZeroInfNanPreserveFloat32}");
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan graphics subgroup operations " +
$"enabled={GraphicsSubgroupOperationsEnabled} " +

View File

@ -91,10 +91,9 @@ internal static unsafe partial class VulkanVideoPresenter
"1",
StringComparison.Ordinal);
private static readonly bool _traceTextureBindingsEnabled =
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS"),
"1",
StringComparison.Ordinal);
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") is "1" or "volume";
private static readonly bool _traceVolumeTextureBindingsOnly =
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") == "volume";
private static readonly uint _traceGuestImageWidth =
uint.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_WIDTH"),

View File

@ -87,6 +87,8 @@ internal static unsafe partial class VulkanVideoPresenter
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
bool IShaderPipelineHost.ShaderSignedZeroInfNanPreserveFloat32Supported =>
_supportsShaderSignedZeroInfNanPreserveFloat32;
// NVIDIA's compiler rejects the elided-EXEC wave64 compute module with NVVM error 3.
bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId;
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;

View File

@ -574,7 +574,8 @@ internal static unsafe partial class VulkanVideoPresenter
const byte DccClearToZero = 0x00;
foreach (var binding in bindings)
{
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc)
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc ||
image.Description.Metadata.Range.Size != 0)
{
continue;
}

View File

@ -162,7 +162,8 @@ internal static unsafe partial class VulkanVideoPresenter
bool execGuardElision)
{
if (!ShaderProgramCache.TryCompilePrewarm(
record, code, compiler, sharedInt64Atomics, execGuardElision, out var compiled, out var layout, out var error))
record, code, compiler, sharedInt64Atomics, execGuardElision,
_supportsShaderSignedZeroInfNanPreserveFloat32, out var compiled, out var layout, out var error))
{
NoteShaderPrewarmFailure(record, error);
return;

View File

@ -442,6 +442,15 @@ public static partial class Gen5SpirvTranslator
break;
}
case "VRcpF32":
result = EmitFloatResult(
instruction,
_module.AddInstruction(
SpirvOp.FDiv,
_floatType,
Float(1),
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VRcpIflagF32":
result = EmitFloatResult(
instruction,
@ -454,7 +463,11 @@ public static partial class Gen5SpirvTranslator
case "VLogF32":
result = EmitFloatResult(
instruction,
Ext(30, _floatType, GetFloatSource(instruction, 0)));
Ext(
30,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VLdexpF32":
result = EmitFloatResult(
@ -468,12 +481,20 @@ public static partial class Gen5SpirvTranslator
case "VExpF32":
result = EmitFloatResult(
instruction,
Ext(29, _floatType, GetFloatSource(instruction, 0)));
Ext(
29,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VRsqF32":
result = EmitFloatResult(
instruction,
Ext(32, _floatType, GetFloatSource(instruction, 0)));
Ext(
32,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VRcpF16":
result = EmitFloat16Result(instruction, destination, _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1), GetFloat16Source(instruction, 0)));
@ -542,7 +563,11 @@ public static partial class Gen5SpirvTranslator
case "VSqrtF32":
result = EmitFloatResult(
instruction,
Ext(31, _floatType, GetFloatSource(instruction, 0)));
Ext(
31,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VSinF32":
result = EmitFloatResult(
@ -553,7 +578,9 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
EmitTrigCycleF32(
GetFloatSource(instruction, 0),
preserveSignedZero: true),
Float(MathF.Tau))));
break;
case "VCosF32":
@ -565,7 +592,9 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
EmitTrigCycleF32(
GetFloatSource(instruction, 0),
preserveSignedZero: false),
Float(MathF.Tau))));
break;
case "VAddF32":

View File

@ -0,0 +1,91 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.ShaderCompiler.Vulkan;
public static partial class Gen5SpirvTranslator
{
private sealed partial class CompilationContext
{
private uint EmitFlushF32DenormToSignedZero(uint value)
{
var bits = Bitcast(_uintType, value);
var absolute = _module.AddInstruction(
SpirvOp.BitwiseAnd,
_uintType,
bits,
UInt(0x7FFF_FFFF));
var sign = _module.AddInstruction(
SpirvOp.BitwiseAnd,
_uintType,
bits,
UInt(0x8000_0000));
var nonZero = IsNotZero(absolute);
var subnormal = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
absolute,
UInt(0x0080_0000));
var flush = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
nonZero,
subnormal);
var selected = _module.AddInstruction(
SpirvOp.Select,
_uintType,
flush,
sign,
bits);
return Bitcast(_floatType, selected);
}
private uint EmitTrigCycleF32(uint source, bool preserveSignedZero)
{
var fraction = Ext(10, _floatType, source);
var bits = Bitcast(_uintType, source);
var absolute = _module.AddInstruction(
SpirvOp.BitwiseAnd,
_uintType,
bits,
UInt(0x7FFF_FFFF));
var large = _module.AddInstruction(
SpirvOp.UGreaterThanEqual,
_boolType,
absolute,
UInt(0x4B00_0000));
var finite = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
absolute,
UInt(0x7F80_0000));
var largeFinite = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
large,
finite);
var reduced = _module.AddInstruction(
SpirvOp.Select,
_floatType,
largeFinite,
Float(0),
fraction);
if (!preserveSignedZero)
{
return reduced;
}
var zero = _module.AddInstruction(
SpirvOp.IEqual,
_boolType,
absolute,
UInt(0));
return _module.AddInstruction(
SpirvOp.Select,
_floatType,
zero,
source,
reduced);
}
}
}

View File

@ -518,6 +518,13 @@ public static partial class Gen5SpirvTranslator
_ => SpirvExecutionModel.GLCompute,
};
_module.AddEntryPoint(model, main, "main", _interfaces);
if (_request.ShaderSignedZeroInfNanPreserveFloat32Supported)
{
_module.AddExecutionMode(
main,
SpirvExecutionMode.SignedZeroInfNanPreserve,
32);
}
if (_stage == Gen5SpirvStage.Pixel)
{
_module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
@ -555,6 +562,11 @@ public static partial class Gen5SpirvTranslator
private void DeclareModule()
{
_module.AddCapability(SpirvCapability.Shader);
if (_request.ShaderSignedZeroInfNanPreserveFloat32Supported)
{
_module.AddExtension("SPV_KHR_float_controls");
_module.AddCapability(SpirvCapability.SignedZeroInfNanPreserve);
}
_module.AddCapability(SpirvCapability.Int64);
_module.AddCapability(SpirvCapability.ImageQuery);
if (UsesSubgroupOperations())

View File

@ -213,6 +213,7 @@ public enum SpirvCapability : uint
ShaderViewportIndexLayerExt = 5254,
RuntimeDescriptorArray = 5302,
PhysicalStorageBufferAddresses = 5347,
SignedZeroInfNanPreserve = 4466,
}
public enum SpirvStorageClass : uint
@ -242,6 +243,7 @@ public enum SpirvExecutionMode : uint
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
SignedZeroInfNanPreserve = 4461,
}
public enum SpirvDecoration : uint

View File

@ -15,6 +15,7 @@ public enum TilerBlockShape
Prt64KB3D,
RenderTarget64KB,
Depth64KB,
RenderTarget64KBGen5,
}
// Assembles the tiler compute shaders: block copies per shape and element size,
@ -251,6 +252,15 @@ public static class TilerShaders
return offset;
}
private static Term[] Gen5RenderTargetTerms(uint bytes) => bytes switch
{
1 => [Y(2, 0x008), Y(4, 0x010), Y(3, 0x0a0), Y(5, 0xf00), Y(6, 0x1000), Y(7, 0x4000), X(0, 0x007), X(3, 0x040), X(5, 0x300), X(4, 0x400), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
2 => [Y(4, 0x070), Y(5, 0xf00), Y(8, 0x5000), X(1, 0x00e), X(4, 0x480), X(5, 0x300), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
4 => [Y(4, 0x070), Y(5, 0xf00), Y(9, 0x1000), Y(8, 0x4000), X(2, 0x00c), X(5, 0x380), X(4, 0x400), X(6, 0x800), X(9, 0xa000)],
8 => [Y(4, 0x010), Y(6, 0x080), Y(5, 0xf00), Y(10, 0x5000), X(3, 0x008), X(4, 0x460), X(5, 0x300), X(6, 0x800), X(10, 0x2000), X(9, 0x8000)],
_ => [X(4, 0x410), X(5, 0x340), X(6, 0x800), X(11, 0xa000), Y(5, 0xf20), Y(6, 0x080), Y(10, 0x1000), Y(11, 0x4000)],
};
private static Term[] DepthTerms(uint bytes) => bytes switch
{
1 => [X(0, 1), X(1, 0x004), X(2, 0x010), X(3, 0x040), X(5, 0x300), X(4, 0x400), X(6, 0x800), X(7, 0x2000), X(8, 0x8000), Y(1, 0x002), Y(2, 0x008), Y(3, 0x0a0), Y(5, 0xf00), Y(6, 0x1000), Y(7, 0x4000)],
@ -314,7 +324,7 @@ public static class TilerShaders
TilerBlockShape.Standard256B => (ThinExtent(256, bytes, 16, 8, 4), 256),
TilerBlockShape.Standard4KB => (ThinExtent(4096, bytes, 64, 32, 16), 4096),
TilerBlockShape.Standard4KB3D => (Thick4KBExtent(bytes), 4096),
TilerBlockShape.Standard64KB or TilerBlockShape.Prt64KB or TilerBlockShape.RenderTarget64KB => (ThinExtent(65536, bytes, 256, 128, 64), 65536),
TilerBlockShape.Standard64KB or TilerBlockShape.Prt64KB or TilerBlockShape.RenderTarget64KB or TilerBlockShape.RenderTarget64KBGen5 => (ThinExtent(65536, bytes, 256, 128, 64), 65536),
TilerBlockShape.Standard64KB3D or TilerBlockShape.Prt64KB3D => (Thick64KBExtent(bytes), 65536),
TilerBlockShape.Depth64KB => (ThinExtent(65536, bytes, 256, 128, 128), 65536),
_ => throw new ArgumentOutOfRangeException(nameof(shape)),
@ -348,6 +358,8 @@ public static class TilerShaders
}
case TilerBlockShape.RenderTarget64KB:
return shaderModule.Xor(RenderTargetOffset(shaderModule, x, y, bytes), ZSpread(shaderModule, z));
case TilerBlockShape.RenderTarget64KBGen5:
return shaderModule.Xor(XorTerms(shaderModule, x, y, z, Gen5RenderTargetTerms(bytes)), ZSpread(shaderModule, z));
case TilerBlockShape.Depth64KB:
return shaderModule.Xor(ZSpread(shaderModule, z), XorTerms(shaderModule, x, y, z, DepthTerms(bytes)));
default:

View File

@ -171,6 +171,7 @@ public sealed class ShaderCompileRequest
// 64-bit atomics are emitted as real 64-bit atomics instead of a pair of
// 32-bit ones, which is not atomic as a pair.
public bool SupportsSharedInt64Atomics { get; init; }
public bool ShaderSignedZeroInfNanPreserveFloat32Supported { get; init; }
public Gen5ComputeSystemRegisters? ComputeSystemRegisters { get; init; }
public IReadOnlyList<Gen5PixelOutputBinding> PixelOutputs { get; init; } = [];

View File

@ -0,0 +1,57 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class GuestExceptionContextLayoutTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ExceptionContext_UsesBareMcontextAndSonyStackAlias(bool saved)
{
const ulong address = 0x1_0000_0100;
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
var context = new CpuContext(memory, Generation.Gen5)
{
Rip = 0x101000,
Rflags = 0x246,
FsBase = 0x303000,
GsBase = 0x404000,
};
context[CpuRegister.Rdi] = 0x11;
context[CpuRegister.Rbx] = 0x22;
context[CpuRegister.Rsp] = 0x202000;
var continuation = saved ? new GuestCpuContinuation
{
Rip = 0x505000,
Rsp = 0x606000,
Rflags = 0x203,
Rdi = 0x33,
Rbx = 0x44,
FsBase = 0x707000,
GsBase = 0x808000,
} : default;
Assert.True(DirectExecutionBackend.TryWriteGuestExceptionContext(context, address, continuation, 0x500));
var bytes = new byte[0x500];
Assert.True(memory.TryRead(address, bytes));
Assert.Equal(saved ? continuation.Rdi : context[CpuRegister.Rdi], Read64(0x08));
Assert.Equal(saved ? continuation.Rbx : context[CpuRegister.Rbx], Read64(0x40));
Assert.Equal(saved ? continuation.Rip : context.Rip, Read64(0xA0));
Assert.Equal(saved ? continuation.Rflags : context.Rflags, Read64(0xB0));
Assert.Equal(saved ? continuation.Rsp : context[CpuRegister.Rsp], Read64(0xB8));
Assert.Equal(Read64(0xB8), Read64(0xF8));
Assert.Equal(0x480UL, Read64(0xC8));
Assert.Equal(saved ? continuation.FsBase : context.FsBase, Read64(0x440));
Assert.Equal(saved ? continuation.GsBase : context.GsBase, Read64(0x448));
ulong Read64(int offset) => BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)));
}
}

View File

@ -78,7 +78,7 @@ internal static class TilerCases
transfer.Tail = true;
break;
case Shape.Layered:
if (kind is not (TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB))
if (kind is not (TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KBGen5))
{
return null;
}
@ -203,7 +203,7 @@ public sealed class GpuTilerTests : IClassFixture<HeadlessVulkanFixture>
public void CpuTwinCases_CoverEveryKindAndElementSize()
{
var cases = TilerCases.All().ToList();
Assert.Equal(44 + 39 + 9, cases.Count);
Assert.Equal((44 + 39 + 9) + 15, cases.Count);
Assert.All(cases, tilerCase => Assert.True(tilerCase.Transfer.SurfaceZ != 0 || tilerCase.Tiled.Length == (int)tilerCase.Transfer.TiledSize));
}

View File

@ -100,31 +100,31 @@ public sealed partial class GuestImageCacheTests
}
[Fact]
public void DccFills_AreAbsorbedOnlyByRegisteredDcc()
public void DccFills_RemainGuestProducersUntilDescriptorDiscovery()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new FatalScope();
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x20000, ReadWrite);
var metadata = address + 0x10000;
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0x20202020u));
harness.Write(metadata, Bytes(0x11223344u));
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0x20202020u));
Assert.False(harness.Images.IsMetadata(metadata));
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0x40404040u));
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0x40404040u));
harness.Write(address + 0xc000, Bytes(0x55667788u));
var target = LinearRequest(address + 0xc000, 4, Format.R8G8B8A8Srgb, GuestPixelFormat.Bits8_8_8_8Srgb, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
target = AsColorTarget(target);
target.Description.Metadata.Kind = MetadataKind.Dcc;
target.Description.Metadata.Range = new GuestSpan(metadata, 0x100);
target.Description.Metadata.Range = new GuestSpan(metadata, 0x1000);
target.Description.Metadata.Compression = DisplayCompression.Dcc256_256_0;
var targetId = harness.Acquire(ref target);
Assert.True(harness.Images.IsMetadata(metadata));
Assert.True(harness.Images.IsMetadataCleared(metadata, 0, out var fill));
Assert.Equal(0x40404040u, fill);
Assert.True(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0x01010101u));
Assert.False(harness.Images.IsMetadata(metadata));
Assert.False(harness.Images.IsMetadataCleared(metadata, 0));
Assert.True(harness.Images.TryAbsorbDccFill(metadata, 0x100, 0xc0c0c0c0u));
Assert.True(harness.Images.IsMetadataCleared(metadata, 5));
Assert.Equal(0x11223344u, harness.ReadUInt32(metadata));
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0x01010101u));
Assert.False(harness.Images.TryAbsorbDccFill(metadata, 0x1000, 0xc0c0c0c0u));
Assert.False(harness.Images.IsMetadataCleared(metadata, 5));
Assert.Equal(0x11223344u, harness.ReadUInt32(metadata));
Assert.False(harness.Images.ClearMetadata(metadata));
// A compressed target rejects readback and read claims but keeps GPU ownership.
@ -144,15 +144,169 @@ public sealed partial class GuestImageCacheTests
Assert.False(videoImage.Binding.NeedsRebind);
Assert.False(videoImage.Uses.VideoOut);
// A depth target must not reuse DCC metadata.
var depth = LinearRequest(address + 0xe000, 4, Format.D32Sfloat, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
depth = AsDepthTarget(depth, Format.D32Sfloat);
depth.Description.Metadata.Kind = MetadataKind.Htile;
depth.Description.Metadata.Range = new GuestSpan(metadata, 0x100);
var depthId = harness.Find(ref depth);
var depthLocal = depth;
harness.Worker.Run(() => Assert.Throws<SchedulerFatalException>(() => harness.Images.AcquireDepthTargetView(depthId, depthLocal)));
Assert.Contains(fatal.Messages, message => message.Contains("depth target reuses metadata that is not HTile"));
harness.Worker.Run(() => Assert.NotEqual(
0UL,
harness.Images.AcquireDepthTargetView(depthId, depthLocal).Handle));
Assert.True(harness.Images.IsMetadata(metadata));
harness.Shutdown();
}
[Fact]
public void DccVolumeOverThirtyTwoSlices_MaterializesAndConsumesEveryUniformSlice()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x80000, ReadWrite);
var imageAddress = address + 0x1000;
var metadataAddress = address + 0x10000;
const uint depth = 64;
const ulong metadataSliceSize = 0x1000;
var metadataBytes = new byte[checked((int)(depth * metadataSliceSize))];
Array.Fill(metadataBytes, (byte)0xc0);
harness.Write(metadataAddress, metadataBytes);
const uint width = 4;
const uint height = 4;
var imageSize = (ulong)width * height * depth * 4;
var target = LinearRequest(
imageAddress,
imageSize,
Format.R8G8B8A8Unorm,
GuestPixelFormat.Bits8_8_8_8UNorm,
GuestImageType.Color3D,
new Extent3D(width, height, depth),
1,
4,
1);
target = AsColorTarget(target);
target.View = target.View with
{
Type = ImageViewType.Type2DArray,
BaseLayer = 0,
LayerCount = depth,
};
target.Description.Metadata.Kind = MetadataKind.Dcc;
target.Description.Metadata.Range = new GuestSpan(metadataAddress, depth * metadataSliceSize);
target.Description.Metadata.DccAlphaMsb = true;
Assert.False(harness.Images.TryAbsorbDccFill(
metadataAddress,
depth * metadataSliceSize,
0xc0c0c0c0u));
var targetId = harness.Find(ref target);
harness.Finish();
Assert.True(targetId.IsValid);
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress));
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress + 31 * metadataSliceSize));
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress + 32 * metadataSliceSize));
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress + (depth - 1) * metadataSliceSize));
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 0));
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 31));
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 32));
harness.Shutdown();
}
[Fact]
public void SelflossShapeDccVolume_HandlesOneHundredTwentyEightSlicesIndependently()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x140000, ReadWrite);
var imageAddress = address + 0x1000;
var metadataAddress = address + 0x20000;
const uint depth = 128;
const ulong metadataSliceSize = 0x2000;
var metadataBytes = new byte[checked((int)(depth * metadataSliceSize))];
Array.Fill(metadataBytes, (byte)0xc0);
const uint mixedSlice = 73;
Array.Fill(
metadataBytes,
(byte)0x55,
checked((int)(mixedSlice * metadataSliceSize + 1)),
checked((int)metadataSliceSize - 1));
harness.Write(metadataAddress, metadataBytes);
const uint width = 4;
const uint height = 4;
var imageSize = (ulong)width * height * depth * 8;
var texture = LinearRequest(
imageAddress,
imageSize,
Format.R16G16B16A16Sfloat,
GuestPixelFormat.Bits16_16_16_16Float,
GuestImageType.Color3D,
new Extent3D(width, height, depth),
1,
8,
1);
texture.Description.Metadata.Kind = MetadataKind.Dcc;
texture.Description.Metadata.Range = new GuestSpan(
metadataAddress,
depth * metadataSliceSize);
texture.Description.Metadata.DccAlphaMsb = true;
var textureId = harness.Find(ref texture);
harness.Finish();
Assert.True(textureId.IsValid);
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress));
Assert.Equal(
0x555555c0u,
harness.ReadUInt32(metadataAddress + mixedSlice * metadataSliceSize));
Assert.Equal(
uint.MaxValue,
harness.ReadUInt32(metadataAddress + (depth - 1) * metadataSliceSize));
Assert.False(harness.Images.IsMetadata(metadataAddress));
harness.Shutdown();
}
[Fact]
public void DccMetadataScan_RejectsAMismatchBeyondTheFirstPooledChunk()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x50000, ReadWrite);
var imageAddress = address + 0x1000;
var metadataAddress = address + 0x10000;
const uint layers = 2;
const ulong metadataSliceSize = 0x11000;
var metadataBytes = new byte[checked((int)(layers * metadataSliceSize))];
Array.Fill(metadataBytes, (byte)0xc0);
metadataBytes[^1] = 0x55;
harness.Write(metadataAddress, metadataBytes);
const uint width = 4;
const uint height = 4;
var texture = LinearRequest(
imageAddress,
(ulong)width * height * layers * 4,
Format.R8G8B8A8Unorm,
GuestPixelFormat.Bits8_8_8_8UNorm,
GuestImageType.Color2D,
new Extent3D(width, height, 1),
layers,
4,
1);
texture.Description.Metadata.Kind = MetadataKind.Dcc;
texture.Description.Metadata.Range = new GuestSpan(
metadataAddress,
layers * metadataSliceSize);
texture.Description.Metadata.DccAlphaMsb = true;
var textureId = harness.Find(ref texture);
harness.Finish();
Assert.True(textureId.IsValid);
Assert.Equal(uint.MaxValue, harness.ReadUInt32(metadataAddress));
Assert.Equal(0xc0c0c0c0u, harness.ReadUInt32(metadataAddress + metadataSliceSize));
Assert.Equal(0x55, harness.Read(metadataAddress + 2 * metadataSliceSize - 1, 1)[0]);
harness.Shutdown();
}
@ -196,8 +350,10 @@ public sealed partial class GuestImageCacheTests
harness.Shutdown();
}
[Fact]
public void ATextureRequestWithDcc_GivesAnImageFoundWithoutMetadataItsDccMetadata()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DccDescriptor_SynchronizesMetadataOnAnImageFoundWithoutIt(bool native)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
@ -220,14 +376,15 @@ public sealed partial class GuestImageCacheTests
var compressed = plain;
compressed.Description.TileMode = GuestTileMode.RenderTarget;
compressed.Description.Metadata.Kind = MetadataKind.Dcc;
compressed.Description.Metadata.Range = new GuestSpan(metadata, 0);
compressed.Description.Metadata.Range = new GuestSpan(metadata, native ? SliceSize : 0);
Assert.Equal(imageId, harness.Find(ref compressed));
Assert.Equal(MetadataKind.Dcc, image.Description.Metadata.Kind);
Assert.Equal(metadata, image.Description.Metadata.Range.Address);
Assert.Equal(SliceSize, image.Description.DccSliceSize);
Assert.False(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize));
Assert.True(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out var code));
Assert.Equal(0x00, code);
harness.Finish();
Assert.Equal(native ? uint.MaxValue : 0u, harness.ReadUInt32(metadata));
Assert.Equal(!native, harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out _));
harness.Shutdown();
}

View File

@ -154,6 +154,21 @@ public sealed class ImageDescriptionTests
Assert.False(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Unsupported, renderTarget: true, gpuModified: true, guestModified: false));
}
[Theory]
[InlineData(true, 0.0f, 1.0f)]
[InlineData(false, 1.0f, 0.0f)]
public void DccFixedClear_RespectsTheAlphaPosition(bool alphaMsb, float red, float alpha)
{
var metadata = new MetadataDescription { DccAlphaMsb = alphaMsb };
Assert.True(PackedClearValue.TryDecodeDccColor(
Format.A2B10G10R10UnormPack32, 0x40404040, metadata, false, out var clear));
Assert.Equal((red, 0.0f, 0.0f, alpha), (clear.Float32_0, clear.Float32_1, clear.Float32_2, clear.Float32_3));
Assert.False(PackedClearValue.TryDecodeDccColor(
Format.A2B10G10R10UnormPack32, 0x40400040, metadata, false, out _));
Assert.False(PackedClearValue.TryDecodeDccColor(
Format.A2B10G10R10UnormPack32, 0xffffffff, metadata, false, out _));
}
[Fact]
public void PackedClears_DecodePerFormat()
{

View File

@ -74,21 +74,40 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
}
[Theory]
[InlineData(GuestImageType.Color2D, 1u, false)]
[InlineData(GuestImageType.Color2D, 1u, true)]
[InlineData(GuestImageType.Color3D, 32u, false)]
[InlineData(GuestImageType.Color3D, 32u, true)]
[InlineData(GuestImageType.Color2DArray, 4u, false)]
public void CompressedTexture_CarriesDccMetadataOnlyForSingleLayer2D(GuestImageType type, uint layers, bool expected)
[InlineData(GuestImageType.Color2DArray, 4u, true)]
public void CompressedTexture_CarriesDccMetadataForEverySlice(GuestImageType type, uint layers, bool storage)
{
const ulong metadata = 0x1_2000_0000;
var words = RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8_8_8UNorm, 32, 32, type, GuestTileMode.RenderTarget, layers: layers);
words[6] |= (1u << 21) | (uint)(((metadata >> 8) & 0xFF) << 24);
words[7] = (uint)(metadata >> 16);
var shape = Sampled2D with { Volume = type == GuestImageType.Color3D, Arrayed = type == GuestImageType.Color2DArray };
var shape = Sampled2D with { Volume = type == GuestImageType.Color3D, Arrayed = type == GuestImageType.Color2DArray, Storage = storage };
var description = ImageRequestBuilders.Texture(words, shape).Request.Description;
Assert.Equal(expected ? MetadataKind.Dcc : MetadataKind.None, description.Metadata.Kind);
Assert.Equal(expected ? metadata : 0UL, description.Metadata.Range.Address);
Assert.Equal(MetadataKind.Dcc, description.Metadata.Kind);
Assert.Equal(metadata, description.Metadata.Range.Address);
Assert.Equal((ulong)layers * 0x1000, description.Metadata.Range.Size);
}
[Fact]
public void DemonSlayerColorLut_CarriesItsVolumeDccFootprintAndAlphaPosition()
{
uint[] words = [0x606D4E00, 0xC3200000, 0x0007C007, 0xA1B00FAC, 0x0000001F, 0x00700000, 0x006B0000, 0x00606D6E];
var description = ImageRequestBuilders.Texture(words, Sampled2D with { Volume = true }).Request.Description;
Assert.Equal(GuestImageType.Color3D, description.Type);
Assert.Equal(new Extent3D(32, 32, 32), description.Extent);
Assert.Equal(Format.A2B10G10R10UnormPack32, description.PixelFormat);
Assert.Equal(MetadataKind.Dcc, description.Metadata.Kind);
Assert.Equal(0x606D6E0000UL, description.Metadata.Range.Address);
Assert.Equal(0x20000UL, description.Metadata.Range.Size);
Assert.True(description.Metadata.DccAlphaMsb);
}
private readonly HeadlessVulkan? _vulkan;
@ -221,6 +240,21 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
Assert.Equal(0u, ImageRequestBuilders.SampleCount(4));
}
[Fact]
public void ColorTarget_LinearTargetPitchMatchesTheLinearTexturePitch()
{
var target = ImageRequestBuilders.ColorTarget(RegisterWords.Color(Base, 816, 604), 0xF, 0, false);
var texture = ImageRequestBuilders.Texture(RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8_8_8UNorm, 816, 604), Sampled2D);
Assert.NotNull(target);
var description = target.Value.Request.Description;
Assert.Equal(new Extent3D(816, 604, 1), description.Extent);
Assert.Equal(832u, description.Pitch);
Assert.Equal(832UL * 604 * 4, description.Data.Size);
Assert.Equal(texture.Request.Description.Pitch, description.Pitch);
Assert.Equal(texture.Request.Description.Data.Size, description.Data.Size);
}
[Fact]
public void Texture_LinearTwoDimensionalFromDescriptor()
{

View File

@ -97,7 +97,7 @@ public sealed class TextureTransferLayoutTests
var target = TextureTransferLayout.Compute(GuestPixelFormat.Bits8_8_8_8UNorm, 128, 128, 1, 2, GuestTileMode.RenderTarget, 0, false, false, "test");
Assert.True(target.TryBuildTileTransfers(2 * 65536, target.BuildCopies(), 1, out var targetTransfers));
Assert.Equal(1u, targetTransfers[1].SurfaceZ);
Assert.Equal(TileBlockKind.RenderTarget64KB, targetTransfers[1].Kind);
Assert.Equal(TileBlockKind.RenderTarget64KBGen5, targetTransfers[1].Kind);
}
[Fact]

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.Gpu.Images;
@ -39,6 +40,7 @@ public sealed class TileGeometryTests
[InlineData(TileBlockKind.Prt64KB3D, 65536u, 16u)]
[InlineData(TileBlockKind.RenderTarget64KB, 65536u, 16u)]
[InlineData(TileBlockKind.Depth64KB, 65536u, 8u)]
[InlineData(TileBlockKind.RenderTarget64KBGen5, 65536u, 16u)]
public void BlockLayouts_CoverEveryElementSize(TileBlockKind kind, uint blockSize, uint maxBytes)
{
for (uint bytes = 1; bytes <= 16; bytes *= 2)
@ -107,6 +109,83 @@ public sealed class TileGeometryTests
}
}
[Theory]
[InlineData(1u, 0x0B75u, 0x8000u, 0x4000u)]
[InlineData(2u, 0x0B7Au, 0x8000u, 0x4100u)]
[InlineData(4u, 0x0BF4u, 0x8100u, 0x4100u)]
[InlineData(8u, 0x1BD8u, 0x4100u, 0x8800u)]
[InlineData(16u, 0x3BB0u, 0x4400u, 0x8800u)]
public void RenderTargetBlockOffsets_MatchRbPlusReferenceSpotValues(
uint bytes,
uint elementOffset,
uint xBlockXor,
uint yBlockXor)
{
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KB, bytes, out var layout));
Assert.True(TileGeometry.TryGetBlockOffset(layout, 37, 23, 0, out var offset));
Assert.Equal(elementOffset, offset);
Assert.True(TileGeometry.TryGetBlockXor(layout, 1, 0, out var blockX));
Assert.Equal(xBlockXor, blockX);
Assert.True(TileGeometry.TryGetBlockXor(layout, 0, 1, out var blockY));
Assert.Equal(yBlockXor, blockY);
}
[Theory]
[InlineData(1u, 0x0b75u, 0x0abdu)]
[InlineData(2u, 0x0b7au, 0x1a7au)]
[InlineData(4u, 0x0bf4u, 0x0af4u)]
[InlineData(8u, 0x1bd8u, 0x5ad8u)]
[InlineData(16u, 0x3bb0u, 0x3ab0u)]
public void Gen5RenderTargetBlockOffsets_MatchReferenceAndDifferFromRbPlus(
uint bytes,
uint expectedRbPlus,
uint expectedGen5)
{
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KB, bytes, out var rbPlus));
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KBGen5, bytes, out var gen5));
Assert.True(TileGeometry.TryGetBlockOffset(rbPlus, 37, 23, 0, out var rbPlusOffset));
Assert.True(TileGeometry.TryGetBlockOffset(gen5, 37, 23, 0, out var gen5Offset));
Assert.Equal(expectedRbPlus, rbPlusOffset);
Assert.Equal(expectedGen5, gen5Offset);
Assert.NotEqual(rbPlusOffset, gen5Offset);
}
[Fact]
public void Gen5RenderTargetGeometry_MatchesTheReferenceVolumeAllocation()
{
const uint width = 480;
const uint height = 270;
const uint depth = 128;
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.RenderTarget64KBGen5, 8, out var block));
Assert.Equal((128u, 64u, 1u), (block.BlockWidth, block.BlockHeight, block.BlockDepth));
var paddedWidth = (width + block.BlockWidth - 1) / block.BlockWidth * block.BlockWidth;
var paddedHeight = (height + block.BlockHeight - 1) / block.BlockHeight * block.BlockHeight;
var blocksPerSlice = (ulong)(paddedWidth / block.BlockWidth) * (paddedHeight / block.BlockHeight);
var sliceSize = blocksPerSlice * block.BlockSize;
var totalSize = sliceSize * depth;
Assert.Equal((512u, 320u), (paddedWidth, paddedHeight));
Assert.Equal(0x140000UL, sliceSize);
Assert.Equal(0xA000000UL, totalSize);
}
[Fact]
public void CpuAndShaderBlockKindOrdinals_StayAligned()
{
var cpuKinds = Enum.GetValues<TileBlockKind>();
var shaderKinds = Enum.GetValues<TilerBlockShape>();
Assert.Equal(cpuKinds.Length, shaderKinds.Length);
for (var index = 0; index < cpuKinds.Length; index++)
{
Assert.Equal(cpuKinds[index].ToString(), shaderKinds[index].ToString());
Assert.Equal((uint)cpuKinds[index], (uint)shaderKinds[index]);
}
}
[Fact]
public void MipTail_StartsWhereTheLevelFitsAndOffsetsRunBackwards()
{

View File

@ -59,6 +59,11 @@ public sealed class TilerParityTests : IClassFixture<HeadlessVulkanFixture>
foreach (var tilerCase in TilerCases.All())
{
var transfer = tilerCase.Transfer;
if (transfer.Kind == TileBlockKind.RenderTarget64KBGen5)
{
continue;
}
var reference = blobs[(int)transfer.Kind]!;
foreach (var toTiled in new[] { false, true })
{

View File

@ -61,7 +61,7 @@ public sealed class ShaderPrewarmListTests : IDisposable
Assert.True(
ShaderProgramCache.TryCompilePrewarm(
record, code, new FakeShaderCompiler(Compile), host.SharedInt64AtomicsEnabled, host.ExecGuardElisionEnabled,
out var compiled, out var layout, out var error),
host.ShaderSignedZeroInfNanPreserveFloat32Supported, out var compiled, out var layout, out var error),
error);
Assert.NotNull(layout);
Assert.Equal(runtime, Assert.IsType<FakeCompiledShader>(compiled).Spirv);

View File

@ -0,0 +1,102 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using Xunit;
namespace SharpEmu.Libs.Tests.Kernel;
[Collection(KernelMemoryCompatStateCollection.Name)]
public sealed class KernelFileRenameTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong SourcePathAddress = MemoryBase + 0x100;
private const ulong DestinationPathAddress = MemoryBase + 0x300;
private const ulong ReadBufferAddress = MemoryBase + 0x500;
[Fact]
public void KernelRename_ReplacesDestinationWhileSourceDescriptorRemainsOpen()
{
var identity = Guid.NewGuid().ToString("N");
var mountPoint = $"/rename-{identity}";
var hostRoot = Path.Combine(Path.GetTempPath(), $"sharpemu-rename-{identity}");
var sourceHostPath = Path.Combine(hostRoot, "STEMP000.DAT");
var destinationHostPath = Path.Combine(hostRoot, "SDATA000.DAT");
var sourceGuestPath = $"{mountPoint}/STEMP000.DAT";
var destinationGuestPath = $"{mountPoint}/SDATA000.DAT";
byte[] payload = [0x10, 0x20, 0x30, 0x40];
var fileDescriptor = -1;
Directory.CreateDirectory(hostRoot);
File.WriteAllBytes(sourceHostPath, payload);
File.WriteAllBytes(destinationHostPath, [0xFF]);
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, hostRoot);
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(SourcePathAddress, sourceGuestPath);
memory.WriteCString(DestinationPathAddress, destinationGuestPath);
try
{
context[CpuRegister.Rdi] = SourcePathAddress;
context[CpuRegister.Rsi] = 0x2;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelExports.KernelOpen(context));
fileDescriptor = unchecked((int)context[CpuRegister.Rax]);
Assert.True(fileDescriptor >= 3);
context[CpuRegister.Rdi] = SourcePathAddress;
context[CpuRegister.Rsi] = DestinationPathAddress;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelRename(context));
Assert.False(File.Exists(sourceHostPath));
var renamedPayload = new byte[payload.Length];
using (var destinationStream = new FileStream(
destinationHostPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete))
{
destinationStream.ReadExactly(renamedPayload);
}
Assert.Equal(payload, renamedPayload);
context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor);
context[CpuRegister.Rsi] = ReadBufferAddress;
context[CpuRegister.Rdx] = unchecked((uint)payload.Length);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelReadUnderscore(context));
Assert.Equal((ulong)payload.Length, context[CpuRegister.Rax]);
Span<byte> actual = stackalloc byte[payload.Length];
Assert.True(memory.TryRead(ReadBufferAddress, actual));
Assert.True(actual.SequenceEqual(payload));
context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelClose(context));
fileDescriptor = -1;
Assert.Equal(payload, File.ReadAllBytes(destinationHostPath));
}
finally
{
if (fileDescriptor >= 0)
{
context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor);
_ = KernelMemoryCompatExports.KernelClose(context);
}
_ = KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
if (Directory.Exists(hostRoot))
{
Directory.Delete(hostRoot, recursive: true);
}
}
}
}

View File

@ -197,6 +197,59 @@ public sealed class KernelMemoryCompatExportsTests
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
}
[Fact]
public void UnderscoreOpen_MissingFileUsesPosixFailureAbi()
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5) { FsBase = GuestMemoryBase + 0x800 };
memory.WriteCString(GuestMemoryBase + 0x100, "/__sharpemu_test_missing__/resource_level_high.bin");
context[CpuRegister.Rdi] = GuestMemoryBase + 0x100;
Assert.Equal(-1, KernelMemoryCompatExports.KernelOpenUnderscore(context));
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.True(context.TryReadUInt32(context.FsBase + 0x40, out var errno));
Assert.Equal(2u, errno);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, KernelExports.KernelOpen(context));
}
[Theory]
[InlineData("/dev/random")]
[InlineData("/dev/urandom")]
public void PosixRandomDevice_OpenReadFstatAndClose(string path)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(GuestMemoryBase + 0x100, path);
context[CpuRegister.Rdi] = GuestMemoryBase + 0x100;
Assert.Equal(0, KernelMemoryCompatExports.KernelOpenUnderscore(context));
var fd = context[CpuRegister.Rax];
Assert.True(fd >= 3);
try
{
context[CpuRegister.Rdi] = fd;
context[CpuRegister.Rsi] = GuestMemoryBase + 0x200;
context[CpuRegister.Rdx] = 32;
Assert.Equal(0, KernelMemoryCompatExports.PosixRead(context));
Assert.Equal(32UL, context[CpuRegister.Rax]);
var randomBytes = new byte[32];
Assert.True(memory.TryRead(GuestMemoryBase + 0x200, randomBytes));
Assert.Contains(randomBytes, value => value != 0);
context[CpuRegister.Rsi] = GuestMemoryBase + 0x400;
Assert.Equal(0, KernelMemoryCompatExports.PosixFstat(context));
}
finally
{
context[CpuRegister.Rdi] = fd;
Assert.Equal(0, KernelMemoryCompatExports.PosixClose(context));
}
context[CpuRegister.Rsi] = GuestMemoryBase + 0x200;
context[CpuRegister.Rdx] = 1;
Assert.Equal(-1, KernelMemoryCompatExports.PosixRead(context));
}
[Fact]
public void PosixOpen_MissingFileReturnsMinusOne()
{
@ -383,6 +436,50 @@ public sealed class KernelMemoryCompatExportsTests
}
}
[Fact]
public void AvailableDirectMemorySize_FullRangeClearsOutputsAndReportsNoSpace()
{
const ulong allocationStart = 0;
const ulong allocationLength = 0x0040_0000;
var context = new CpuContext(new FakeCpuMemory(GuestMemoryBase, 0x1000), Generation.Gen5);
try
{
AllocateDirectMemory(context, allocationStart, allocationLength);
Assert.True(context.TryWriteUInt64(SpanStartOutAddress, 0xDEAD_BEEF));
Assert.True(context.TryWriteUInt64(SpanSizeOutAddress, 0xDEAD_BEEF));
context[CpuRegister.Rdi] = allocationStart;
context[CpuRegister.Rsi] = allocationStart + allocationLength;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = SpanStartOutAddress;
context[CpuRegister.R8] = SpanSizeOutAddress;
Assert.Equal(unchecked((int)0x8002000C), KernelMemoryCompatExports.KernelAvailableDirectMemorySize(context));
Assert.True(context.TryReadUInt64(SpanStartOutAddress, out var spanStart));
Assert.True(context.TryReadUInt64(SpanSizeOutAddress, out var spanSize));
Assert.Equal(0UL, spanStart);
Assert.Equal(0UL, spanSize);
}
finally
{
ReleaseDirectMemory(context, allocationStart, allocationLength);
}
}
[Fact]
public void AvailableDirectMemorySize_EmptySearchRangeReportsNoSpace()
{
var context = new CpuContext(new FakeCpuMemory(GuestMemoryBase, 0x1000), Generation.Gen5);
context[CpuRegister.Rdi] = 0x0010_0000;
context[CpuRegister.Rsi] = 0x0010_0000;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = SpanStartOutAddress;
context[CpuRegister.R8] = SpanSizeOutAddress;
Assert.Equal(unchecked((int)0x8002000C), KernelMemoryCompatExports.KernelAvailableDirectMemorySize(context));
}
private static void AllocateDirectMemory(CpuContext context, ulong start, ulong length, ulong outputAddress = AllocationOutAddress)
{
context[CpuRegister.Rdi] = start;

View File

@ -190,8 +190,6 @@ public sealed class GuestRedZonePatcherTests
Assert.Equal(expected, ((delegate* unmanaged<ulong, ulong>)image.Base)(image.Base));
}
// Demon's Souls FUN_800ad8fe0: a two-byte AND whose successor is a branch
// target is patched by starting the span at the instruction before it.
[Fact]
public unsafe void PatchesShortAccessBeforeBranchTargetFromThePreviousInstruction()
{
@ -201,14 +199,14 @@ public sealed class GuestRedZonePatcherTests
var argument = OperatingSystem.IsWindows() ? (byte)0x91 : (byte)0x97;
byte[] function =
[
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // mov rax, value
0x48, 0x89, 0x44, 0x24, 0xF8, // mov [rsp-8], rax
0x48, 0x8D, argument, 0x00, 0x20, 0x00, 0x00, // lea rdx, [arg+0x2000]
0xB8, 0xFE, 0xFF, 0xFF, 0xFF, // mov eax, 0xFFFFFFFE
0x21, 0x02, // and [rdx], eax
0x48, 0x8B, 0x44, 0x24, 0xF8, // target: mov rax, [rsp-8]
0xC3, // ret
0xEB, 0xF8, // jmp target
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0x89, 0x44, 0x24, 0xF8,
0x48, 0x8D, argument, 0x00, 0x20, 0x00, 0x00,
0xB8, 0xFE, 0xFF, 0xFF, 0xFF,
0x21, 0x02,
0x48, 0x8B, 0x44, 0x24, 0xF8,
0xC3,
0xEB, 0xF8,
];
using var image = PatchedImage.Create(function, [(0x2000, 7u)]);
@ -222,6 +220,44 @@ public sealed class GuestRedZonePatcherTests
Assert.Equal(6u, image.ReadUInt32(0x2000));
}
[Fact]
public unsafe void LeavesShortAccessBeforeBranchTargetUnchangedWhenAnIndirectBranchIsUnresolved()
{
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
return;
var argument = OperatingSystem.IsWindows() ? (byte)0x91 : (byte)0x97;
byte[] function =
[
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0x89, 0x44, 0x24, 0xF8,
0x48, 0x8D, argument, 0x00, 0x20, 0x00, 0x00,
0xB8, 0xFE, 0xFF, 0xFF, 0xFF,
0x21, 0x02,
0x48, 0x8B, 0x44, 0x24, 0xF8,
0xC3,
0xEB, 0xF8,
0xFF, 0xE0,
];
using var image = PatchedImage.Create(function, [(0x2000, 7u)]);
Assert.Equal(0, image.Result.PatchedSites);
if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
{
Assert.Equal(1, image.Result.RedZoneFunctions);
Assert.Equal(1, image.Result.UnrelocatableSites);
Assert.Equal(1, image.Result.IndirectBranchRefusals);
}
for (var index = 0; index < function.Length; index++)
{
Assert.Equal(function[index], image.ReadByte((ulong)index));
}
Assert.Equal(0x1122_3344_5566_7788UL, ((delegate* unmanaged<ulong, ulong>)image.Base)(image.Base));
Assert.Equal(6u, image.ReadUInt32(0x2000));
}
private sealed unsafe class PatchedImage : IDisposable
{
private const ulong ImageSize = 0x10000;

View File

@ -46,14 +46,48 @@ public sealed class NpAuthExportsTests
Assert.Equal(unchecked((int)0x80550304), BinaryPrimitives.ReadInt32LittleEndian(resultBytes));
}
[Fact]
public void OnlineOperation_ReturnsServiceDownWithoutProducingCredentials()
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void OnlineOperation_CompletesSignedOutWithoutProducingCredentials(bool async, bool idToken)
{
NpAuthExports.ResetRuntimeState();
var ctx = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
var requestId = NpAuthExports.NpAuthCreateRequest(ctx);
ctx[CpuRegister.Rdi] = unchecked((uint)requestId);
var memory = new FakeCpuMemory(MemoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
ctx[CpuRegister.Rdi] = MemoryBase;
Assert.True(ctx.TryWriteUInt64(MemoryBase, 24));
var requestId = async
? NpAuthExports.NpAuthCreateAsyncRequest(ctx)
: NpAuthExports.NpAuthCreateRequest(ctx);
Assert.True(requestId > 0);
Assert.Equal(unchecked((int)0x80550401), NpAuthExports.NpAuthGetAuthorizationCodeV3(ctx));
var outputAddress = MemoryBase + 0x100;
var issuerAddress = MemoryBase + 0x1800;
var credentials = new byte[idToken ? 4104 : 136];
Array.Fill(credentials, (byte)0xAA);
Assert.True(memory.TryWrite(outputAddress, credentials));
Assert.True(ctx.TryWriteUInt32(issuerAddress, uint.MaxValue));
ctx[CpuRegister.Rdi] = unchecked((uint)requestId);
ctx[CpuRegister.Rdx] = outputAddress;
ctx[CpuRegister.Rcx] = issuerAddress;
var result = idToken
? NpAuthExports.NpAuthGetIdTokenV3(ctx)
: NpAuthExports.NpAuthGetAuthorizationCodeV3(ctx);
Assert.Equal(async ? 0 : unchecked((int)0x80550006), result);
Assert.True(memory.TryRead(outputAddress, credentials));
Assert.All(credentials, value => Assert.Equal((byte)0, value));
Assert.True(ctx.TryReadUInt32(issuerAddress, out var issuer));
Assert.Equal(idToken ? uint.MaxValue : 0u, issuer);
var resultAddress = MemoryBase + 0x1880;
ctx[CpuRegister.Rsi] = resultAddress;
Assert.Equal(0, NpAuthExports.NpAuthPollAsync(ctx));
Assert.True(ctx.TryReadUInt32(resultAddress, out var completionResult));
Assert.Equal(0x80550006u, completionResult);
Assert.Equal(unchecked((int)0x80550301), NpAuthExports.NpAuthGetAuthorizationCodeV3(ctx));
}
}

View File

@ -12,6 +12,29 @@ public sealed class NpEntitlementAccessExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
[Fact]
public void GetGameTrialsFlagReportsAFullGameAndOnlyWritesOneWord()
{
var memory = new FakeCpuMemory(MemoryBase, 0x100);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(MemoryBase, new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }));
context[CpuRegister.Rdi] = MemoryBase;
Assert.Equal(0, NpEntitlementAccessExports.NpEntitlementAccessGetGameTrialsFlag(context));
Span<byte> actual = stackalloc byte[8];
Assert.True(memory.TryRead(MemoryBase, actual));
Assert.Equal(new byte[] { 0, 0, 0, 0, 5, 6, 7, 8 }, actual.ToArray());
}
[Theory]
[InlineData(0UL, unchecked((int)0x817D0002))]
[InlineData(0x2_0000_0000UL, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)]
public void GetGameTrialsFlagValidatesOutputMemory(ulong address, int expected)
{
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x100), Generation.Gen5);
context[CpuRegister.Rdi] = address;
Assert.Equal(expected, NpEntitlementAccessExports.NpEntitlementAccessGetGameTrialsFlag(context));
}
[Theory]
[InlineData("GHOST2APP0000000")]
[InlineData("GHOST2BASE000000")]

View File

@ -27,6 +27,29 @@ public sealed class NpUniversalDataSystemExportsTests : IDisposable
public void Dispose() => NpUniversalDataSystemState.ResetForTests();
[Fact]
public void ContextBootstrap_PreservesHandlesAcrossExplicitInitialization()
{
var context = CreateContext(userId: 7, serviceLabel: 3, options: 0x11);
var serviceHandle = CreateServiceHandle();
var before = NpUniversalDataSystemState.GetMemoryStat();
Assert.True(before.CurrentInUseSize > 0);
Initialize(poolSize: 0x20_0000);
Initialize(poolSize: 0x40_0000);
SetRegisters((ulong)(uint)context, (ulong)(uint)serviceHandle, 0x22);
AssertSuccess(NpUniversalDataSystemExports.NpUniversalDataSystemRegisterContext(_context));
var after = NpUniversalDataSystemState.GetMemoryStat();
Assert.Equal(0x40_0000UL, after.PoolSize);
Assert.True(after.CurrentInUseSize >= before.CurrentInUseSize);
Assert.True(after.MaximumInUseSize >= before.MaximumInUseSize);
AssertSuccess(NpUniversalDataSystemExports.NpUniversalDataSystemTerminate(_context));
SetRegisters((ulong)(uint)context, (ulong)(uint)serviceHandle, 0x22);
Assert.Equal(ErrorInvalidArgument, NpUniversalDataSystemExports.NpUniversalDataSystemRegisterContext(_context));
}
[Fact]
public void FullLifecycle_PreservesAttachedChildAfterItsHandleIsDestroyed()
{

View File

@ -7,7 +7,7 @@ using Xunit;
namespace SharpEmu.Libs.Tests.Pad;
public sealed class PadExportsTests
public sealed class PadExportsTests : IDisposable
{
private const ulong Base = 0x1_0000_0000;
private const int InvalidHandle = unchecked((int)0x80920003);
@ -17,29 +17,85 @@ public sealed class PadExportsTests
public PadExportsTests()
{
PadExports.ResetOpenedPadForTests();
_ctx = new CpuContext(_memory, Generation.Gen5);
}
public void Dispose() => PadExports.ResetOpenedPadForTests();
[Theory]
[InlineData(0, 0)]
[InlineData(4, 4)]
[InlineData(15, 12)]
public void DeviceClassParseData_BoundsThePayloadAndPreservesAdjacentMemory(int length, int copied)
{
var padData = new byte[0x78];
padData[0x4C] = 1;
padData[0x6B] = (byte)length;
for (var index = 0; index < 12; index++)
{
padData[0x6C + index] = (byte)(index + 1);
}
Assert.True(_memory.TryWrite(Base + 0x100, padData));
Assert.True(_memory.TryWrite(Base + 0x200, Enumerable.Repeat((byte)0xA5, 32).ToArray()));
_ctx[CpuRegister.Rdi] = 0;
_ctx[CpuRegister.Rsi] = Base + 0x100;
_ctx[CpuRegister.Rdx] = Base + 0x200;
Assert.Equal(0, PadExports.PadDeviceClassParseData(_ctx));
var output = new byte[32];
Assert.True(_memory.TryRead(Base + 0x200, output));
Assert.Equal(copied == 0 ? 0 : -1, BitConverter.ToInt32(output));
Assert.Equal(1, output[4]);
Assert.Equal(copied, output[8]);
Assert.Equal(padData.AsSpan(0x6C, copied).ToArray(), output.AsSpan(12, copied).ToArray());
Assert.All(output[24..], value => Assert.Equal(0xA5, value));
}
[Fact]
public void DeviceClassGetExtendedInformation_LeavesAdjacentMemoryIntact()
{
const ulong address = Base + 0x100;
Assert.True(_memory.TryWrite(address, Enumerable.Repeat((byte)0xA5, 32).ToArray()));
_ctx[CpuRegister.Rdi] = 0;
_ctx[CpuRegister.Rsi] = address;
Assert.Equal(0, PadExports.PadDeviceClassGetExtendedInformation(_ctx));
var output = new byte[32];
Assert.True(_memory.TryRead(address, output));
Assert.All(output[..0x14], value => Assert.Equal(0, value));
Assert.All(output[0x14..], value => Assert.Equal(0xA5, value));
}
[NativeX64Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, InvalidHandle)]
[InlineData(-1, InvalidHandle)]
public void SetTiltCorrectionState_ValidatesHandle(int handle, int expected)
{
if (handle == 1)
{
Assert.Equal(1, OpenPad(0x10000000, 0));
}
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx));
}
// ABI: int scePadResetOrientation(int32_t handle) — handle only, no out
// parameter, so the only failure mode is a bad handle.
[Theory]
[NativeX64Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, InvalidHandle)]
[InlineData(-1, InvalidHandle)]
public void ResetOrientation_ValidatesHandle(int handle, int expected)
{
if (handle == 1)
{
Assert.Equal(1, OpenPad(0x10000000, 0));
}
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
Assert.Equal(expected, PadExports.PadResetOrientation(_ctx));
Assert.Equal(unchecked((ulong)expected), _ctx[CpuRegister.Rax]);
@ -132,4 +188,78 @@ public sealed class PadExportsTests
PadExports.ResetOpenedPadForTests();
}
}
[NativeX64Fact]
public void SpecialAndRemotePortsHaveIndependentLiveHandles()
{
const int noHandle = unchecked((int)0x80920008);
const int alreadyOpened = unchecked((int)0x80920004);
const ulong dataAddress = Base + 0x200;
Assert.Equal(0, PadExports.PadInit(_ctx));
_ctx[CpuRegister.Rdi] = 0xFF;
_ctx[CpuRegister.Rsi] = 16;
_ctx[CpuRegister.Rdx] = 0;
Assert.Equal(noHandle, PadExports.PadGetHandle(_ctx));
var standard = OpenPad(0x10000000, 0);
var special = OpenPad(0x10000000, 2);
var remote = OpenPad(0xFF, 16);
Assert.Equal(1, standard);
Assert.Equal(2, special);
Assert.Equal(3, remote);
Assert.Equal(alreadyOpened, OpenPad(0xFF, 16));
Assert.Equal(remote, PadExports.PadGetHandle(_ctx));
foreach (var handle in new[] { standard, special, remote })
{
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
_ctx[CpuRegister.Rsi] = dataAddress;
Assert.Equal(0, PadExports.PadGetControllerInformation(_ctx));
Assert.Equal(0, PadExports.PadReadState(_ctx));
}
_ctx[CpuRegister.Rdi] = unchecked((ulong)remote);
Assert.Equal(0, PadExports.PadClose(_ctx));
Assert.Equal(InvalidHandle, PadExports.PadReadState(_ctx));
Assert.Equal(InvalidHandle, PadExports.PadClose(_ctx));
_ctx[CpuRegister.Rdi] = 0xFF;
_ctx[CpuRegister.Rsi] = 16;
_ctx[CpuRegister.Rdx] = 0;
Assert.Equal(noHandle, PadExports.PadGetHandle(_ctx));
_ctx[CpuRegister.Rdi] = 0x10000000;
_ctx[CpuRegister.Rsi] = 0;
Assert.Equal(standard, PadExports.PadGetHandle(_ctx));
_ctx[CpuRegister.Rsi] = 2;
Assert.Equal(special, PadExports.PadGetHandle(_ctx));
Assert.True(OpenPad(0xFF, 16) > remote);
}
[NativeX64Theory]
[InlineData(-1, 0, 0, unchecked((int)0x80920008))]
[InlineData(0x10000001, 0, 0, unchecked((int)0x80920007))]
[InlineData(0x10000000, 1, 0, unchecked((int)0x80920007))]
[InlineData(0x10000000, 16, 0, unchecked((int)0x80920007))]
[InlineData(0xFF, 0, 0, unchecked((int)0x80920007))]
[InlineData(0xFF, 16, 1, unchecked((int)0x80920007))]
public void Open_RejectsUnsupportedTuples(int userId, int type, int index, int expected)
{
PadExports.PadInit(_ctx);
_ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
_ctx[CpuRegister.Rsi] = unchecked((ulong)type);
_ctx[CpuRegister.Rdx] = unchecked((ulong)index);
_ctx[CpuRegister.Rcx] = 0;
Assert.Equal(expected, PadExports.PadOpen(_ctx));
}
private int OpenPad(int userId, int type)
{
PadExports.PadInit(_ctx);
_ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
_ctx[CpuRegister.Rsi] = unchecked((ulong)type);
_ctx[CpuRegister.Rdx] = 0;
_ctx[CpuRegister.Rcx] = 0;
return PadExports.PadOpen(_ctx);
}
}

View File

@ -247,6 +247,25 @@ public sealed class SaveDataExportsTests : IDisposable
Assert.Equal(16382UL, freeBlocks);
}
[Fact]
public void GetMountInfo_WritesOnlyTheGuestStructure()
{
Assert.Equal(0, Mount());
WriteAscii(MountPointStr, MountPoint);
Span<byte> sentinel = stackalloc byte[0x10];
sentinel.Fill(0xCD);
Assert.True(_memory.TryWrite(MountInfo + 0x30, sentinel));
Assert.Equal(
0,
SaveDataExports.SaveDataGetMountInfo(
Reg(rdi: MountPointStr, rsi: MountInfo)));
Span<byte> after = stackalloc byte[0x10];
Assert.True(_memory.TryRead(MountInfo + 0x30, after));
Assert.True(after.SequenceEqual(sentinel));
}
[Fact]
public void DirNameSearch_ReportsBlockCapacityAndRemainingBlocks()
{

View File

@ -0,0 +1,58 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.UserService;
using Xunit;
namespace SharpEmu.Libs.Tests.UserService;
public sealed class UserServiceExportsTests
{
[Theory]
[InlineData(0x10000000)]
[InlineData(1)]
public void GetUserNumberWritesOneBasedSlotWithoutOverwritingAdjacentData(int userId)
{
const ulong address = 0x1_0000_0000;
var memory = new FakeCpuMemory(address, 0x100);
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(address, new byte[] { 0xEE, 0xEE, 0xEE, 0xEE, 0xAA, 0xBB, 0xCC, 0xDD }));
ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
ctx[CpuRegister.Rsi] = address;
Assert.Equal(0, UserServiceExports.UserServiceGetUserNumber(ctx));
Span<byte> actual = stackalloc byte[8];
Assert.True(memory.TryRead(address, actual));
Assert.Equal(new byte[] { 1, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD }, actual.ToArray());
}
[Theory]
[InlineData(0x10000001, 0x1_0000_0000UL, unchecked((int)0x80960009))]
[InlineData(0x10000000, 0UL, unchecked((int)0x80960005))]
[InlineData(0x10000000, 0x2_0000_0000UL, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)]
public void GetUserNumberValidatesUserAndOutput(int userId, ulong address, int expected)
{
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x100), Generation.Gen5);
ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
ctx[CpuRegister.Rsi] = address;
Assert.Equal(expected, UserServiceExports.UserServiceGetUserNumber(ctx));
}
[Fact]
public void TerminateResetsTheLoginEvent()
{
const ulong address = 0x1_0000_0000;
var memory = new FakeCpuMemory(address, 0x100);
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx));
Assert.Equal(0, UserServiceExports.UserServiceInitialize(ctx));
ctx[CpuRegister.Rdi] = address;
Assert.Equal(0, UserServiceExports.UserServiceGetEvent(ctx));
Assert.Equal(unchecked((int)0x80960007), UserServiceExports.UserServiceGetEvent(ctx));
Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx));
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
Assert.Equal(0, UserServiceExports.UserServiceInitialize(ctx));
Assert.Equal(0, UserServiceExports.UserServiceGetEvent(ctx));
Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx));
}
}