Compare commits
69 Commits
1016_part3
...
render_pip
| Author | SHA1 | Date |
|---|---|---|
|
|
b7856322a8 | |
|
|
160521fd78 | |
|
|
8164636d99 | |
|
|
f19c4ca4d5 | |
|
|
c61c1602aa | |
|
|
d0ffcb24d2 | |
|
|
880270b344 | |
|
|
d5332f01ca | |
|
|
fab999316e | |
|
|
6631a665f8 | |
|
|
002007c323 | |
|
|
b26c59460d | |
|
|
d4276d89f8 | |
|
|
e5c2755789 | |
|
|
22ec74e085 | |
|
|
7b730a51fc | |
|
|
8616383a58 | |
|
|
416464db1e | |
|
|
cd51a9ea2e | |
|
|
8f8737e2d1 | |
|
|
3fba66473b | |
|
|
e2ad2084f8 | |
|
|
9b552ffb72 | |
|
|
8c24a40bfa | |
|
|
643a565a22 | |
|
|
8f7fa3933f | |
|
|
48258b1e50 | |
|
|
ca076237c6 | |
|
|
dc73931dd6 | |
|
|
1fbecd3233 | |
|
|
ae97ac9525 | |
|
|
b179883b5c | |
|
|
32c9a37b0e | |
|
|
5d6b59eeb8 | |
|
|
8d0af3d8f7 | |
|
|
a1b44452fa | |
|
|
d2090f47fb | |
|
|
b5c15a7426 | |
|
|
fd2afcd8c8 | |
|
|
6d69163297 | |
|
|
c622b64ace | |
|
|
0de84e34df | |
|
|
84c830f7ce | |
|
|
d24f6c6a85 | |
|
|
08891b1fef | |
|
|
7a6e3d0308 | |
|
|
4087ab84f8 | |
|
|
5febf063f4 | |
|
|
c02f2163f2 | |
|
|
8a0d713665 | |
|
|
3f01c0e91c | |
|
|
593cec957d | |
|
|
3f84f59065 | |
|
|
15768908a1 | |
|
|
682ad298ef | |
|
|
7ebf642aff | |
|
|
b307b2f8fd | |
|
|
28c922c7a5 | |
|
|
ae406fc049 | |
|
|
61697fa461 | |
|
|
c7bbb09311 | |
|
|
a298369293 | |
|
|
4d5697892e | |
|
|
46a7d3a446 | |
|
|
f32266834c | |
|
|
0af8dce081 | |
|
|
289f090b50 | |
|
|
47d4c07a8d | |
|
|
1811cef944 |
|
|
@ -9,6 +9,7 @@
|
|||
[Aa]rtifacts/
|
||||
logs/
|
||||
.tmp/
|
||||
src/SharpEmu.Demo/
|
||||
|
||||
x86/
|
||||
x64/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
|
||||
<PackageVersion Include="FFmpeg.AutoGen" Version="7.1.1" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="ZstdSharp.Port" Version="0.8.5" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using SharpEmu.Core.Runtime;
|
|||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.GUI;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -732,7 +733,9 @@ internal static partial class Program
|
|||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath));
|
||||
var directory = ZArchive.IsArchivePath(ebootPath)
|
||||
? ZArchiveCache.ExtractMetadata(ebootPath)
|
||||
: Path.GetDirectoryName(Path.GetFullPath(ebootPath));
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
return null;
|
||||
|
|
@ -1010,7 +1013,7 @@ internal static partial class Program
|
|||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin|path-to-game.zar>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,4 +180,75 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<SharpEmuDemoTag>49e6b3b</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" />
|
||||
<!-- Older demo releases include preview music; keep the bundled demo silent. -->
|
||||
<Delete Files="$(SharpEmuDemoDir)sce_sys/snd0.at9" />
|
||||
<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" />
|
||||
<Delete Files="$(OutDir)$(SharpEmuDemoFolderName)/sce_sys/snd0.at9" />
|
||||
</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" />
|
||||
<Delete Files="$(PublishDir)$(SharpEmuDemoFolderName)/sce_sys/snd0.at9" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -5603,6 +5604,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return false;
|
||||
}
|
||||
|
||||
// Map does not reject a range that is already mapped: it reuses and zero-fills it. Two threads
|
||||
// creating guest threads at once could both find the same slot free and share one stack, so
|
||||
// choosing and mapping a slot happen under one lock.
|
||||
private static readonly object GuestThreadRegionGate = new();
|
||||
|
||||
private static bool TryMapGuestThreadRegion(
|
||||
IVirtualMemory virtualMemory,
|
||||
ulong baseAddress,
|
||||
|
|
@ -5610,6 +5616,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
ProgramHeaderFlags protection,
|
||||
out ulong mappedBase,
|
||||
out string? error)
|
||||
{
|
||||
lock (GuestThreadRegionGate)
|
||||
{
|
||||
return TryMapGuestThreadRegionLocked(virtualMemory, baseAddress, size, protection, out mappedBase, out error);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMapGuestThreadRegionLocked(
|
||||
IVirtualMemory virtualMemory,
|
||||
ulong baseAddress,
|
||||
ulong size,
|
||||
ProgramHeaderFlags protection,
|
||||
out ulong mappedBase,
|
||||
out string? error)
|
||||
{
|
||||
for (int i = 0; i < GuestThreadRegionSlots; i++)
|
||||
{
|
||||
|
|
@ -5644,6 +5664,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
IVirtualMemory virtualMemory,
|
||||
out ulong tlsBase,
|
||||
out string? error)
|
||||
{
|
||||
lock (GuestThreadRegionGate)
|
||||
{
|
||||
return TryMapGuestThreadTlsRegionLocked(virtualMemory, out tlsBase, out error);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMapGuestThreadTlsRegionLocked(
|
||||
IVirtualMemory virtualMemory,
|
||||
out ulong tlsBase,
|
||||
out string? error)
|
||||
{
|
||||
for (int i = 0; i < GuestThreadRegionSlots; i++)
|
||||
{
|
||||
|
|
@ -5726,18 +5757,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool LegacyGuestPriorities =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_GUEST_PRIORITY"), "legacy", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static ThreadPriority MapGuestThreadPriority(int priority)
|
||||
{
|
||||
if (priority <= 478)
|
||||
{
|
||||
return ThreadPriority.Highest;
|
||||
return LegacyGuestPriorities ? ThreadPriority.Highest : ThreadPriority.Normal;
|
||||
}
|
||||
if (priority >= 733)
|
||||
{
|
||||
return ThreadPriority.Lowest;
|
||||
}
|
||||
|
||||
return ThreadPriority.Normal;
|
||||
return LegacyGuestPriorities ? ThreadPriority.Normal : ThreadPriority.BelowNormal;
|
||||
}
|
||||
|
||||
private void ApplyGuestThreadAffinity(ulong guestAffinityMask)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2070,6 +2070,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
GuestImageWriteTracker.NotifyManagedWrite(virtualAddress, (ulong)source.Length);
|
||||
GuestGpuMemoryHook.MarkCpuWrite(virtualAddress, (ulong)source.Length);
|
||||
|
||||
if (!source.IsEmpty && _backedSpace is { } backed && !_disposed &&
|
||||
backed.TryWriteSingleView(virtualAddress, source))
|
||||
{
|
||||
NotifyGuestWriteWatch(virtualAddress, source);
|
||||
return true;
|
||||
}
|
||||
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
|
|
@ -148,6 +149,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
public OrbisGen2Result Run(string ebootPath)
|
||||
{
|
||||
using var archiveMount = MountArchive(ref ebootPath);
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
|
|
@ -159,6 +161,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);
|
||||
|
|
@ -399,6 +402,72 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
return result;
|
||||
}
|
||||
|
||||
// A .zar holds the whole application folder. Mount it at a cache folder that gets the files host
|
||||
// code reads by path (the executable, its modules and sce_sys); everything else is read from the
|
||||
// archive when the guest asks for it. The path becomes the cached executable.
|
||||
private static ArchiveMountScope? MountArchive(ref string ebootPath)
|
||||
{
|
||||
if (!ZArchive.IsArchivePath(ebootPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var archivePath = Path.GetFullPath(ebootPath);
|
||||
var archive = ZArchive.Open(archivePath) ??
|
||||
throw new InvalidDataException($"'{archivePath}' is not a valid .zar archive.");
|
||||
if (!archive.IsFile(archive.LookUp("eboot.bin")))
|
||||
{
|
||||
archive.Dispose();
|
||||
throw new FileNotFoundException("The archive has no eboot.bin at its root.", archivePath);
|
||||
}
|
||||
|
||||
var cacheRoot = ZArchiveCache.Prepare(archivePath);
|
||||
App0Archive.Mount(archive, cacheRoot);
|
||||
try
|
||||
{
|
||||
foreach (var entry in archive.GetEntries(archive.LookUp(string.Empty)))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
if (entry.Name.Equals("sce_sys", StringComparison.OrdinalIgnoreCase) ||
|
||||
entry.Name.Equals("sce_module", StringComparison.OrdinalIgnoreCase) ||
|
||||
entry.Name.Equals("sce_modules", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
App0Archive.ExtractDirectory(entry.Name);
|
||||
}
|
||||
}
|
||||
else if (entry.Name.Equals("eboot.bin", StringComparison.OrdinalIgnoreCase) ||
|
||||
Path.GetExtension(entry.Name).Equals(".prx", StringComparison.OrdinalIgnoreCase) ||
|
||||
Path.GetExtension(entry.Name).Equals(".sprx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
App0Archive.Extract(Path.Combine(cacheRoot, entry.Name));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var modules in new[] { Path.Combine("Media", "Modules"), Path.Combine("Media", "Plugins") })
|
||||
{
|
||||
if (App0Archive.IsDirectory(Path.Combine(cacheRoot, modules)))
|
||||
{
|
||||
App0Archive.ExtractDirectory(modules);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
App0Archive.Unmount();
|
||||
throw;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Mounted archive '{archivePath}' at '{cacheRoot}'");
|
||||
ebootPath = Path.Combine(cacheRoot, "eboot.bin");
|
||||
return new ArchiveMountScope();
|
||||
}
|
||||
|
||||
private sealed class ArchiveMountScope : IDisposable
|
||||
{
|
||||
public void Dispose() => App0Archive.Unmount();
|
||||
}
|
||||
|
||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||
{
|
||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||
|
|
@ -805,6 +874,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
|
||||
try
|
||||
{
|
||||
App0Archive.Extract(modulePath);
|
||||
var fileInfo = new FileInfo(modulePath);
|
||||
if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > int.MaxValue)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// 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"),
|
||||
];
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,25 @@
|
|||
"Library.Context.CopyTitleId": "Copy title ID",
|
||||
"Library.Context.GameSettings": "Game settings…",
|
||||
"Library.Context.Remove": "Remove from library",
|
||||
"Library.Context.PackZar": "Pack as .zar…",
|
||||
|
||||
"Library.Pack.Title": "Pack as .zar",
|
||||
"Library.Pack.Heading": "Packing {0}",
|
||||
"Library.Pack.Explanation": "This stores the whole game folder in a single compressed .zar file. SharpEmu runs the game straight from it without unpacking, so it usually takes less disk space and plays the same. The game folder is kept unless you choose to delete it below.",
|
||||
"Library.Pack.Preparing": "Reading the game folder…",
|
||||
"Library.Pack.Progress": "{0} / {1} GB ({2}%)",
|
||||
"Library.Pack.Cancelling": "Cancelling…",
|
||||
"Library.Pack.Done": "Done. The archive is ready to launch.",
|
||||
"Library.Pack.Failed": "Packing failed: {0}",
|
||||
"Library.Pack.Close": "Close",
|
||||
"Library.Pack.MuteMusic": "Mute the title music while packing",
|
||||
"Library.Pack.DeleteSource": "Delete the game folder after packing",
|
||||
"Library.Pack.DeleteSourceWarning": "The folder is permanently deleted once the archive is written and checked. Keep a backup until you know the archive works.",
|
||||
"Library.Pack.Verifying": "Checking the archive…",
|
||||
"Library.Pack.Deleting": "Deleting the game folder…",
|
||||
"Library.Pack.DoneDeleted": "Done. The archive is ready and the game folder was deleted.",
|
||||
"Library.Pack.DeleteSkipped": "Done. The game folder was kept because the archive could not be confirmed to hold all of it.",
|
||||
"Library.Pack.DeleteFailed": "The archive is ready, but deleting the game folder failed: {0}",
|
||||
|
||||
"Library.Empty.Title": "Your library is empty",
|
||||
"Library.Empty.Hint": "Add a folder containing your games to get started.",
|
||||
|
|
@ -201,6 +220,7 @@
|
|||
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
|
||||
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
|
||||
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
|
||||
"Status.PackedZar": "Packed game archive: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copied to clipboard.",
|
||||
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
|
||||
"Status.Running": "Running {0}",
|
||||
|
|
@ -219,6 +239,8 @@
|
|||
"Dialog.SaveLogFile": "Select where to save the Log file",
|
||||
"Dialog.PlainTextFiles": "Plain Text Files",
|
||||
"Dialog.LogFiles": "Log Files",
|
||||
"Dialog.SaveZar": "Save the game archive",
|
||||
"Dialog.ZarFiles": "Game archives",
|
||||
|
||||
"Options.About" : "About",
|
||||
"About.Github.Label": "GitHub",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||
"Library.AddFolder": "Ajouter un dossier",
|
||||
"Library.OpenFile": "Ouvrir un fichier…",
|
||||
"Library.View.Grid": "Vue empilée",
|
||||
"Library.View.Carousel": "Vue en rangée",
|
||||
|
||||
"Library.Context.Launch": "Lancer",
|
||||
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
|
||||
|
|
@ -57,8 +59,8 @@
|
|||
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
|
||||
"Options.Env.Group.Debug": "Débogage",
|
||||
"Options.Env.Group.General": "Général",
|
||||
"Options.Env.RenderDoc.Desc": "Charge l'API in-application de RenderDoc afin de capturer des images depuis l'émulateur.\nAppuyez sur F12 pendant le jeu pour capturer une image ; les captures sont écrites dans user/logs/capture_logs/<TITLE_ID>.\nNécessite l'installation de RenderDoc. Ralentit le GPU et bloque certains jeux : laissez cette option désactivée sauf en cas de débogage.",
|
||||
"Options.Env.DisableVkImplicitLayers.Desc": "Lorsque RenderDoc est activé, d'autres overlays en conflit peuvent provoquer une perte du périphérique. Activer cette option peut corriger le problème.",
|
||||
"Options.Env.RenderDoc.Desc": "Charger l’API intégrée de RenderDoc afin de capturer des images depuis l’émulateur.\nAppuyez sur F12 pendant le jeu pour capturer une image ; les captures sont écrites dans user/logs/capture_logs/<TITLE_ID>.\nNécessite l’installation de RenderDoc. Ralentit le GPU et bloque certains jeux : laissez cette option désactivée sauf en cas de débogage.",
|
||||
"Options.Env.DisableVkImplicitLayers.Desc": "Lorsque RenderDoc est activé, d’autres overlays en conflit peuvent provoquer une perte du périphérique. Activer cette option peut corriger le problème.",
|
||||
"Options.Section.Emulation": "ÉMULATION",
|
||||
"Options.Section.Logging": "JOURNALISATION",
|
||||
"Options.Section.Launcher": "LANCEUR",
|
||||
|
|
@ -83,7 +85,7 @@
|
|||
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
|
||||
|
||||
"Options.CrashDump.Label": "Fichiers de vidage après plantage",
|
||||
"Options.CrashDump.Desc": "Enregistre les fichiers de vidage à côté du journal d’exécution (Windows x64 uniquement). Ces fichiers peuvent être volumineux et contenir des données privées.",
|
||||
"Options.CrashDump.Desc": "Enregistrer les fichiers de vidage à côté du journal d’exécution (Windows x64 uniquement). Ces fichiers peuvent être volumineux et contenir des données privées.",
|
||||
"Options.LogToFile.Label": "Enregistrer dans un fichier",
|
||||
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
|
||||
|
||||
|
|
@ -121,15 +123,15 @@
|
|||
"Launch.Idle": "Inactif",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Lancer",
|
||||
"Launch.CustomEnv": "Lancer avec des variables d'environnement personnalisées",
|
||||
"Launch.CustomEnv.Title": "Lancer avec des variables d'environnement personnalisées",
|
||||
"Launch.CustomEnv.Desc": "Saisissez une variable d'environnement par ligne au format NOM=VALEUR. Les valeurs personnalisées remplacent les réglages GUI et spécifiques au jeu portant le même nom.",
|
||||
"Launch.CustomEnv.Documentation": "Voir les variables d'environnement non documentées ↗",
|
||||
"Launch.CustomEnv": "Lancer avec des variables d’environnement personnalisées",
|
||||
"Launch.CustomEnv.Title": "Lancer avec des variables d’environnement personnalisées",
|
||||
"Launch.CustomEnv.Desc": "Saisissez une variable d’environnement par ligne au format NOM=VALEUR. Les valeurs personnalisées remplacent les réglages GUI et spécifiques au jeu portant le même nom.",
|
||||
"Launch.CustomEnv.Documentation": "Voir les variables d’environnement non documentées ↗",
|
||||
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXEMPLE=1",
|
||||
"Launch.CustomEnv.SaveGame": "Enregistrer pour le jeu",
|
||||
"Launch.CustomEnv.SaveGlobal": "Enregistrer globalement",
|
||||
"Launch.CustomEnv.Play": "Jouer",
|
||||
"Launch.CustomEnv.Invalid": "Variable d'environnement invalide : {0}",
|
||||
"Launch.CustomEnv.Invalid": "Variable d’environnement invalide : {0}",
|
||||
"Launch.CustomEnv.SavedGame": "Enregistré pour ce jeu.",
|
||||
"Launch.CustomEnv.SavedGlobal": "Enregistré globalement.",
|
||||
"Launch.Stop": "■ Arrêter",
|
||||
|
|
@ -237,8 +239,8 @@
|
|||
"Options.Hdr.Desc": "Utiliser le HDR lorsque l’écran sélectionné et le backend graphique le prennent en charge. Le mode automatique revient au SDR.",
|
||||
"Options.Hdr.Auto": "Automatique",
|
||||
"Options.Env.BinkPlayback.Label": "Lecture vidéo Bink",
|
||||
"Options.Env.BinkPlayback.Desc": "Host lit les vidéos Bink via le pont FFmpeg, mais certains jeux peuvent rencontrer des problèmes.\nGuest utilise le décodeur propre au moteur du jeu.\nSkip ignore les vidéos Bink, ce qui peut faciliter le développement.",
|
||||
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Guest",
|
||||
"Options.Env.BinkPlayback.Desc": "Hôte lit les vidéos Bink via le pont FFmpeg, mais certains jeux peuvent rencontrer des problèmes.\nInvité utilise le décodeur propre au moteur du jeu.\nIgnorer permet de passer les vidéos Bink, ce qui peut faciliter le développement.",
|
||||
"Options.Env.BinkPlayback.Host": "Hôte (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Invité",
|
||||
"Options.Env.BinkPlayback.Skip": "Ignorer"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxPackZar"
|
||||
Header="{Binding [Library.Context.PackZar], Source={x:Static local:Localization.Instance}}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▣" FontSize="12" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxGameSettings"
|
||||
Header="{Binding [Library.Context.GameSettings], Source={x:Static local:Localization.Instance}}">
|
||||
|
|
@ -299,6 +306,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
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Avalonia.Threading;
|
|||
using Avalonia.VisualTree;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Runtime;
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.Pad;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
|
|
@ -178,6 +179,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 +260,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;
|
||||
|
|
@ -371,18 +406,13 @@ public partial class MainWindow : Window
|
|||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
|
||||
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
|
||||
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
||||
CtxPackZar.Click += async (_, _) => await PackSelectedAsZarAsync();
|
||||
|
||||
Opened += async (_, _) => await OnOpenedAsync();
|
||||
Closing += (_, _) => BeginWindowClosing();
|
||||
Closed += (_, _) => CompleteWindowClosing();
|
||||
|
||||
SdlLauncherGamepad.EnsureStarted();
|
||||
_gamepadTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(50),
|
||||
};
|
||||
EnvRenderDocToggle.IsCheckedChanged += (_, _) =>
|
||||
|
||||
SetEnvironmentToggle(
|
||||
"SHARPEMU_RENDERDOC",
|
||||
EnvRenderDocToggle.IsChecked == true);
|
||||
|
|
@ -390,24 +420,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 +504,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 +531,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 +547,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 +888,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 +932,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 +1002,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 +1019,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 +1037,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()
|
||||
|
|
@ -1828,6 +1954,19 @@ public partial class MainWindow : Window
|
|||
/// </summary>
|
||||
private static long ComputeInstallSize(string ebootPath)
|
||||
{
|
||||
// An archive is the whole game; the folder it sits in may hold other games.
|
||||
if (ZArchive.IsArchivePath(ebootPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileInfo(ebootPath).Length;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(ebootPath);
|
||||
if (directory is null)
|
||||
{
|
||||
|
|
@ -1876,7 +2015,9 @@ public partial class MainWindow : Window
|
|||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
|
||||
// A game is a folder with eboot.bin or a .zar archive of one.
|
||||
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration)
|
||||
.Concat(Directory.EnumerateFiles(folder, "*.zar", enumeration)))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(file);
|
||||
if (!seen.Add(fullPath) || excludedPaths.Contains(fullPath))
|
||||
|
|
@ -1884,21 +2025,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 +2036,59 @@ 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}");
|
||||
}
|
||||
|
||||
// An archive's metadata is read from its cache folder, which stands in for the game folder.
|
||||
var metadataPath = fullPath;
|
||||
if (ZArchive.IsArchivePath(fullPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ZArchiveCache.ExtractMetadata(fullPath) is { } cacheRoot)
|
||||
{
|
||||
metadataPath = Path.Combine(cacheRoot, "eboot.bin");
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GUI][WARN] Could not read archive '{fullPath}': {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var (title, titleId, version) = TryReadParamJson(metadataPath);
|
||||
return new GameEntry(
|
||||
title ?? GameNameFor(metadataPath), titleId, version, fullPath, size,
|
||||
FindCoverFor(metadataPath), FindBackgroundFor(metadataPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the game title, title id and content version from
|
||||
/// sce_sys/param.json next to the executable, when present.
|
||||
|
|
@ -2083,6 +2260,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)
|
||||
|
|
@ -2137,6 +2503,100 @@ public partial class MainWindow : Window
|
|||
CtxLaunch.IsEnabled = !_isRunning;
|
||||
CtxCopyTitleId.IsEnabled = game.TitleId is not null;
|
||||
CtxGameSettings.IsEnabled = !string.IsNullOrWhiteSpace(game.TitleId);
|
||||
CtxPackZar.IsEnabled = !ZArchive.IsArchivePath(game.Path);
|
||||
}
|
||||
|
||||
// Packs the selected game's folder into a .zar chosen by the user, beside the folder by default.
|
||||
private async Task PackSelectedAsZarAsync()
|
||||
{
|
||||
if (GameList.SelectedItem is not GameEntry game || ZArchive.IsArchivePath(game.Path) ||
|
||||
Path.GetDirectoryName(game.Path) is not { } gameFolder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var loc = Localization.Instance;
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
Title = loc.Get("Dialog.SaveZar"),
|
||||
SuggestedFileName = SuggestedArchiveName(Path.GetFileName(gameFolder), game.TitleId, game.Version),
|
||||
DefaultExtension = "zar",
|
||||
FileTypeChoices = [new FilePickerFileType(loc.Get("Dialog.ZarFiles")) { Patterns = ["*.zar"] }],
|
||||
};
|
||||
if (Path.GetDirectoryName(gameFolder) is { } parent &&
|
||||
await StorageProvider.TryGetFolderFromPathAsync(parent) is { } startFolder)
|
||||
{
|
||||
options.SuggestedStartLocation = startFolder;
|
||||
}
|
||||
|
||||
var file = await StorageProvider.SaveFilePickerAsync(options);
|
||||
var destination = file?.TryGetLocalPath();
|
||||
if (string.IsNullOrEmpty(destination))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A running game may hold files in the folder open, so it is never deleted then.
|
||||
var dialog = new ZarPackDialog(game.Name, gameFolder, destination, canDeleteSource: !_isRunning);
|
||||
dialog.MuteMusicChanged += SetTitleMusicMuted;
|
||||
try
|
||||
{
|
||||
if (await dialog.ShowDialog<bool>(this))
|
||||
{
|
||||
AppendConsoleLine(loc.Format("Status.PackedZar", destination), SuccessLineBrush);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetTitleMusicMuted(false);
|
||||
}
|
||||
}
|
||||
|
||||
// TITLEID-version (PPSA01234-1.004): the title id differs between regions of the same game and
|
||||
// the version between updates, so each archive gets its own name and cache folder. Without a
|
||||
// title id the game folder's name stands in.
|
||||
internal static string SuggestedArchiveName(string folderName, string? titleId, string? version)
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(titleId) ? folderName : titleId.Trim();
|
||||
var shortVersion = ShortVersion(version);
|
||||
if (shortVersion.Length > 0)
|
||||
{
|
||||
name = $"{name}-{shortVersion}";
|
||||
}
|
||||
|
||||
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
name = name.Replace(invalid, '_');
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
// "01.004.000" reads as "1.004": the leading zeros of the major number go, and the last part
|
||||
// stays only when it is not zero. A version in another form is kept as it is.
|
||||
private static string ShortVersion(string? version)
|
||||
{
|
||||
var text = version?.Trim() ?? string.Empty;
|
||||
var parts = text.Split('.');
|
||||
if (parts.Length < 2 || !parts.All(part => part.Length > 0 && part.All(char.IsAsciiDigit)))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
var major = parts[0].TrimStart('0');
|
||||
var shortened = $"{(major.Length == 0 ? "0" : major)}.{parts[1]}";
|
||||
return parts.Length > 2 && parts[2].Any(digit => digit != '0')
|
||||
? $"{shortened}.{string.Join('.', parts[2..])}"
|
||||
: shortened;
|
||||
}
|
||||
|
||||
private void SetTitleMusicMuted(bool muted)
|
||||
{
|
||||
_sndPreview.Muted = muted;
|
||||
if (!muted && WindowState != WindowState.Minimized)
|
||||
{
|
||||
_sndPreview.Resume();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenSelectedGameFolder()
|
||||
|
|
@ -2312,7 +2772,9 @@ public partial class MainWindow : Window
|
|||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(game.Path);
|
||||
var directory = ZArchive.IsArchivePath(game.Path)
|
||||
? ZArchiveCache.GetCacheRoot(game.Path)
|
||||
: Path.GetDirectoryName(game.Path);
|
||||
var sndPath = directory is null ? null : Path.Combine(directory, "sce_sys", "snd0.at9");
|
||||
if (sndPath is not null && File.Exists(sndPath))
|
||||
{
|
||||
|
|
@ -2420,7 +2882,7 @@ public partial class MainWindow : Window
|
|||
FileTypeFilter = new[]
|
||||
{
|
||||
new FilePickerFileType(Localization.Instance.Get("Dialog.PsExecutables"))
|
||||
{ Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
|
||||
{ Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf", "*.zar" } },
|
||||
FilePickerFileTypes.All,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,9 +26,44 @@ internal sealed class SndPreviewPlayer
|
|||
private GCHandle _pinnedWav;
|
||||
private bool _playing;
|
||||
private bool _paused;
|
||||
private bool _muted;
|
||||
private string? _cachedPath;
|
||||
private byte[]? _cachedWav;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the music silent while set, even across <see cref="Play"/> and
|
||||
/// <see cref="Resume"/>; the track stays ready so clearing it and
|
||||
/// resuming restarts the music.
|
||||
/// </summary>
|
||||
public bool Muted
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _muted;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_muted = value;
|
||||
if (value && _playing)
|
||||
{
|
||||
_ = PlaySound(0, 0, 0);
|
||||
_playing = false;
|
||||
_paused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts looping the given snd0.at9 after a short debounce.</summary>
|
||||
public void Play(string at9Path)
|
||||
{
|
||||
|
|
@ -87,6 +122,12 @@ internal sealed class SndPreviewPlayer
|
|||
|
||||
// The WAV image must stay pinned while winmm plays from it.
|
||||
_pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned);
|
||||
if (_muted)
|
||||
{
|
||||
_paused = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
||||
if (!_playing)
|
||||
{
|
||||
|
|
@ -144,7 +185,7 @@ internal sealed class SndPreviewPlayer
|
|||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_paused || !_pinnedWav.IsAllocated)
|
||||
if (!_paused || _muted || !_pinnedWav.IsAllocated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SharpEmu.GUI"
|
||||
x:Class="SharpEmu.GUI.ZarPackDialog"
|
||||
Width="520"
|
||||
SizeToContent="Height"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{StaticResource BgBrush}"
|
||||
Title="{Binding [Library.Pack.Title], Source={x:Static local:Localization.Instance}}">
|
||||
<StackPanel Margin="24" Spacing="12">
|
||||
<TextBlock x:Name="HeadingText"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Text="{Binding [Library.Pack.Explanation], Source={x:Static local:Localization.Instance}}" />
|
||||
|
||||
<TextBlock x:Name="DestinationText"
|
||||
Foreground="{StaticResource MutedBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<ProgressBar x:Name="PackProgress"
|
||||
Minimum="0"
|
||||
Maximum="1"
|
||||
Height="8"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock x:Name="StatusText"
|
||||
Foreground="{StaticResource MutedBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox x:Name="DeleteSourceCheck"
|
||||
Content="{Binding [Library.Pack.DeleteSource], Source={x:Static local:Localization.Instance}}" />
|
||||
<TextBlock Margin="28,0,0,0"
|
||||
Foreground="{StaticResource DangerBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding [Library.Pack.DeleteSourceWarning], Source={x:Static local:Localization.Instance}}" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,*,Auto">
|
||||
<ToggleButton x:Name="MuteMusicButton"
|
||||
Grid.Column="0"
|
||||
Classes="ghost"
|
||||
Padding="10,4"
|
||||
ToolTip.Tip="{Binding [Library.Pack.MuteMusic], Source={x:Static local:Localization.Instance}}"
|
||||
Content="🔊" />
|
||||
<Button x:Name="CloseButton"
|
||||
Grid.Column="2"
|
||||
Classes="ghost"
|
||||
Content="{Binding [Common.Cancel], Source={x:Static local:Localization.Instance}}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia.Controls;
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
// Packs a game folder into a .zar on a worker thread and shows its progress. The button cancels
|
||||
// while packing and closes once done; the dialog returns true when the archive was written. When
|
||||
// the user opts in, the game folder is deleted afterwards, but only once the finished archive opens
|
||||
// and holds every file of the folder at the same size.
|
||||
public partial class ZarPackDialog : Window
|
||||
{
|
||||
private readonly CancellationTokenSource _cancellation = new();
|
||||
private readonly string _sourceFolder = string.Empty;
|
||||
private readonly string _destination = string.Empty;
|
||||
private bool _finished;
|
||||
private bool _succeeded;
|
||||
|
||||
public ZarPackDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ZarPackDialog(string gameName, string sourceFolder, string destination, bool canDeleteSource = true)
|
||||
: this()
|
||||
{
|
||||
_sourceFolder = sourceFolder;
|
||||
_destination = destination;
|
||||
DeleteSourceCheck.IsEnabled = canDeleteSource;
|
||||
HeadingText.Text = Localization.Instance.Format("Library.Pack.Heading", gameName);
|
||||
DestinationText.Text = destination;
|
||||
StatusText.Text = Localization.Instance.Get("Library.Pack.Preparing");
|
||||
CloseButton.Click += (_, _) => OnCloseClicked();
|
||||
MuteMusicButton.IsCheckedChanged += (_, _) =>
|
||||
{
|
||||
var muted = MuteMusicButton.IsChecked == true;
|
||||
MuteMusicButton.Content = muted ? "🔇" : "🔊";
|
||||
MuteMusicChanged?.Invoke(muted);
|
||||
};
|
||||
Opened += async (_, _) => await PackAsync();
|
||||
Closing += (_, _) =>
|
||||
{
|
||||
if (!_finished)
|
||||
{
|
||||
_cancellation.Cancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Raised when the mute button changes; the owner silences its title music while it is set.
|
||||
public event Action<bool>? MuteMusicChanged;
|
||||
|
||||
private void OnCloseClicked()
|
||||
{
|
||||
if (_finished)
|
||||
{
|
||||
Close(_succeeded);
|
||||
return;
|
||||
}
|
||||
|
||||
CloseButton.IsEnabled = false;
|
||||
StatusText.Text = Localization.Instance.Get("Library.Pack.Cancelling");
|
||||
_cancellation.Cancel();
|
||||
}
|
||||
|
||||
private async Task PackAsync()
|
||||
{
|
||||
// Progress<T> captures the UI context, so reports arrive on the UI thread.
|
||||
var progress = new Progress<ZArchiveWriter.Progress>(report =>
|
||||
{
|
||||
var fraction = report.BytesTotal == 0 ? 1 : (double)report.BytesDone / report.BytesTotal;
|
||||
PackProgress.Value = fraction;
|
||||
StatusText.Text = Localization.Instance.Format(
|
||||
"Library.Pack.Progress",
|
||||
FormatGigabytes(report.BytesDone),
|
||||
FormatGigabytes(report.BytesTotal),
|
||||
(int)(fraction * 100));
|
||||
});
|
||||
|
||||
string? error = null;
|
||||
var cancelled = false;
|
||||
try
|
||||
{
|
||||
await Task.Run(() => ZArchiveWriter.Pack(_sourceFolder, _destination, progress, _cancellation.Token));
|
||||
_succeeded = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
cancelled = true;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
error = exception.Message;
|
||||
}
|
||||
|
||||
_finished = true;
|
||||
if (cancelled && !IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelled)
|
||||
{
|
||||
Close(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PackProgress.Value = _succeeded ? 1 : PackProgress.Value;
|
||||
StatusText.Text = _succeeded
|
||||
? Localization.Instance.Get("Library.Pack.Done")
|
||||
: Localization.Instance.Format("Library.Pack.Failed", error ?? string.Empty);
|
||||
DeleteSourceCheck.IsEnabled = false;
|
||||
if (_succeeded && DeleteSourceCheck.IsChecked == true)
|
||||
{
|
||||
StatusText.Text = await DeleteSourceAsync();
|
||||
}
|
||||
|
||||
CloseButton.Content = Localization.Instance.Get("Library.Pack.Close");
|
||||
CloseButton.IsEnabled = true;
|
||||
}
|
||||
|
||||
// Deletes the game folder once the archive is confirmed to hold all of it. Returns the status
|
||||
// to show.
|
||||
private async Task<string> DeleteSourceAsync()
|
||||
{
|
||||
var loc = Localization.Instance;
|
||||
CloseButton.IsEnabled = false;
|
||||
StatusText.Text = loc.Get("Library.Pack.Verifying");
|
||||
try
|
||||
{
|
||||
var source = _sourceFolder;
|
||||
var destination = _destination;
|
||||
if (!File.Exists(Path.Combine(source, "eboot.bin")) ||
|
||||
!await Task.Run(() => ZArchiveWriter.MatchesFolder(destination, source)))
|
||||
{
|
||||
return loc.Get("Library.Pack.DeleteSkipped");
|
||||
}
|
||||
|
||||
StatusText.Text = loc.Get("Library.Pack.Deleting");
|
||||
await Task.Run(() => Directory.Delete(source, recursive: true));
|
||||
return loc.Get("Library.Pack.DoneDeleted");
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return loc.Format("Library.Pack.DeleteFailed", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatGigabytes(long bytes) => (bytes / (1024.0 * 1024 * 1024)).ToString("0.00");
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO.Enumeration;
|
||||
|
||||
namespace SharpEmu.HLE.FileSystem;
|
||||
|
||||
// A game run from a .zar mounts /app0 at a small host directory that holds the files host code reads
|
||||
// by path (the executable, its modules, sce_sys); every other /app0 path is served from the archive.
|
||||
// A host path under that directory that is not on disk resolves here: stat, listing and read-only
|
||||
// streams come from the archive, and a host consumer that needs a real file can extract just it.
|
||||
public static class App0Archive
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static ZArchive? _archive;
|
||||
private static string? _root;
|
||||
|
||||
public static bool IsMounted => Volatile.Read(ref _archive) is not null;
|
||||
|
||||
public static string? Root => Volatile.Read(ref _root);
|
||||
|
||||
public static void Mount(ZArchive archive, string root)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_archive?.Dispose();
|
||||
_root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(root));
|
||||
Volatile.Write(ref _archive, archive);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Unmount()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_archive?.Dispose();
|
||||
Volatile.Write(ref _archive, null);
|
||||
_root = null;
|
||||
}
|
||||
}
|
||||
|
||||
// The archive and the node a host path names, when it lies under the mount and the archive has it.
|
||||
public static bool TryResolve(string hostPath, out ZArchive archive, out int node)
|
||||
{
|
||||
archive = Volatile.Read(ref _archive)!;
|
||||
node = ZArchive.InvalidNode;
|
||||
var root = Volatile.Read(ref _root);
|
||||
if (archive is null || root is null || string.IsNullOrEmpty(hostPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string full;
|
||||
try
|
||||
{
|
||||
full = Path.TrimEndingDirectorySeparator(Path.GetFullPath(hostPath));
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string relative;
|
||||
if (string.Equals(full, root, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relative = string.Empty;
|
||||
}
|
||||
else if (full.Length > root.Length && full.StartsWith(root, StringComparison.OrdinalIgnoreCase) &&
|
||||
full[root.Length] is '\\' or '/')
|
||||
{
|
||||
relative = full[(root.Length + 1)..];
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
node = archive.LookUp(relative);
|
||||
return node != ZArchive.InvalidNode;
|
||||
}
|
||||
|
||||
public static bool TryGetFile(string hostPath, out ulong size)
|
||||
{
|
||||
size = 0;
|
||||
if (!TryResolve(hostPath, out var archive, out var node) || !archive.IsFile(node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size = archive.GetFileSize(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsDirectory(string hostPath) =>
|
||||
TryResolve(hostPath, out var archive, out var node) && archive.IsDirectory(node);
|
||||
|
||||
public static bool Exists(string hostPath) => TryResolve(hostPath, out _, out _);
|
||||
|
||||
// The names in the archive directory a host path names; empty when there is none.
|
||||
public static IReadOnlyList<ZArchive.Entry> List(string hostPath) =>
|
||||
TryResolve(hostPath, out var archive, out var node) ? archive.GetEntries(node) : [];
|
||||
|
||||
// A read-only, seekable stream over the archive file a host path names.
|
||||
public static ZArchiveFileStream? OpenRead(string hostPath) =>
|
||||
TryResolve(hostPath, out var archive, out var node) && archive.IsFile(node)
|
||||
? new ZArchiveFileStream(archive, node, hostPath)
|
||||
: null;
|
||||
|
||||
// Opens a host path for reading: from the archive when it is under the mount and not on disk,
|
||||
// otherwise as a host file.
|
||||
public static Stream OpenReadOrFile(string hostPath, FileShare share = FileShare.Read) =>
|
||||
!File.Exists(hostPath) && OpenRead(hostPath) is { } archived
|
||||
? archived
|
||||
: new FileStream(hostPath, FileMode.Open, FileAccess.Read, share);
|
||||
|
||||
// Writes the archive file a host path names to that path, for host code that opens files itself.
|
||||
// True when the file is on disk afterwards.
|
||||
public static bool Extract(string hostPath)
|
||||
{
|
||||
if (File.Exists(hostPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (OpenRead(hostPath) is not { } source)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using (source)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
WriteFile(source, hostPath);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Copies an archive file to a host path through a temporary name, so a partial copy is never seen.
|
||||
internal static void WriteFile(Stream source, string hostPath)
|
||||
{
|
||||
if (File.Exists(hostPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(hostPath))!);
|
||||
var temporary = hostPath + ".partial";
|
||||
using (var target = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
source.CopyTo(target, ZArchive.BlockSize);
|
||||
}
|
||||
|
||||
File.Move(temporary, hostPath, overwrite: true);
|
||||
}
|
||||
|
||||
// Every file in the mounted archive, as paths relative to the mount with '/' separators.
|
||||
public static List<string> ListAllFiles()
|
||||
{
|
||||
var files = new List<string>();
|
||||
if (Volatile.Read(ref _archive) is { } archive)
|
||||
{
|
||||
AddFiles(archive, archive.LookUp(string.Empty), string.Empty, files);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static void AddFiles(ZArchive archive, int directory, string prefix, List<string> files)
|
||||
{
|
||||
foreach (var entry in archive.GetEntries(directory))
|
||||
{
|
||||
var path = prefix + entry.Name;
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
AddFiles(archive, archive.LookUp(path), path + "/", files);
|
||||
}
|
||||
else
|
||||
{
|
||||
files.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Files under a host directory and all its subdirectories whose names match a pattern: those on
|
||||
// disk and, when the directory is under the mount, those in the archive.
|
||||
public static IEnumerable<string> EnumerateFilesRecursive(string hostDirectory, string pattern)
|
||||
{
|
||||
var found = Directory.Exists(hostDirectory)
|
||||
? Directory.EnumerateFiles(hostDirectory, pattern, SearchOption.AllDirectories).ToList()
|
||||
: [];
|
||||
var root = Root;
|
||||
if (root is null || !TryResolve(hostDirectory, out var archive, out var node) || !archive.IsDirectory(node))
|
||||
{
|
||||
return found;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(found, StringComparer.OrdinalIgnoreCase);
|
||||
var start = Path.GetRelativePath(root, Path.GetFullPath(hostDirectory));
|
||||
var prefix = start == "." ? string.Empty : start.Replace('\\', '/') + "/";
|
||||
var archived = new List<string>();
|
||||
AddFiles(archive, node, prefix, archived);
|
||||
foreach (var relative in archived)
|
||||
{
|
||||
var hostPath = Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (FileSystemName.MatchesSimpleExpression(pattern, Path.GetFileName(hostPath)) && seen.Add(hostPath))
|
||||
{
|
||||
found.Add(hostPath);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
// Extracts every file under the archive directory into the mount, for the files host code reads by path.
|
||||
public static void ExtractDirectory(string relative)
|
||||
{
|
||||
var root = Root ?? throw new InvalidOperationException("No archive is mounted.");
|
||||
var directory = Path.Combine(root, relative);
|
||||
foreach (var entry in List(directory))
|
||||
{
|
||||
var path = Path.Combine(relative, entry.Name);
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
ExtractDirectory(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
Extract(Path.Combine(root, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One archive file as a read-only, seekable stream; reads at any position go through the archive's
|
||||
// block cache. Name is the host path it was opened as.
|
||||
public sealed class ZArchiveFileStream : Stream
|
||||
{
|
||||
private readonly ZArchive _archive;
|
||||
private readonly int _node;
|
||||
private readonly long _length;
|
||||
private long _position;
|
||||
|
||||
public ZArchiveFileStream(ZArchive archive, int node, string name)
|
||||
{
|
||||
_archive = archive;
|
||||
_node = node;
|
||||
_length = (long)archive.GetFileSize(node);
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => true;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => _length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
set => _position = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
|
||||
// Reads at an offset without moving the position, like a positional read on a file handle.
|
||||
public int ReadAt(Span<byte> buffer, long offset) =>
|
||||
offset < 0 ? throw new ArgumentOutOfRangeException(nameof(offset)) : _archive.Read(_node, (ulong)offset, buffer);
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
var read = ReadAt(buffer, _position);
|
||||
_position += read;
|
||||
return read;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
var target = origin switch
|
||||
{
|
||||
SeekOrigin.Begin => offset,
|
||||
SeekOrigin.Current => _position + offset,
|
||||
SeekOrigin.End => _length + offset,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(origin)),
|
||||
};
|
||||
Position = target;
|
||||
return target;
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException("An archive file is read-only.");
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException("An archive file is read-only.");
|
||||
}
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using ZstdSharp;
|
||||
|
||||
namespace SharpEmu.HLE.FileSystem;
|
||||
|
||||
// A read-only ZArchive (.zar): a game folder packed into zstd-compressed 64 KiB blocks with an index,
|
||||
// so any byte range reads without unpacking the archive. Format by Exzap
|
||||
// (https://github.com/Exzap/ZArchive, MIT No Attribution); every multi-byte field is big-endian.
|
||||
//
|
||||
// Layout, read from the footer at the end of the file:
|
||||
// compressed data blocks of 64 KiB once decompressed; a block stored at exactly 64 KiB is raw
|
||||
// offset records per 16 blocks: the first block's offset (u64) and each block's size - 1 (u16)
|
||||
// names per node: a 1-byte length (or 2 bytes when the top bit is set) and the bytes
|
||||
// file tree 16-byte nodes; node 0 is the unnamed root directory
|
||||
public sealed class ZArchive : IDisposable
|
||||
{
|
||||
public const int BlockSize = 64 * 1024;
|
||||
private const int BlocksPerOffsetRecord = 16;
|
||||
private const int OffsetRecordBytes = 8 + 2 * BlocksPerOffsetRecord;
|
||||
private const int NodeBytes = 16;
|
||||
private const int FooterBytes = 16 * 6 + 32 + 8 + 4 + 4;
|
||||
private const uint FooterMagic = 0x169F52D6;
|
||||
private const uint FooterVersion1 = 0x61BF3A01;
|
||||
private const uint FileFlag = 0x80000000;
|
||||
private const uint NameOffsetMask = 0x7FFFFFFF;
|
||||
private const int CacheBlocks = 64;
|
||||
|
||||
public const int InvalidNode = -1;
|
||||
|
||||
private readonly FileStream _stream;
|
||||
private readonly OffsetRecord[] _offsetRecords;
|
||||
private readonly byte[] _names;
|
||||
private readonly Node[] _nodes;
|
||||
private readonly ulong _dataOffset;
|
||||
private readonly ulong _dataSize;
|
||||
private readonly object _gate = new();
|
||||
private readonly Decompressor _decompressor = new();
|
||||
private readonly byte[] _compressed = new byte[BlockSize];
|
||||
// Decompressed blocks, least recently used first.
|
||||
private readonly LinkedList<(ulong Block, byte[] Data)> _cache = new();
|
||||
private readonly Dictionary<ulong, LinkedListNode<(ulong Block, byte[] Data)>> _cached = new();
|
||||
|
||||
private readonly record struct OffsetRecord(ulong BaseOffset, ushort[] Sizes);
|
||||
|
||||
private readonly record struct Node(uint NameAndType, uint Word0, uint Word1, uint Word2)
|
||||
{
|
||||
public bool IsFile => (NameAndType & FileFlag) != 0;
|
||||
|
||||
public uint NameOffset => NameAndType & NameOffsetMask;
|
||||
|
||||
public ulong FileOffset => Word0 | ((ulong)(Word2 & 0xFFFF) << 32);
|
||||
|
||||
public ulong FileSize => Word1 | ((ulong)(Word2 & 0xFFFF0000) << 16);
|
||||
|
||||
public uint FirstChild => Word0;
|
||||
|
||||
public uint ChildCount => Word1;
|
||||
}
|
||||
|
||||
public readonly record struct Entry(string Name, bool IsDirectory, ulong Size);
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
private ZArchive(string path, FileStream stream, OffsetRecord[] offsetRecords, byte[] names, Node[] nodes, ulong dataOffset, ulong dataSize)
|
||||
{
|
||||
Path = path;
|
||||
_stream = stream;
|
||||
_offsetRecords = offsetRecords;
|
||||
_names = names;
|
||||
_nodes = nodes;
|
||||
_dataOffset = dataOffset;
|
||||
_dataSize = dataSize;
|
||||
}
|
||||
|
||||
public static bool IsArchivePath(string? path) =>
|
||||
path is not null && path.EndsWith(".zar", StringComparison.OrdinalIgnoreCase) && File.Exists(path);
|
||||
|
||||
// Null when the file is not a well-formed version-1 archive.
|
||||
public static ZArchive? Open(string path)
|
||||
{
|
||||
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1, FileOptions.RandomAccess);
|
||||
try
|
||||
{
|
||||
if (TryOpen(path, stream) is { } archive)
|
||||
{
|
||||
return archive;
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or ArgumentException or OverflowException)
|
||||
{
|
||||
}
|
||||
|
||||
stream.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ZArchive? TryOpen(string path, FileStream stream)
|
||||
{
|
||||
var length = (ulong)stream.Length;
|
||||
if (length <= FooterBytes)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Span<byte> footer = stackalloc byte[FooterBytes];
|
||||
ReadExactly(stream, length - FooterBytes, footer);
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(footer[(FooterBytes - 4)..]);
|
||||
var version = BinaryPrimitives.ReadUInt32BigEndian(footer[(FooterBytes - 8)..]);
|
||||
var totalSize = BinaryPrimitives.ReadUInt64BigEndian(footer[(FooterBytes - 16)..]);
|
||||
if (magic != FooterMagic || version != FooterVersion1 || totalSize != length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Six (offset, size) pairs: compressed data, offset records, names, file tree, metadata
|
||||
// directory and metadata; the last two are unused.
|
||||
var sections = new (ulong Offset, ulong Size)[6];
|
||||
for (var index = 0; index < sections.Length; index++)
|
||||
{
|
||||
var offset = BinaryPrimitives.ReadUInt64BigEndian(footer[(index * 16)..]);
|
||||
var size = BinaryPrimitives.ReadUInt64BigEndian(footer[(index * 16 + 8)..]);
|
||||
if (offset > length || size > length - offset)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
sections[index] = (offset, size);
|
||||
}
|
||||
|
||||
var (dataOffset, dataSize) = sections[0];
|
||||
var (recordsOffset, recordsSize) = sections[1];
|
||||
var (namesOffset, namesSize) = sections[2];
|
||||
var (treeOffset, treeSize) = sections[3];
|
||||
if (recordsSize == 0 || recordsSize % OffsetRecordBytes != 0 || recordsSize > uint.MaxValue ||
|
||||
namesSize > int.MaxValue || treeSize == 0 || treeSize % NodeBytes != 0 || treeSize > int.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var recordBytes = new byte[recordsSize];
|
||||
ReadExactly(stream, recordsOffset, recordBytes);
|
||||
var records = new OffsetRecord[recordsSize / OffsetRecordBytes];
|
||||
for (var index = 0; index < records.Length; index++)
|
||||
{
|
||||
var record = recordBytes.AsSpan(index * OffsetRecordBytes, OffsetRecordBytes);
|
||||
var sizes = new ushort[BlocksPerOffsetRecord];
|
||||
for (var block = 0; block < sizes.Length; block++)
|
||||
{
|
||||
sizes[block] = BinaryPrimitives.ReadUInt16BigEndian(record[(8 + block * 2)..]);
|
||||
}
|
||||
|
||||
records[index] = new OffsetRecord(BinaryPrimitives.ReadUInt64BigEndian(record), sizes);
|
||||
}
|
||||
|
||||
var names = new byte[namesSize];
|
||||
ReadExactly(stream, namesOffset, names);
|
||||
var treeBytes = new byte[treeSize];
|
||||
ReadExactly(stream, treeOffset, treeBytes);
|
||||
var nodes = new Node[treeSize / NodeBytes];
|
||||
for (var index = 0; index < nodes.Length; index++)
|
||||
{
|
||||
var node = treeBytes.AsSpan(index * NodeBytes, NodeBytes);
|
||||
nodes[index] = new Node(
|
||||
BinaryPrimitives.ReadUInt32BigEndian(node),
|
||||
BinaryPrimitives.ReadUInt32BigEndian(node[4..]),
|
||||
BinaryPrimitives.ReadUInt32BigEndian(node[8..]),
|
||||
BinaryPrimitives.ReadUInt32BigEndian(node[12..]));
|
||||
}
|
||||
|
||||
if (nodes[0].IsFile || NameOf(names, nodes[0].NameOffset) is not { Length: 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (!node.IsFile && ((ulong)node.FirstChild + node.ChildCount > (ulong)nodes.Length))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new ZArchive(path, stream, records, names, nodes, dataOffset, dataSize);
|
||||
}
|
||||
|
||||
// The node at the path, with '/' or '\' separators and names compared without case;
|
||||
// the root for an empty path; InvalidNode when it is missing.
|
||||
public int LookUp(string path)
|
||||
{
|
||||
var current = 0;
|
||||
foreach (var part in path.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var directory = _nodes[current];
|
||||
if (directory.IsFile)
|
||||
{
|
||||
return InvalidNode;
|
||||
}
|
||||
|
||||
var match = InvalidNode;
|
||||
for (var child = directory.FirstChild; child < directory.FirstChild + directory.ChildCount; child++)
|
||||
{
|
||||
if (string.Equals(NameOf(_names, _nodes[child].NameOffset), part, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
match = (int)child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match == InvalidNode)
|
||||
{
|
||||
return InvalidNode;
|
||||
}
|
||||
|
||||
current = match;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public bool IsFile(int node) => (uint)node < (uint)_nodes.Length && _nodes[node].IsFile;
|
||||
|
||||
public bool IsDirectory(int node) => (uint)node < (uint)_nodes.Length && !_nodes[node].IsFile;
|
||||
|
||||
public ulong GetFileSize(int node) => IsFile(node) ? _nodes[node].FileSize : 0;
|
||||
|
||||
public IReadOnlyList<Entry> GetEntries(int directory)
|
||||
{
|
||||
if (!IsDirectory(directory))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var node = _nodes[directory];
|
||||
var entries = new List<Entry>((int)node.ChildCount);
|
||||
for (var child = node.FirstChild; child < node.FirstChild + node.ChildCount; child++)
|
||||
{
|
||||
var entry = _nodes[child];
|
||||
if (NameOf(_names, entry.NameOffset) is { Length: > 0 } name)
|
||||
{
|
||||
entries.Add(new Entry(name, !entry.IsFile, entry.IsFile ? entry.FileSize : 0));
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Copies file bytes from offset into destination; returns the count read, short at the end of
|
||||
// the file. Throws InvalidDataException for a damaged block.
|
||||
public int Read(int file, ulong offset, Span<byte> destination)
|
||||
{
|
||||
if (!IsFile(file))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var node = _nodes[file];
|
||||
if (offset >= node.FileSize)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = (int)Math.Min((ulong)destination.Length, node.FileSize - offset);
|
||||
var position = node.FileOffset + offset;
|
||||
var written = 0;
|
||||
lock (_gate)
|
||||
{
|
||||
while (written < count)
|
||||
{
|
||||
var block = position / BlockSize;
|
||||
var within = (int)(position % BlockSize);
|
||||
var step = Math.Min(count - written, BlockSize - within);
|
||||
LoadBlock(block).AsSpan(within, step).CopyTo(destination[written..]);
|
||||
written += step;
|
||||
position += (ulong)step;
|
||||
}
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
private byte[] LoadBlock(ulong block)
|
||||
{
|
||||
if (_cached.TryGetValue(block, out var cached))
|
||||
{
|
||||
_cache.Remove(cached);
|
||||
_cache.AddLast(cached);
|
||||
return cached.Value.Data;
|
||||
}
|
||||
|
||||
var recordIndex = block / BlocksPerOffsetRecord;
|
||||
if (recordIndex >= (ulong)_offsetRecords.Length)
|
||||
{
|
||||
throw new InvalidDataException($"The archive block {block} is past its offset records.");
|
||||
}
|
||||
|
||||
var record = _offsetRecords[recordIndex];
|
||||
var sub = (int)(block % BlocksPerOffsetRecord);
|
||||
var offset = record.BaseOffset;
|
||||
for (var index = 0; index < sub; index++)
|
||||
{
|
||||
offset += (ulong)record.Sizes[index] + 1;
|
||||
}
|
||||
|
||||
var size = record.Sizes[sub] + 1;
|
||||
if (offset + (ulong)size > _dataSize)
|
||||
{
|
||||
throw new InvalidDataException($"The archive block {block} lies outside the compressed data.");
|
||||
}
|
||||
|
||||
byte[] data;
|
||||
LinkedListNode<(ulong Block, byte[] Data)> slot;
|
||||
if (_cache.Count >= CacheBlocks)
|
||||
{
|
||||
slot = _cache.First!;
|
||||
_cache.RemoveFirst();
|
||||
_cached.Remove(slot.Value.Block);
|
||||
data = slot.Value.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
data = new byte[BlockSize];
|
||||
slot = new LinkedListNode<(ulong Block, byte[] Data)>((block, data));
|
||||
}
|
||||
|
||||
if (size == BlockSize)
|
||||
{
|
||||
ReadExactly(_stream, _dataOffset + offset, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadExactly(_stream, _dataOffset + offset, _compressed.AsSpan(0, size));
|
||||
if (!_decompressor.TryUnwrap(_compressed.AsSpan(0, size), data, out var produced) || produced != BlockSize)
|
||||
{
|
||||
throw new InvalidDataException($"The archive block {block} does not decompress to {BlockSize} bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
slot.Value = (block, data);
|
||||
_cache.AddLast(slot);
|
||||
_cached[block] = slot;
|
||||
return data;
|
||||
}
|
||||
|
||||
// The name at the offset; empty for a missing or malformed one. A length of 128 or more takes a
|
||||
// second byte: (first & 0x7F) | (second << 7), as the archive writer stores it.
|
||||
private static string NameOf(byte[] names, uint offset)
|
||||
{
|
||||
if (offset == NameOffsetMask || offset >= (uint)names.Length)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var position = (int)offset;
|
||||
var length = names[position] & 0x7F;
|
||||
if ((names[position] & 0x80) != 0)
|
||||
{
|
||||
if (position + 1 >= names.Length)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
length |= names[position + 1] << 7;
|
||||
position += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
return position + length > names.Length ? string.Empty : Encoding.UTF8.GetString(names, position, length);
|
||||
}
|
||||
|
||||
private static void ReadExactly(FileStream stream, ulong offset, Span<byte> destination)
|
||||
{
|
||||
if (RandomAccess.Read(stream.SafeFileHandle, destination, (long)offset) != destination.Length)
|
||||
{
|
||||
throw new IOException("The archive ended before a section it lists.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stream.Dispose();
|
||||
_decompressor.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.FileSystem;
|
||||
|
||||
// Each .zar gets one host folder under user/zar-cache, named after the archive. /app0 is mounted
|
||||
// there while the game runs and the library reads the game's metadata from it. A stamp of the
|
||||
// archive's path, size and write time tells whether the files in it came from this archive; when
|
||||
// it differs, the folder starts over.
|
||||
public static class ZArchiveCache
|
||||
{
|
||||
private const string StampName = ".sharpemu-archive";
|
||||
|
||||
// The files the library shows for a game: its parameters, icon and key art.
|
||||
private static readonly string[] MetadataFiles =
|
||||
[
|
||||
"sce_sys/param.json",
|
||||
"sce_sys/icon0.png",
|
||||
"sce_sys/pic0.png",
|
||||
"sce_sys/pic1.png",
|
||||
"sce_sys/snd0.at9",
|
||||
];
|
||||
|
||||
private static string DefaultCacheParent => Path.Combine(AppContext.BaseDirectory, "user", "zar-cache");
|
||||
|
||||
// The cache folder an archive uses, without creating or checking it.
|
||||
public static string GetCacheRoot(string archivePath, string? cacheParent = null) =>
|
||||
Path.Combine(cacheParent ?? DefaultCacheParent, Path.GetFileNameWithoutExtension(archivePath));
|
||||
|
||||
public static string Prepare(string archivePath) => Prepare(archivePath, DefaultCacheParent);
|
||||
|
||||
public static string Prepare(string archivePath, string cacheParent)
|
||||
{
|
||||
archivePath = Path.GetFullPath(archivePath);
|
||||
var info = new FileInfo(archivePath);
|
||||
var cacheRoot = GetCacheRoot(archivePath, cacheParent);
|
||||
var stampPath = Path.Combine(cacheRoot, StampName);
|
||||
var stamp = $"{archivePath}|{info.Length}|{info.LastWriteTimeUtc.Ticks}";
|
||||
if (!File.Exists(stampPath) || File.ReadAllText(stampPath) != stamp)
|
||||
{
|
||||
if (Directory.Exists(cacheRoot))
|
||||
{
|
||||
Directory.Delete(cacheRoot, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(cacheRoot);
|
||||
File.WriteAllText(stampPath, stamp);
|
||||
}
|
||||
|
||||
return cacheRoot;
|
||||
}
|
||||
|
||||
// Extracts an archive's sce_sys metadata into its cache folder and returns the folder, or null
|
||||
// when the file is not a readable archive. The folder stands in for the game folder.
|
||||
public static string? ExtractMetadata(string archivePath, string? cacheParent = null)
|
||||
{
|
||||
using var archive = ZArchive.Open(archivePath);
|
||||
if (archive is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheRoot = cacheParent is null ? Prepare(archivePath) : Prepare(archivePath, cacheParent);
|
||||
foreach (var relative in MetadataFiles)
|
||||
{
|
||||
var node = archive.LookUp(relative);
|
||||
if (!archive.IsFile(node))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var hostPath = Path.Combine(cacheRoot, relative.Replace('/', Path.DirectorySeparatorChar));
|
||||
using var source = new ZArchiveFileStream(archive, node, hostPath);
|
||||
App0Archive.WriteFile(source, hostPath);
|
||||
}
|
||||
|
||||
return cacheRoot;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using ZstdSharp;
|
||||
|
||||
namespace SharpEmu.HLE.FileSystem;
|
||||
|
||||
// Packs a folder into a .zar the way the ZArchive writer lays one out: file data appended in order
|
||||
// and cut into 64 KiB zstd blocks (the last padded with zeros, a block that does not shrink stored
|
||||
// raw), the data section padded to 8 bytes, then offset records, names, a breadth-first file tree
|
||||
// with each directory's children sorted without case, empty metadata and the footer. Data streams
|
||||
// through one block at a time, so a large game needs little memory.
|
||||
public static class ZArchiveWriter
|
||||
{
|
||||
private const int CompressionLevel = 6;
|
||||
private const uint Version = 0x61BF3A01;
|
||||
private const uint Magic = 0x169F52D6;
|
||||
|
||||
public readonly record struct Progress(long BytesDone, long BytesTotal, int FilesDone, int FilesTotal);
|
||||
|
||||
private sealed class PathNode(string name, bool isFile)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public bool IsFile { get; } = isFile;
|
||||
public List<PathNode> Children { get; } = [];
|
||||
public ulong FileOffset { get; set; }
|
||||
public ulong FileSize { get; set; }
|
||||
public uint NameOffset { get; set; }
|
||||
public uint FirstChild { get; set; }
|
||||
}
|
||||
|
||||
// Writes the archive through a temporary name and moves it into place only when complete, so a
|
||||
// cancelled or failed pack leaves no partial .zar behind.
|
||||
public static void Pack(
|
||||
string sourceDirectory,
|
||||
string destinationPath,
|
||||
IProgress<Progress>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var source = Path.GetFullPath(sourceDirectory);
|
||||
var destination = Path.GetFullPath(destinationPath);
|
||||
if (destination.StartsWith(Path.TrimEndingDirectorySeparator(source) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new IOException("The archive cannot be written inside the folder being packed.");
|
||||
}
|
||||
|
||||
var files = Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)
|
||||
.Select(path => new FileInfo(path))
|
||||
.OrderBy(info => info.FullName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var bytesTotal = files.Sum(info => info.Length);
|
||||
var temporary = destination + ".partial";
|
||||
try
|
||||
{
|
||||
using (var output = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20))
|
||||
{
|
||||
Write(source, files, bytesTotal, output, progress, cancellationToken);
|
||||
}
|
||||
|
||||
File.Move(temporary, destination, overwrite: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(temporary);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the archive opens and holds every file of the folder at the same size: the check
|
||||
// that must pass before the folder may be deleted.
|
||||
public static bool MatchesFolder(string archivePath, string sourceDirectory)
|
||||
{
|
||||
using var archive = ZArchive.Open(archivePath);
|
||||
if (archive is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var source = Path.GetFullPath(sourceDirectory);
|
||||
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var node = archive.LookUp(Path.GetRelativePath(source, file));
|
||||
if (!archive.IsFile(node) || archive.GetFileSize(node) != (ulong)new FileInfo(file).Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Write(
|
||||
string source,
|
||||
List<FileInfo> files,
|
||||
long bytesTotal,
|
||||
FileStream output,
|
||||
IProgress<Progress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var root = new PathNode(string.Empty, isFile: false);
|
||||
var block = new byte[ZArchive.BlockSize];
|
||||
var filled = 0;
|
||||
var blockIndex = 0;
|
||||
var records = new List<(ulong BaseOffset, ushort[] Sizes)>();
|
||||
using var compressor = new Compressor(CompressionLevel);
|
||||
ulong dataOffset = 0;
|
||||
var lastReport = 0L;
|
||||
|
||||
void FlushBlock()
|
||||
{
|
||||
var compressed = compressor.Wrap(block);
|
||||
var offset = (ulong)output.Position;
|
||||
int size;
|
||||
if (compressed.Length >= ZArchive.BlockSize)
|
||||
{
|
||||
output.Write(block);
|
||||
size = ZArchive.BlockSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Write(compressed);
|
||||
size = compressed.Length;
|
||||
}
|
||||
|
||||
if (blockIndex % 16 == 0)
|
||||
{
|
||||
records.Add((offset, new ushort[16]));
|
||||
}
|
||||
|
||||
records[^1].Sizes[blockIndex % 16] = (ushort)(size - 1);
|
||||
blockIndex++;
|
||||
filled = 0;
|
||||
}
|
||||
|
||||
for (var index = 0; index < files.Count; index++)
|
||||
{
|
||||
var parts = Path.GetRelativePath(source, files[index].FullName).Split(Path.DirectorySeparatorChar);
|
||||
var directory = root;
|
||||
foreach (var part in parts[..^1])
|
||||
{
|
||||
var next = directory.Children.Find(child => string.Equals(child.Name, part, StringComparison.OrdinalIgnoreCase));
|
||||
if (next is null)
|
||||
{
|
||||
next = new PathNode(part, isFile: false);
|
||||
directory.Children.Add(next);
|
||||
}
|
||||
|
||||
directory = next;
|
||||
}
|
||||
|
||||
var file = new PathNode(parts[^1], isFile: true) { FileOffset = dataOffset };
|
||||
using (var input = new FileStream(files[index].FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1 << 20))
|
||||
{
|
||||
int read;
|
||||
while ((read = input.Read(block, filled, block.Length - filled)) > 0)
|
||||
{
|
||||
filled += read;
|
||||
dataOffset += (ulong)read;
|
||||
file.FileSize += (ulong)read;
|
||||
if (filled == block.Length)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
FlushBlock();
|
||||
if ((long)dataOffset - lastReport >= 16 << 20)
|
||||
{
|
||||
lastReport = (long)dataOffset;
|
||||
progress?.Report(new Progress(lastReport, bytesTotal, index, files.Count));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
directory.Children.Add(file);
|
||||
}
|
||||
|
||||
if (filled > 0)
|
||||
{
|
||||
Array.Clear(block, filled, block.Length - filled);
|
||||
FlushBlock();
|
||||
}
|
||||
|
||||
var dataSize = (ulong)output.Position;
|
||||
while (output.Position % 8 != 0)
|
||||
{
|
||||
output.WriteByte(0);
|
||||
}
|
||||
|
||||
var sections = new (ulong Offset, ulong Size)[6];
|
||||
sections[0] = (0, dataSize);
|
||||
var start = (ulong)output.Position;
|
||||
foreach (var (baseOffset, sizes) in records)
|
||||
{
|
||||
WriteU64(output, baseOffset);
|
||||
foreach (var size in sizes)
|
||||
{
|
||||
WriteU16(output, size);
|
||||
}
|
||||
}
|
||||
|
||||
sections[1] = (start, (ulong)output.Position - start);
|
||||
|
||||
// Names in breadth-first order, as the reference writer interns them.
|
||||
var ordered = new List<PathNode>();
|
||||
var queue = new Queue<PathNode>();
|
||||
queue.Enqueue(root);
|
||||
var nextIndex = 1u;
|
||||
while (queue.TryDequeue(out var node))
|
||||
{
|
||||
ordered.Add(node);
|
||||
if (node.IsFile)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
node.Children.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||
node.FirstChild = nextIndex;
|
||||
nextIndex += (uint)node.Children.Count;
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
queue.Enqueue(child);
|
||||
}
|
||||
}
|
||||
|
||||
start = (ulong)output.Position;
|
||||
foreach (var node in ordered.Skip(1))
|
||||
{
|
||||
node.NameOffset = (uint)((ulong)output.Position - start);
|
||||
var name = Encoding.UTF8.GetBytes(node.Name);
|
||||
if (name.Length >= 0x8000)
|
||||
{
|
||||
throw new IOException($"The name '{node.Name}' is too long for an archive.");
|
||||
}
|
||||
|
||||
if (name.Length >= 0x80)
|
||||
{
|
||||
output.WriteByte((byte)((name.Length & 0x7F) | 0x80));
|
||||
output.WriteByte((byte)(name.Length >> 7));
|
||||
}
|
||||
else
|
||||
{
|
||||
output.WriteByte((byte)name.Length);
|
||||
}
|
||||
|
||||
output.Write(name);
|
||||
}
|
||||
|
||||
sections[2] = (start, (ulong)output.Position - start);
|
||||
start = (ulong)output.Position;
|
||||
foreach (var node in ordered)
|
||||
{
|
||||
var nameOffset = ReferenceEquals(node, root) ? 0x7FFFFFFFu : node.NameOffset;
|
||||
WriteU32(output, nameOffset | (node.IsFile ? 0x80000000u : 0));
|
||||
if (node.IsFile)
|
||||
{
|
||||
WriteU32(output, (uint)node.FileOffset);
|
||||
WriteU32(output, (uint)node.FileSize);
|
||||
WriteU32(output, (uint)((node.FileOffset >> 32) & 0xFFFF) | (uint)((node.FileSize >> 16) & 0xFFFF0000));
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteU32(output, node.FirstChild);
|
||||
WriteU32(output, (uint)node.Children.Count);
|
||||
WriteU32(output, 0);
|
||||
}
|
||||
}
|
||||
|
||||
sections[3] = (start, (ulong)output.Position - start);
|
||||
sections[4] = ((ulong)output.Position, 0);
|
||||
sections[5] = ((ulong)output.Position, 0);
|
||||
foreach (var (offset, size) in sections)
|
||||
{
|
||||
WriteU64(output, offset);
|
||||
WriteU64(output, size);
|
||||
}
|
||||
|
||||
output.Write(new byte[32]);
|
||||
WriteU64(output, (ulong)output.Position + 8 + 4 + 4);
|
||||
WriteU32(output, Version);
|
||||
WriteU32(output, Magic);
|
||||
progress?.Report(new Progress(bytesTotal, bytesTotal, files.Count, files.Count));
|
||||
}
|
||||
|
||||
private static void WriteU16(Stream stream, ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[2];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(bytes, value);
|
||||
stream.Write(bytes);
|
||||
}
|
||||
|
||||
private static void WriteU32(Stream stream, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytes, value);
|
||||
stream.Write(bytes);
|
||||
}
|
||||
|
||||
private static void WriteU64(Stream stream, ulong value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64BigEndian(bytes, value);
|
||||
stream.Write(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
private readonly PageGuard _pages;
|
||||
private readonly ReaderWriterLockSlim _spansLock = new();
|
||||
private readonly SpanSet _spans = new();
|
||||
private SpanSnapshot _spanSnapshot = SpanSnapshot.Empty;
|
||||
private long _spanVersion;
|
||||
|
||||
public long SpanVersion => Interlocked.Read(ref _spanVersion);
|
||||
|
|
@ -153,15 +154,7 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
_spansLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _spans.Contains(address, size);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_spansLock.ExitReadLock();
|
||||
}
|
||||
return Volatile.Read(ref _spanSnapshot).Contains(address, size);
|
||||
}
|
||||
|
||||
public void ForEachSpan(Action<ulong, ulong> visit)
|
||||
|
|
@ -207,6 +200,7 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
try
|
||||
{
|
||||
_spans.Add(address, size);
|
||||
Volatile.Write(ref _spanSnapshot, _spans.Snapshot());
|
||||
Interlocked.Increment(ref _spanVersion);
|
||||
}
|
||||
finally
|
||||
|
|
@ -280,6 +274,7 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
try
|
||||
{
|
||||
_spans.Remove(address, size);
|
||||
Volatile.Write(ref _spanSnapshot, _spans.Snapshot());
|
||||
Interlocked.Increment(ref _spanVersion);
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
|
|
@ -198,6 +198,14 @@ public sealed class GuestPageTracker
|
|||
if (Volatile.Read(ref _regions[index]) is { } region)
|
||||
{
|
||||
tracked = true;
|
||||
if (region.IsCpuDirtyThroughout(offset, bytes))
|
||||
{
|
||||
remaining -= bytes;
|
||||
offset = 0;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
using (region.Lock.Hold())
|
||||
{
|
||||
if (region.IsModified(WriteOrigin.Gpu, offset, bytes))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,22 @@
|
|||
|
||||
namespace SharpEmu.HLE.GpuMemory;
|
||||
|
||||
public sealed class SpanSnapshot(ulong[] starts, ulong[] ends)
|
||||
{
|
||||
public static readonly SpanSnapshot Empty = new([], []);
|
||||
|
||||
public bool Contains(ulong address, ulong size)
|
||||
{
|
||||
var index = Array.BinarySearch(starts, address);
|
||||
if (index < 0)
|
||||
{
|
||||
index = ~index - 1;
|
||||
}
|
||||
|
||||
return index >= 0 && ends[index] >= address + size;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SpanSet
|
||||
{
|
||||
private readonly SortedList<ulong, ulong> _spans = new();
|
||||
|
|
@ -79,6 +95,19 @@ public sealed class SpanSet
|
|||
return index > 0 && _spans.GetValueAtIndex(index - 1) > address;
|
||||
}
|
||||
|
||||
public SpanSnapshot Snapshot()
|
||||
{
|
||||
var starts = new ulong[_spans.Count];
|
||||
var ends = new ulong[_spans.Count];
|
||||
for (var index = 0; index < starts.Length; index++)
|
||||
{
|
||||
starts[index] = _spans.GetKeyAtIndex(index);
|
||||
ends[index] = _spans.GetValueAtIndex(index);
|
||||
}
|
||||
|
||||
return new SpanSnapshot(starts, ends);
|
||||
}
|
||||
|
||||
public void ForEach(Action<ulong, ulong> visit)
|
||||
{
|
||||
foreach (var (start, end) in _spans)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,20 @@ public sealed class TrackedRegion
|
|||
return GetDirtyMask(side).AnyInRange(start, end);
|
||||
}
|
||||
|
||||
public bool IsCpuDirtyThroughout(ulong offset, ulong size)
|
||||
{
|
||||
var (start, end) = GetPageRange(BaseAddress + offset, size);
|
||||
for (var page = start; page < end; page++)
|
||||
{
|
||||
if (!_cpuDirty.Get(page))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsCpuWriteHot(ulong offset, ulong size)
|
||||
{
|
||||
var (start, end) = GetPageRange(BaseAddress + offset, size);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ public sealed class GuestSpaceOwner : IDisposable
|
|||
// Reads through a view that covers the whole range without any lock; false otherwise.
|
||||
public bool TryReadSingleView(ulong address, Span<byte> data) => _views.TryReadSingleView(address, data);
|
||||
|
||||
public bool TryWriteSingleView(ulong address, ReadOnlySpan<byte> data) => _views.TryWriteSingleView(address, data);
|
||||
|
||||
public object AliasSnapshot => _views.AliasSnapshot;
|
||||
|
||||
public bool TryEnterAliasAccess() => _views.TryEnterAliasAccess();
|
||||
|
|
|
|||
|
|
@ -118,6 +118,24 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool TryWriteSingleView(ulong address, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (!TryAccessSingleView(address, (ulong)data.Length, out var target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
data.CopyTo(new Span<byte>((void*)target, data.Length));
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadBacking(ulong address, Span<byte> data)
|
||||
{
|
||||
if (TryAccessSingleView(address, (ulong)data.Length, out var source))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.SDL3-CS" />
|
||||
<PackageReference Include="ZstdSharp.Port" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -136,12 +136,7 @@ public static partial class AgcExports
|
|||
var groupCountZ = (uint)ctx[CpuRegister.Rcx];
|
||||
var modifier = (uint)ctx[CpuRegister.R8];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItDispatchDirect, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, groupCountX) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, groupCountY) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, groupCountZ) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, DirectDispatchInitiator(modifier)))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(5, ItDispatchDirect, 0), groupCountX, groupCountY, groupCountZ, DirectDispatchInitiator(modifier)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -166,9 +161,7 @@ public static partial class AgcExports
|
|||
{
|
||||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RAcbReset)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 0))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItNop, RAcbReset), 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -198,15 +191,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RAcquireMem)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 0x8000_0000u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(baseAddress >> 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, gcrControl))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(8, ItNop, RAcquireMem), 0x8000_0000u, noSize ? 0 : (uint)(sizeBytes >> 8), 0, (uint)(baseAddress >> 8), 0, pollCycles / 40, gcrControl))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -301,11 +286,7 @@ public static partial class AgcExports
|
|||
var argumentsAddress = ctx[CpuRegister.Rsi];
|
||||
var modifier = (uint)ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItDispatchIndirect, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)argumentsAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(argumentsAddress >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (modifier & 0xA038u) | 0x41u))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(4, ItDispatchIndirect, 0), (uint)argumentsAddress, (uint)(argumentsAddress >> 32), (modifier & 0xA038u) | 0x41u))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -360,18 +341,7 @@ public static partial class AgcExports
|
|||
var packetAddressValue = interrupt == 4 ? 0UL : destinationAddress;
|
||||
var packetData = interrupt == 4 ? 0UL : data;
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RReleaseMem)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, action | (cachePolicy << 8)) ||
|
||||
!TryWriteUInt32(
|
||||
ctx,
|
||||
commandAddress + 8,
|
||||
gcrControl | (dataSelection << 16) | (interrupt << 24)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)packetAddressValue) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(packetAddressValue >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)packetData) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(packetData >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, interruptContextId & 0x07FF_FFFFu))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(8, ItNop, RReleaseMem), action | (cachePolicy << 8), gcrControl | (dataSelection << 16) | (interrupt << 24), (uint)packetAddressValue, (uint)(packetAddressValue >> 32), (uint)packetData, (uint)(packetData >> 32), interruptContextId & 0x07FF_FFFFu))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -397,9 +367,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RDrawReset)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 0))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItNop, RDrawReset), 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -448,12 +416,7 @@ public static partial class AgcExports
|
|||
(resetCounters << 18) |
|
||||
(counterMask << 10) |
|
||||
(counterSelect << 2);
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItGetLodStats, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, control) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)destinationAddress & ~0x3Fu) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(destinationAddress >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, packetControl))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(5, ItGetLodStats, 0), control, (uint)destinationAddress & ~0x3Fu, (uint)(destinationAddress >> 32), packetControl))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -493,15 +456,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RAcquireMem)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (engine << 31) | cbDbOp) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(baseAddress >> 8)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, gcrControl))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(8, ItNop, RAcquireMem), (engine << 31) | cbDbOp, noSize ? 0 : (uint)(sizeBytes >> 8), 0, (uint)(baseAddress >> 8), 0, pollCycles / 40, gcrControl))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -631,16 +586,7 @@ public static partial class AgcExports
|
|||
var control = 0x10u |
|
||||
compareFunction |
|
||||
(writeSpace == 1 ? 0x100u : 0u);
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 9, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(9, ItCondWrite, RZero)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, control) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)readAddress & ~0x3u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(readAddress >> 32) & 0xFFFFu) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, reference) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, mask) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)writeAddress & ~0x3u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 28, (uint)(writeAddress >> 32) & 0xFFFFu) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 32, writeValue))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(9, ItCondWrite, RZero), control, (uint)readAddress & ~0x3u, (uint)(readAddress >> 32) & 0xFFFFu, reference, mask, (uint)writeAddress & ~0x3u, (uint)(writeAddress >> 32) & 0xFFFFu, writeValue))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -752,9 +698,7 @@ public static partial class AgcExports
|
|||
var address = ctx[CpuRegister.Rdx];
|
||||
var reference = ctx[CpuRegister.Rcx];
|
||||
if (commandBufferAddress == 0 || size > 1 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RZero)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 0))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItNop, RZero), 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -790,11 +734,7 @@ public static partial class AgcExports
|
|||
var baseIndex = (uint)ctx[CpuRegister.Rsi];
|
||||
var address = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItSetBase, 0) | (baseIndex << 1)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 1) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)address & ~7u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(address >> 32)))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(4, ItSetBase, 0) | (baseIndex << 1), 1, (uint)address & ~7u, (uint)(address >> 32)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -813,10 +753,7 @@ public static partial class AgcExports
|
|||
var dataOffset = (uint)ctx[CpuRegister.Rsi];
|
||||
var modifier = (uint)ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItDispatchIndirect, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, dataOffset) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (modifier & 0xA038u) | 0x41u))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(3, ItDispatchIndirect, 0), dataOffset, (modifier & 0xA038u) | 0x41u))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -884,9 +821,7 @@ public static partial class AgcExports
|
|||
{
|
||||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RPopMarker)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 0))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItNop, RPopMarker), 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,21 @@ public static partial class AgcExports
|
|||
|
||||
internal sealed record RetainedTargetlessDraw(RegisterBanks Banks, TargetlessDrawArguments Arguments);
|
||||
|
||||
// A draw or dispatch the translation deferred, with the banks it was issued under.
|
||||
internal abstract record DeferredWork;
|
||||
|
||||
// Takes the deferred work of a command stream interpreted on its own thread.
|
||||
internal interface IDeferredWorkSink
|
||||
{
|
||||
// True on the thread that interprets the stream; elsewhere the translation runs work itself.
|
||||
bool IsProducerThread { get; }
|
||||
|
||||
// The non-draw items handed over so far; a draw's resolution waits until they ran.
|
||||
long Gate { get; }
|
||||
|
||||
void EnqueueDeferred(DeferredWork work, bool isDraw);
|
||||
}
|
||||
|
||||
// The snapshots the submit-time prepass captured for one submission.
|
||||
private sealed record SubmittedGeometrySnapshots(
|
||||
Dictionary<ulong, SubmittedIndexSnapshot>? IndexSnapshots,
|
||||
|
|
@ -160,9 +175,18 @@ public static partial class AgcExports
|
|||
{
|
||||
_pipelines = new ShaderPipelineCache(_context, pipelineHost, GuestGpu.Current, CreateShaderHeaderRegistry(_context));
|
||||
_executor = new RenderExecutor(renderHost, _pipelines);
|
||||
if (host is IConcurrentResolveHost concurrentHost)
|
||||
{
|
||||
_parallel = new ParallelProgramPrefetch(_executor, renderHost, concurrentHost);
|
||||
_executor.Prefetch = _parallel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves the programs of deferred draws ahead of them, for a host that answers reads from
|
||||
// another thread; draws are deferred whenever it exists.
|
||||
private readonly ParallelProgramPrefetch? _parallel;
|
||||
|
||||
// The context bank of the queue whose slice runs now; the Metal host rebuilds its records from it.
|
||||
internal ContextRegisters? CurrentContextRegisters => _current?.TypedRegisters?.Context;
|
||||
|
||||
|
|
@ -196,17 +220,194 @@ public static partial class AgcExports
|
|||
}
|
||||
}
|
||||
|
||||
private sealed record PendingDraw(
|
||||
ulong SubmitId,
|
||||
SubmittedDcbState State,
|
||||
RegisterBanks Banks,
|
||||
bool Indexed,
|
||||
DrawIndexedArguments IndexedArguments,
|
||||
DrawAutoArguments AutoArguments,
|
||||
SubmittedVertexSnapshot? VertexSnapshot,
|
||||
SubmittedIndexSnapshot? IndexSnapshot,
|
||||
ParallelProgramPrefetch.Job? Job) : DeferredWork;
|
||||
|
||||
private sealed record PendingDispatch(
|
||||
ulong SubmitId,
|
||||
SubmittedDcbState State,
|
||||
RegisterBanks Banks,
|
||||
uint GroupsX,
|
||||
uint GroupsY,
|
||||
uint GroupsZ,
|
||||
uint DispatchInitiator,
|
||||
ulong IndirectArgumentsAddress) : DeferredWork;
|
||||
|
||||
// With the command stream on its own thread, its draws and dispatches reach the render
|
||||
// thread through the sink, which runs them with RunDeferred in stream order.
|
||||
internal IDeferredWorkSink? Sink { get; set; }
|
||||
|
||||
private bool Producing => Sink is { IsProducerThread: true };
|
||||
|
||||
// The queue state of the work the render thread runs now; _current belongs to the thread
|
||||
// interpreting the stream.
|
||||
private SubmittedDcbState? _executing;
|
||||
|
||||
public void RunDeferred(DeferredWork work)
|
||||
{
|
||||
switch (work)
|
||||
{
|
||||
case PendingDraw draw:
|
||||
{
|
||||
var executing = _executing;
|
||||
_executing = draw.State;
|
||||
RecordKnownColorTargets(draw.State, draw.Banks);
|
||||
if (_parallel is not null)
|
||||
{
|
||||
_parallel.Current = draw.Job;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RunExecutorDraw(draw.SubmitId, draw.State, draw.Banks, draw.Indexed, draw.IndexedArguments, draw.AutoArguments,
|
||||
clearSnapshots: false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = executing;
|
||||
if (_parallel is not null)
|
||||
{
|
||||
_parallel.Current = null;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PendingDispatch dispatch:
|
||||
{
|
||||
var executing = _executing;
|
||||
_executing = dispatch.State;
|
||||
try
|
||||
{
|
||||
ExecuteDispatch(_executor!, dispatch.SubmitId, dispatch.Banks, dispatch.GroupsX, dispatch.GroupsY, dispatch.GroupsZ,
|
||||
dispatch.DispatchInitiator, dispatch.IndirectArgumentsAddress);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = executing;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw _host.Fatal($"The deferred work is unknown: {work.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
// The resolution worker, so the render thread can order it with the stream's other work.
|
||||
internal ParallelProgramPrefetch? Prefetch => _parallel;
|
||||
|
||||
private readonly Queue<PendingDraw> _pendingDraws = new();
|
||||
|
||||
public bool HasPendingDraws => _pendingDraws.Count != 0;
|
||||
|
||||
private bool _draining;
|
||||
|
||||
// Runs every deferred draw in issue order with the banks and snapshots it was issued with.
|
||||
// A draw calls back into the host, whose entry points drain too: those calls run inside
|
||||
// the draw and must not start the next one.
|
||||
public void DrainPendingDraws()
|
||||
{
|
||||
if (_draining)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_draining = true;
|
||||
try
|
||||
{
|
||||
DrainPendingDrawsCore();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_draining = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainPendingDrawsCore()
|
||||
{
|
||||
while (_pendingDraws.TryDequeue(out var pending))
|
||||
{
|
||||
var current = _current;
|
||||
var state = pending.State;
|
||||
_current = state;
|
||||
state.CurrentVertexSnapshot = pending.VertexSnapshot;
|
||||
state.CurrentIndexSnapshot = pending.IndexSnapshot;
|
||||
if (_parallel is not null)
|
||||
{
|
||||
_parallel.Current = pending.Job;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RunExecutorDraw(pending.SubmitId, state, pending.Banks, pending.Indexed, pending.IndexedArguments, pending.AutoArguments);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_current = current;
|
||||
if (_parallel is not null)
|
||||
{
|
||||
_parallel.Current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What follows a drain may change guest memory and cache state the worker reads.
|
||||
_parallel?.WaitIdle();
|
||||
}
|
||||
|
||||
private bool TryRunExecutorDraw(ulong submitId, SubmittedDcbState state, bool indexed, in DrawIndexedArguments indexedArguments, in DrawAutoArguments autoArguments)
|
||||
{
|
||||
if (_executor is not { } executor)
|
||||
if (_executor is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var banks = RequireTypedRegisters(state);
|
||||
RecordKnownColorTargets(state, banks);
|
||||
var producing = Producing;
|
||||
if (!producing)
|
||||
{
|
||||
// A draw run by the render thread records its targets there.
|
||||
RecordKnownColorTargets(state, banks);
|
||||
}
|
||||
|
||||
state.FrameDrawCount++;
|
||||
state.SawIndexedDraw |= indexed;
|
||||
if (_parallel is not null || producing)
|
||||
{
|
||||
var snapshot = banks.Snapshot();
|
||||
var pending = new PendingDraw(submitId, state, snapshot, indexed, indexedArguments, autoArguments,
|
||||
state.CurrentVertexSnapshot, state.CurrentIndexSnapshot, _parallel?.Submit(snapshot, producing ? Sink!.Gate : 0));
|
||||
if (producing)
|
||||
{
|
||||
Sink!.EnqueueDeferred(pending, isDraw: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingDraws.Enqueue(pending);
|
||||
}
|
||||
|
||||
state.CurrentIndexSnapshot = null;
|
||||
state.CurrentVertexSnapshot = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
RunExecutorDraw(submitId, state, banks, indexed, indexedArguments, autoArguments);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RunExecutorDraw(ulong submitId, SubmittedDcbState state, RegisterBanks banks, bool indexed, in DrawIndexedArguments indexedArguments, in DrawAutoArguments autoArguments,
|
||||
bool clearSnapshots = true)
|
||||
{
|
||||
var executor = _executor!;
|
||||
var drawStarted = DcbParseProfile.Begin();
|
||||
try
|
||||
{
|
||||
|
|
@ -222,17 +423,19 @@ public static partial class AgcExports
|
|||
finally
|
||||
{
|
||||
DcbParseProfile.RecordDraw(drawStarted);
|
||||
state.CurrentIndexSnapshot = null;
|
||||
state.CurrentVertexSnapshot = null;
|
||||
// The snapshot fields belong to the interpreting thread when it is another one.
|
||||
if (clearSnapshots)
|
||||
{
|
||||
state.CurrentIndexSnapshot = null;
|
||||
state.CurrentVertexSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keeps the banks the draw was issued under; the flip replays it into the display buffer.
|
||||
internal void RetainTargetlessDraw(RegisterBanks banks, in TargetlessDrawArguments arguments)
|
||||
{
|
||||
var state = RequireCurrent();
|
||||
var state = _executing ?? RequireCurrent();
|
||||
if (RenderTrace.Enabled)
|
||||
{
|
||||
RenderTrace.Write($"TargetlessRetention submit={arguments.SubmitId} replaced={state.RetainedTargetlessDraw is not null} previousSubmit={state.RetainedTargetlessDraw?.Arguments.SubmitId} export=0x{banks.Shader.Vertex.ExportAddress:X16} pixel=0x{banks.Shader.Pixel.Address:X16}");
|
||||
|
|
@ -363,22 +566,35 @@ public static partial class AgcExports
|
|||
{
|
||||
var banks = RequireTypedRegisters(state);
|
||||
state.FrameDispatchCount++;
|
||||
var executorStarted = DcbParseProfile.Begin();
|
||||
try
|
||||
if (Producing)
|
||||
{
|
||||
executor.Dispatch(submitId, banks, endX, endY, endZ, dispatchInitiator, indirectArgumentsAddress);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DcbParseProfile.RecordDispatch(executorStarted);
|
||||
var snapshot = banks.Snapshot();
|
||||
Sink!.EnqueueDeferred(new PendingDispatch(submitId, state, snapshot, endX, endY, endZ, dispatchInitiator, indirectArgumentsAddress),
|
||||
isDraw: false);
|
||||
return;
|
||||
}
|
||||
|
||||
ExecuteDispatch(executor, submitId, banks, endX, endY, endZ, dispatchInitiator, indirectArgumentsAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
throw _host.Fatal("The command stream has no render executor.");
|
||||
}
|
||||
|
||||
private static void ExecuteDispatch(RenderExecutor executor, ulong submitId, RegisterBanks banks, uint endX, uint endY, uint endZ,
|
||||
uint dispatchInitiator, ulong indirectArgumentsAddress)
|
||||
{
|
||||
var executorStarted = DcbParseProfile.Begin();
|
||||
try
|
||||
{
|
||||
executor.Dispatch(submitId, banks, endX, endY, endZ, dispatchInitiator, indirectArgumentsAddress);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DcbParseProfile.RecordDispatch(executorStarted);
|
||||
}
|
||||
}
|
||||
|
||||
// The retained draw runs into the display buffer with the target words it was last bound with.
|
||||
private void ReplayRetainedTargetlessDraw(RenderExecutor executor, SubmittedDcbState state, int handle, int displayBufferIndex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -249,11 +249,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)targetDwords & 0xFFFFFu))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(4, ItIndirectBuffer, RZero), (uint)(target & 0xFFFF_FFFFUL), (uint)((target >> 32) & 0xFFFFUL), (uint)targetDwords & 0xFFFFFu))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,16 +46,7 @@ public static partial class AgcExports
|
|||
uint perInstanceObjectIdSupport)
|
||||
{
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItSetUconfigRegIndex, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, 0x2000_0000u | VgtIndexType) ||
|
||||
!TryWriteUInt32(
|
||||
ctx,
|
||||
commandAddress + 8,
|
||||
0x400u |
|
||||
(indexSize & 0x3u) |
|
||||
((cachePolicy & 0x3u) << 6) |
|
||||
((perInstanceObjectIdSupport & 0x1u) << 14)))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(3, ItSetUconfigRegIndex, 0), 0x2000_0000u | VgtIndexType, 0x400u | (indexSize & 0x3u) | ((cachePolicy & 0x3u) << 6) | ((perInstanceObjectIdSupport & 0x1u) << 14)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -77,9 +68,7 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
if (commandBufferAddress == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RIndexCount)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, indexCount))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItNop, RIndexCount), indexCount))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -112,9 +101,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNumInstances, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, instanceCount))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItNumInstances, 0), instanceCount))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -140,12 +127,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) ||
|
||||
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 8, (uint)(indexAddress >> 32)) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 12, Pm4(2, ItIndexBufferSize, 0)) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 16, indexCount))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var baseCommand, Pm4(3, ItIndexBase, 0), (uint)indexAddress, (uint)(indexAddress >> 32), Pm4(2, ItIndexBufferSize, 0), indexCount))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -155,13 +137,7 @@ public static partial class AgcExports
|
|||
// former five-dword packet omitted both the real base and the count
|
||||
// field, so every call made by Unity looked like a zero-count draw to
|
||||
// the submitted-command parser and the complete scene was discarded.
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var drawCommand) ||
|
||||
!TryWriteUInt32(ctx, drawCommand, Pm4(6, ItDrawIndex2, 0)) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 4, indexCount) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 8, (uint)indexAddress) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 12, (uint)(indexAddress >> 32)) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 16, indexCount) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 20, 0))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var drawCommand, Pm4(6, ItDrawIndex2, 0), indexCount, (uint)indexAddress, (uint)(indexAddress >> 32), indexCount, 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -204,12 +180,7 @@ public static partial class AgcExports
|
|||
? 2u
|
||||
: ((modifierBits >> 3) & 0x20u) | 2u;
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var drawCommand) ||
|
||||
!TryWriteUInt32(ctx, drawCommand, Pm4(5, ItDrawIndirect, 0)) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 4, dataOffset) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 8, firstVertexRegister) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 12, firstInstanceRegister) ||
|
||||
!TryWriteUInt32(ctx, drawCommand + 16, drawInitiator))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var drawCommand, Pm4(5, ItDrawIndirect, 0), dataOffset, firstVertexRegister, firstInstanceRegister, drawInitiator))
|
||||
{
|
||||
var rejects = Interlocked.Increment(ref _indirectDrawEmitRejectCount);
|
||||
if (rejects <= 8 || rejects % 250 == 0)
|
||||
|
|
@ -244,14 +215,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, 0))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(7, ItNop, RDrawIndexAuto), indexCount, 0, 0, 0, 0, 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -390,12 +354,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItIndexBase, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)(indexBufferAddress & 0xFFFF_FFFFUL)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(indexBufferAddress >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, Pm4(2, ItIndexBufferSize, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, indexCount))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(3, ItIndexBase, 0), (uint)(indexBufferAddress & 0xFFFF_FFFFUL), (uint)(indexBufferAddress >> 32), Pm4(2, ItIndexBufferSize, 0), indexCount))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -424,12 +383,7 @@ public static partial class AgcExports
|
|||
var drawInitiator = (drawModifier & (1UL << 32)) != 0
|
||||
? 0u
|
||||
: ((uint)drawModifier >> 3) & 0x20u;
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItDrawIndexOffset2, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, Math.Max(indexCount, 1u)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, indexOffset) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, indexCount) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, drawInitiator))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(5, ItDrawIndexOffset2, 0), Math.Max(indexCount, 1u), indexOffset, indexCount, drawInitiator))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,9 +69,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(2, ItEventWrite, 0), eventType))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -95,14 +93,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RWaitFlipDone)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, videoOutHandle) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, displayBufferIndex) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 24, 0))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(7, ItNop, RWaitFlipDone), videoOutHandle, displayBufferIndex, 0, 0, 0, 0))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
@ -128,13 +119,7 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(6, ItNop, RFlip)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, videoOutHandle) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, unchecked((uint)displayBufferIndex)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, flipMode) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(flipArg & 0xFFFF_FFFFUL)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)(flipArg >> 32)))
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(6, ItNop, RFlip), videoOutHandle, unchecked((uint)displayBufferIndex), flipMode, (uint)(flipArg & 0xFFFF_FFFFUL), (uint)(flipArg >> 32)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
public static partial class AgcExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "NKIzURsgV7I",
|
||||
ExportName = "sceAgcGetGsOversubscription",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgcDriver")]
|
||||
public static int GetGsOversubscription(CpuContext ctx)
|
||||
{
|
||||
var output = ctx[CpuRegister.Rdi];
|
||||
var shader = ctx[CpuRegister.Rsi];
|
||||
var budget = (uint)ctx[CpuRegister.Rdx];
|
||||
if (output == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var pcAllocation = 0u;
|
||||
var lateAllocation = 0u;
|
||||
// These endpoints initialize both descriptors without dereferencing the shader.
|
||||
if (budget == uint.MaxValue)
|
||||
{
|
||||
pcAllocation = 0x7FF;
|
||||
lateAllocation = 0x7F_0000;
|
||||
}
|
||||
else if (budget != 0)
|
||||
{
|
||||
ctx.GetXmmRegister(0, out var factorBits, out _);
|
||||
var factor = BitConverter.UInt32BitsToSingle((uint)factorBits);
|
||||
if (!float.IsFinite(factor) ||
|
||||
!TryGetGsOccupancy(ctx, shader, out var stages, out var baseline, out var expanded))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var minimum = Math.Min(baseline.Vertex, baseline.Export);
|
||||
var waveShift = (stages & 0x0040_0000) != 0 ? 5 : 6;
|
||||
var maximum = Math.Min(Math.Min(expanded.Vertex, expanded.Export), Math.Min(budget >> waveShift, 1024));
|
||||
var headroom = maximum > minimum ? maximum - minimum : 0;
|
||||
var scaled = MathF.FusedMultiplyAdd(factor, headroom, minimum);
|
||||
if (!(scaled >= 0 && scaled < 4294967296f))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var target = (uint)scaled;
|
||||
if (target > minimum && target < baseline.Export)
|
||||
{
|
||||
var range = Math.Max(expanded.Vertex - baseline.Vertex, 1);
|
||||
var lines = Math.Max(Math.Min(((ulong)(target - baseline.Vertex) << 10) / range, 1024), 1);
|
||||
pcAllocation = ((uint)lines * 2 - 1) & 0x7FF;
|
||||
lateAllocation = 0x7F_0000;
|
||||
}
|
||||
else if (target > minimum)
|
||||
{
|
||||
var range = Math.Max(expanded.Export - baseline.Export, 1);
|
||||
var waves = Math.Min((ulong)(target - baseline.Export) * 127 / range, 127);
|
||||
pcAllocation = 0x7FF;
|
||||
lateAllocation = (uint)waves << 16;
|
||||
}
|
||||
}
|
||||
|
||||
// The result is two bank-specific register descriptors, not two scalar values.
|
||||
Span<byte> registers = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(registers, UserConfigRegisterOffset.ParameterOversubscription);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(registers[4..], pcAllocation);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(registers[8..], ShaderRegisterOffset.SpiShaderPgmRsrc4Gs);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(registers[12..], lateAllocation);
|
||||
if (!ctx.Memory.TryWrite(output, registers))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.gs_oversubscription output=0x{output:X16} shader=0x{shader:X16} budget={budget} pc_alloc=0x{pcAllocation:X8} late_alloc=0x{lateAllocation:X8}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private readonly record struct GsOccupancy(uint Vertex, uint Export);
|
||||
|
||||
private static bool TryGetGsOccupancy(CpuContext ctx, ulong shader, out uint stages, out GsOccupancy baseline, out GsOccupancy expanded)
|
||||
{
|
||||
stages = 0;
|
||||
baseline = expanded = default;
|
||||
if (shader == 0 ||
|
||||
!TryReadUInt64(ctx, shader + ShaderCxRegistersOffset, out var table) || table == 0 ||
|
||||
!TryReadByte(ctx, shader + 0x5B, out var count) ||
|
||||
!TryReadUInt64(ctx, shader + ShaderSpecialsOffset, out var specials) || specials == 0 ||
|
||||
!TryReadUInt32(ctx, specials + 12, out stages))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint onchip = 0, subgroup = 0, vertexOutput = 0, clipOutput = 0, maxOutput = 0;
|
||||
var found = 0;
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, table + (ulong)index * 8, out var offset) ||
|
||||
!TryReadUInt32(ctx, table + (ulong)index * 8 + 4, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (offset)
|
||||
{
|
||||
case ContextRegisterOffset.VgtGsOnchipCntl: onchip = value; found |= 1; break;
|
||||
case ContextRegisterOffset.GeNggSubgroupCntl: subgroup = value; found |= 2; break;
|
||||
case ContextRegisterOffset.SpiVsOutConfig: vertexOutput = value; found |= 4; break;
|
||||
case ContextRegisterOffset.PaClVsOutCntl: clipOutput = value; found |= 8; break;
|
||||
case ContextRegisterOffset.GeMaxOutputPerSubgroup: maxOutput = value; found |= 16; break;
|
||||
}
|
||||
}
|
||||
|
||||
var outputWords = ((maxOutput & 0x3FF) + 31) >> 5;
|
||||
if (found != 31 || outputWords == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var work = ((((onchip >> 11) & 0x7FF) * (subgroup & 0x1FF)) + 31) >> 5;
|
||||
var waves = Math.Max(work, outputWords);
|
||||
if ((stages & 0x0040_0000) == 0) waves >>= 1;
|
||||
waves = Math.Max(waves, 1);
|
||||
var exports = 1 + ((clipOutput >> 21) & 1) + ((clipOutput >> 22) & 1) + ((clipOutput >> 23) & 1);
|
||||
var parameters = ((vertexOutput >> 2) & 0xF) + 1;
|
||||
var vertexCapacity = (vertexOutput & 0x80) != 0 ? 2048 : 1024 / parameters;
|
||||
var expandedVertexCapacity = (vertexOutput & 0x80) != 0 ? 2048 : 2048 / parameters;
|
||||
baseline = new GsOccupancy(waves * (vertexCapacity / outputWords), waves * ((128 / exports * 4) / outputWords));
|
||||
expanded = new GsOccupancy(waves * (expandedVertexCapacity / outputWords), waves * ((382 / exports * 4) / outputWords));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -109,12 +109,9 @@ public static partial class AgcExports
|
|||
ulong commandAddress;
|
||||
using (AgcRegisterPacketProfile.Measure(AgcRegisterPacketProfile.Phase.PacketSetup))
|
||||
{
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var markerAddress) ||
|
||||
!TryWriteUInt32(ctx, markerAddress, Pm4(2, ItNop, RZero)) ||
|
||||
!TryWriteUInt32(ctx, markerAddress + 4, CbSetShRegisterRangeMarker) ||
|
||||
if (!TryEmitCommandDwords(ctx, commandBufferAddress, out _, Pm4(2, ItNop, RZero), CbSetShRegisterRangeMarker) ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItSetShReg, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, offset))
|
||||
!TryWriteCommandDwords(ctx, commandAddress, Pm4(packetDwords, ItSetShReg, 0), offset))
|
||||
{
|
||||
AgcRegisterPacketProfile.RecordResult(false);
|
||||
return ReturnPointer(ctx, 0);
|
||||
|
|
@ -288,12 +285,7 @@ public static partial class AgcExports
|
|||
_ => 0u,
|
||||
};
|
||||
if (op == 0 ||
|
||||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(5, op, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)(registersAddress & 0xFFFF_FFFCUL)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(registersAddress >> 32)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, 0x8000_0000u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 16, registerCount & 0x3FFFu))
|
||||
!TryEmitCommandDwords(ctx, commandBufferAddress, out var commandAddress, Pm4(5, op, 0), (uint)(registersAddress & 0xFFFF_FFFCUL), (uint)(registersAddress >> 32), 0x8000_0000u, registerCount & 0x3FFFu))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -491,15 +491,40 @@ public static partial class AgcExports
|
|||
private static bool TryAllocateCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, out ulong commandAddress)
|
||||
=> TryPrepareCommandDwords(ctx, commandBufferAddress, sizeDwords, true, out commandAddress);
|
||||
|
||||
private static bool TryEmitCommandDwords(CpuContext ctx, ulong commandBufferAddress, out ulong commandAddress, params ReadOnlySpan<uint> dwords)
|
||||
{
|
||||
return TryAllocateCommandDwords(ctx, commandBufferAddress, (uint)dwords.Length, out commandAddress) &&
|
||||
TryWriteCommandDwords(ctx, commandAddress, dwords);
|
||||
}
|
||||
|
||||
private static bool TryWriteCommandDwords(CpuContext ctx, ulong address, params ReadOnlySpan<uint> dwords) =>
|
||||
ctx.Memory.TryWrite(address, System.Runtime.InteropServices.MemoryMarshal.AsBytes(dwords));
|
||||
|
||||
private const int CommandBufferStateBytes = (int)(CommandBufferReservedDwOffset + sizeof(uint) - CommandBufferCursorUpOffset);
|
||||
|
||||
private static bool TryReadCommandBufferState(CpuContext ctx, ulong commandBufferAddress, out ulong cursorUp, out ulong cursorDown, out ulong callback, out ulong userData, out uint reservedDwords)
|
||||
{
|
||||
Span<byte> state = stackalloc byte[CommandBufferStateBytes];
|
||||
if (!ctx.Memory.TryRead(commandBufferAddress + CommandBufferCursorUpOffset, state))
|
||||
{
|
||||
cursorUp = cursorDown = callback = userData = 0;
|
||||
reservedDwords = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
cursorUp = BinaryPrimitives.ReadUInt64LittleEndian(state);
|
||||
cursorDown = BinaryPrimitives.ReadUInt64LittleEndian(state[(int)(CommandBufferCursorDownOffset - CommandBufferCursorUpOffset)..]);
|
||||
callback = BinaryPrimitives.ReadUInt64LittleEndian(state[(int)(CommandBufferCallbackOffset - CommandBufferCursorUpOffset)..]);
|
||||
userData = BinaryPrimitives.ReadUInt64LittleEndian(state[(int)(CommandBufferUserDataOffset - CommandBufferCursorUpOffset)..]);
|
||||
reservedDwords = BinaryPrimitives.ReadUInt32LittleEndian(state[(int)(CommandBufferReservedDwOffset - CommandBufferCursorUpOffset)..]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryPrepareCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, bool advanceCursor, out ulong commandAddress)
|
||||
{
|
||||
commandAddress = 0;
|
||||
if (sizeDwords == 0 ||
|
||||
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorUpOffset, out var cursorUp) ||
|
||||
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorDownOffset, out var cursorDown) ||
|
||||
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCallbackOffset, out var callback) ||
|
||||
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferUserDataOffset, out var userData) ||
|
||||
!TryReadUInt32(ctx, commandBufferAddress + CommandBufferReservedDwOffset, out var reservedDwords))
|
||||
!TryReadCommandBufferState(ctx, commandBufferAddress, out var cursorUp, out var cursorDown, out var callback, out var userData, out var reservedDwords))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
|
|
@ -100,23 +101,40 @@ public static class AmprExports
|
|||
public uint Flush;
|
||||
}
|
||||
|
||||
// A host file, or a file of an archive-mounted app0 that is not on disk.
|
||||
private sealed class CachedHostFile : IDisposable
|
||||
{
|
||||
private readonly SafeFileHandle? _handle;
|
||||
private readonly ZArchiveFileStream? _archived;
|
||||
|
||||
public CachedHostFile(string path)
|
||||
{
|
||||
Handle = File.OpenHandle(
|
||||
if (!File.Exists(path) && App0Archive.OpenRead(path) is { } archived)
|
||||
{
|
||||
_archived = archived;
|
||||
Length = archived.Length;
|
||||
return;
|
||||
}
|
||||
|
||||
_handle = File.OpenHandle(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
FileOptions.RandomAccess);
|
||||
Length = RandomAccess.GetLength(Handle);
|
||||
Length = RandomAccess.GetLength(_handle);
|
||||
}
|
||||
|
||||
public SafeFileHandle Handle { get; }
|
||||
public long Length { get; }
|
||||
|
||||
public void Dispose() => Handle.Dispose();
|
||||
public int Read(Span<byte> buffer, long offset) =>
|
||||
_archived?.ReadAt(buffer, offset) ?? RandomAccess.Read(_handle!, buffer, offset);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_handle?.Dispose();
|
||||
_archived?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CachedHostFileEntry
|
||||
|
|
@ -1252,8 +1270,7 @@ public static class AmprExports
|
|||
}
|
||||
|
||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||
var read = RandomAccess.Read(
|
||||
cachedFile.Handle,
|
||||
var read = cachedFile.Read(
|
||||
buffer.AsSpan(0, request),
|
||||
unchecked((long)absoluteOffset));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -269,20 +270,29 @@ internal static class AmprFileRegistry
|
|||
var relatives = new List<string>(256 * 1024);
|
||||
try
|
||||
{
|
||||
foreach (var hostPath in Directory.EnumerateFiles(
|
||||
normalizedRoot,
|
||||
"*",
|
||||
SearchOption.AllDirectories))
|
||||
// An app0 mounted from an archive holds only a few files on disk; the archive lists them all.
|
||||
if (App0Archive.Root is { } archiveRoot &&
|
||||
string.Equals(archiveRoot, Path.TrimEndingDirectorySeparator(normalizedRoot), HostFsPath.Comparison))
|
||||
{
|
||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||
.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(relative) ||
|
||||
relative.StartsWith("..", StringComparison.Ordinal))
|
||||
relatives.AddRange(App0Archive.ListAllFiles());
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var hostPath in Directory.EnumerateFiles(
|
||||
normalizedRoot,
|
||||
"*",
|
||||
SearchOption.AllDirectories))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||
.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(relative) ||
|
||||
relative.StartsWith("..", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
relatives.Add(relative);
|
||||
relatives.Add(relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.FileSystem;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.Media;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
|
|
@ -2592,6 +2593,8 @@ public static class AvPlayerExports
|
|||
out string resolved)
|
||||
{
|
||||
resolved = string.Empty;
|
||||
// A media file only in an archive-mounted app0 is extracted so the host decoder can open it.
|
||||
App0Archive.Extract(Path.Combine(root, relativePath));
|
||||
var current = root;
|
||||
var segments = relativePath.Split(
|
||||
Path.DirectorySeparatorChar,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,21 @@ using SharpEmu.Libs.VideoOut;
|
|||
|
||||
namespace SharpEmu.Libs.Codec;
|
||||
|
||||
internal sealed record Videodec2DecodedFrame(
|
||||
byte[] Nv12,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Pitch,
|
||||
bool ErrorFrame)
|
||||
{
|
||||
public ulong Pts { get; init; } = ulong.MaxValue;
|
||||
public ulong Dts { get; init; } = ulong.MaxValue;
|
||||
public ulong AttachedData { get; init; }
|
||||
public bool KeyFrame { get; init; }
|
||||
public uint Profile { get; init; }
|
||||
public uint Level { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder
|
||||
/// handle, feeding it pre-demuxed Annex-B access units from guest memory.
|
||||
|
|
@ -29,6 +44,8 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
// Enough lookahead to absorb decode jitter without adding visible latency.
|
||||
private const int FrameQueueCapacity = 4;
|
||||
|
||||
private const int WorkQueueCapacity = 64;
|
||||
|
||||
// Fallback when the stream doesn't declare a usable framerate.
|
||||
private const double FallbackFps = 30.0;
|
||||
|
||||
|
|
@ -45,12 +62,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
private bool _disposed;
|
||||
|
||||
// Unbounded: access units are small, backpressure lives on the frame queue below.
|
||||
private readonly Channel<byte[]?> _workChannel =
|
||||
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
|
||||
Channel.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
||||
|
|
@ -74,6 +91,10 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private uint _lastWidth;
|
||||
private uint _lastHeight;
|
||||
|
||||
private long _workerInputCount;
|
||||
private long _workerFrameCount;
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
|
||||
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
|
||||
{
|
||||
_codecContext = codecContext;
|
||||
|
|
@ -106,18 +127,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
|
||||
if (codec == null)
|
||||
{
|
||||
Trace("create failed: H.264 decoder not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext = ffmpeg.avcodec_alloc_context3(codec);
|
||||
if (codecContext == null)
|
||||
{
|
||||
Trace("create failed: avcodec_alloc_context3 returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE;
|
||||
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: avcodec_open2 rejected H.264 decoder");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +161,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: frame or packet allocation returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Videodec2Decoder(codecContext, frame, packet);
|
||||
var decoder = new Videodec2Decoder(codecContext, frame, packet);
|
||||
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
|
||||
return decoder;
|
||||
}
|
||||
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
|
||||
{
|
||||
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
|
||||
Trace($"create failed: {ex.GetType().Name}: {ex.Message}");
|
||||
if (codecContext != null)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
|
|
@ -174,18 +203,252 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
public bool EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
return _workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public void RequestDrain()
|
||||
public bool RequestDrain()
|
||||
{
|
||||
_workChannel.Writer.TryWrite(null);
|
||||
return _workChannel.Writer.TryWrite(null);
|
||||
}
|
||||
|
||||
public bool DecodeAccessUnit(
|
||||
byte[] accessUnit,
|
||||
ulong pts,
|
||||
ulong dts,
|
||||
ulong attachedData,
|
||||
out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed || accessUnit.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length);
|
||||
if (packetResult < 0)
|
||||
{
|
||||
Trace($"decode failed: av_new_packet={packetResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* source = accessUnit)
|
||||
{
|
||||
Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length);
|
||||
}
|
||||
|
||||
_packet->pts = ToAvTimestamp(pts);
|
||||
_packet->dts = ToAvTimestamp(dts);
|
||||
|
||||
var havePendingFrame = false;
|
||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (pendingResult < 0)
|
||||
{
|
||||
Trace($"decode failed: receive-before-resend={pendingResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
havePendingFrame = true;
|
||||
sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
}
|
||||
|
||||
if (sendResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_send_packet={sendResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!havePendingFrame)
|
||||
{
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData);
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"flush failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
decodedFrame = ConvertFrameToNv12Locked();
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ffmpeg.avcodec_flush_buffers(_codecContext);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
|
||||
private Videodec2DecodedFrame? ConvertFrameToNv12Locked(
|
||||
ulong fallbackPts = ulong.MaxValue,
|
||||
ulong fallbackDts = ulong.MaxValue,
|
||||
ulong attachedData = 0)
|
||||
{
|
||||
if (_frame->width <= 0 || _frame->height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var width = checked((uint)_frame->width);
|
||||
var height = checked((uint)_frame->height);
|
||||
uint pitch;
|
||||
ulong lumaBytes;
|
||||
ulong requiredBytes;
|
||||
try
|
||||
{
|
||||
pitch = checked((width + 255u) & ~255u);
|
||||
lumaBytes = checked((ulong)pitch * height);
|
||||
var chromaRows = ((ulong)height + 1UL) / 2UL;
|
||||
requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows));
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requiredBytes > int.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_swsContext = ffmpeg.sws_getCachedContext(
|
||||
_swsContext,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
(AVPixelFormat)_frame->format,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
AVPixelFormat.AV_PIX_FMT_NV12,
|
||||
ffmpeg.SWS_FAST_BILINEAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
if (_swsContext == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nv12 = new byte[(int)requiredBytes];
|
||||
fixed (byte* destination = nv12)
|
||||
{
|
||||
var destinationPlanes = new byte*[4]
|
||||
{
|
||||
destination,
|
||||
destination + checked((int)lumaBytes),
|
||||
null,
|
||||
null,
|
||||
};
|
||||
var destinationStrides = new int[4]
|
||||
{
|
||||
checked((int)pitch),
|
||||
checked((int)pitch),
|
||||
0,
|
||||
0,
|
||||
};
|
||||
var rows = ffmpeg.sws_scale(
|
||||
_swsContext,
|
||||
_frame->data,
|
||||
_frame->linesize,
|
||||
0,
|
||||
_frame->height,
|
||||
destinationPlanes,
|
||||
destinationStrides);
|
||||
if (rows != _frame->height)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Videodec2DecodedFrame(
|
||||
nv12,
|
||||
width,
|
||||
height,
|
||||
pitch,
|
||||
(_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0)
|
||||
{
|
||||
Pts = FromAvTimestamp(_frame->pts, fallbackPts),
|
||||
Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts),
|
||||
AttachedData = attachedData,
|
||||
KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0,
|
||||
Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0,
|
||||
Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static long ToAvTimestamp(ulong timestamp) =>
|
||||
timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE;
|
||||
|
||||
private static ulong FromAvTimestamp(long timestamp, ulong fallback) =>
|
||||
timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp;
|
||||
|
||||
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
|
||||
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
|
||||
{
|
||||
|
|
@ -233,15 +496,28 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var inputOrdinal = Interlocked.Increment(ref _workerInputCount);
|
||||
var decodedOk = item is null
|
||||
? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height)
|
||||
: DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height);
|
||||
if (ShouldTrace(inputOrdinal))
|
||||
{
|
||||
Trace(
|
||||
$"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " +
|
||||
$"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}");
|
||||
}
|
||||
|
||||
if (!decodedOk || !hasPicture || bgraFrame is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var frameOrdinal = Interlocked.Increment(ref _workerFrameCount);
|
||||
if (ShouldTrace(frameOrdinal))
|
||||
{
|
||||
Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
|
||||
|
|
@ -564,4 +840,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
|
|
@ -15,10 +16,18 @@ public static class Videodec2Exports
|
|||
{
|
||||
private const int Ok = 0;
|
||||
|
||||
private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024;
|
||||
private const uint FrameBufferAlignment = 0x100;
|
||||
|
||||
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> PictureInfos = new();
|
||||
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
|
||||
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
private static long _decodeCallCount;
|
||||
private static long _decodedPictureCount;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RnDibcGCPKw",
|
||||
ExportName = "sceVideodec2QueryComputeMemoryInfo",
|
||||
|
|
@ -27,24 +36,25 @@ public static class Videodec2Exports
|
|||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
if (paramAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
// Success needs no memory writes; the game initializes from its own fields.
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||
private const int VideodecErrorStructSize = unchecked((int)0x811D0101);
|
||||
private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F);
|
||||
private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202);
|
||||
|
||||
// Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`.
|
||||
private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024;
|
||||
private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024;
|
||||
|
||||
// Opaque token the game hands back unmodified to later Videodec2 calls.
|
||||
private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eD+X2SmxUt4",
|
||||
ExportName = "sceVideodec2AllocateComputeQueue",
|
||||
|
|
@ -52,8 +62,14 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
||||
{
|
||||
var queueAddress = ctx[CpuRegister.Rdi];
|
||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var queueAddress = ctx[CpuRegister.Rdx];
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 ||
|
||||
!ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) ||
|
||||
computeMemoryAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(
|
||||
queueAddress, computeMemoryAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -61,7 +77,18 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
||||
[SysAbiExport(
|
||||
Nid = "UvtA3FAiF4Y",
|
||||
ExportName = "sceVideodec2ReleaseComputeQueue",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2ReleaseComputeQueue(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "qqMCwlULR+E",
|
||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
||||
|
|
@ -69,12 +96,18 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) ||
|
||||
// Frame-slot size: must be nonzero or the game divides its arena by zero.
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000))
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -99,7 +132,9 @@ public static class Videodec2Exports
|
|||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
||||
var decoder = Videodec2Decoder.TryCreate();
|
||||
Decoders[handle] = decoder;
|
||||
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
|
||||
|
||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
||||
{
|
||||
|
|
@ -111,7 +146,6 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
|
||||
[SysAbiExport(
|
||||
Nid = "l1hXwscLuCY",
|
||||
ExportName = "sceVideodec2Flush",
|
||||
|
|
@ -120,33 +154,25 @@ public static class Videodec2Exports
|
|||
public static int Videodec2Flush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rsi];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
if (Decoders.TryGetValue(handle, out var decoder) &&
|
||||
decoder is not null &&
|
||||
decoder.FlushOutput(out var decodedFrame) &&
|
||||
decodedFrame is not null)
|
||||
{
|
||||
// Drain in order: report an already-finished frame before queuing a new drain request.
|
||||
if (decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) &&
|
||||
ctx.TryWriteUInt64(outputInfoAddress + 0x10, height))
|
||||
{
|
||||
_ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
decoder.RequestDrain();
|
||||
}
|
||||
_ = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// No state to reset.
|
||||
[SysAbiExport(
|
||||
Nid = "wJXikG6QFN8",
|
||||
ExportName = "sceVideodec2Reset",
|
||||
|
|
@ -154,6 +180,12 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2Reset(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
decoder.Reset();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +205,6 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// rcx[0] is the picture-ready flag (1 = frame published); it lives in
|
||||
// uninitialized stack and must always be written explicitly.
|
||||
[SysAbiExport(
|
||||
Nid = "852F5+q6+iM",
|
||||
ExportName = "sceVideodec2Decode",
|
||||
|
|
@ -186,8 +216,9 @@ public static class Videodec2Exports
|
|||
var inputAuStruct = ctx[CpuRegister.Rsi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rdx];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rcx];
|
||||
var callOrdinal = Interlocked.Increment(ref _decodeCallCount);
|
||||
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -195,6 +226,7 @@ public static class Videodec2Exports
|
|||
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
|
||||
{
|
||||
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
|
||||
TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -208,35 +240,235 @@ public static class Videodec2Exports
|
|||
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
|
||||
{
|
||||
// Nothing sane to feed/fill this call; not an error.
|
||||
TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData);
|
||||
|
||||
var auBuffer = new byte[auDataSize];
|
||||
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
|
||||
{
|
||||
TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
||||
decoder.EnqueueAccessUnit(auBuffer);
|
||||
var decodeOk = decoder.DecodeAccessUnit(
|
||||
auBuffer, pts, dts, attachedData, out var decodedFrame);
|
||||
if (ShouldTrace(callOrdinal))
|
||||
{
|
||||
var prefixBytes = Math.Min(auBuffer.Length, 8);
|
||||
Trace(
|
||||
$"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " +
|
||||
$"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " +
|
||||
$"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " +
|
||||
$"picture={decodedFrame is not null}");
|
||||
}
|
||||
|
||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
if (!decodeOk || decodedFrame is null)
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
||||
var copied = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
if (copied)
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
Interlocked.Increment(ref _decodedPictureCount);
|
||||
}
|
||||
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NoPicture = [0];
|
||||
private static readonly byte[] PictureReady = [1];
|
||||
[SysAbiExport(
|
||||
Nid = "NtXRa3dRzU0",
|
||||
ExportName = "sceVideodec2GetPictureInfo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2GetPictureInfo(CpuContext ctx)
|
||||
{
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdi];
|
||||
var firstPictureInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var secondPictureInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || firstPictureInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) ||
|
||||
(outputInfoSize != 0x30 && outputInfoSize != 0x38))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) ||
|
||||
!TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) ||
|
||||
!ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) ||
|
||||
isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 ||
|
||||
!PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorOutputInfo);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) ||
|
||||
(requestedSize != 104 && requestedSize != 120))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
var picture = new byte[(int)requestedSize];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize);
|
||||
picture[0x08] = 1;
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData);
|
||||
picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0;
|
||||
picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue);
|
||||
picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u);
|
||||
picture[0x34] = 1;
|
||||
picture[0x4E] = 1;
|
||||
picture[0x4F] = 5;
|
||||
if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (secondPictureInfoAddress != 0 &&
|
||||
!TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"picture_info buffer=0x{pixelsAddress:X16} " +
|
||||
$"pts=0x{decodedFrame.Pts:X16} size={requestedSize}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NotAccepted = [0];
|
||||
private static readonly byte[] Accepted = [1];
|
||||
private static readonly byte[] NoPicture = [0, 0, 0, 0];
|
||||
private static readonly byte[] PictureReady = [1, 0, 1, 0];
|
||||
private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0];
|
||||
|
||||
private static bool TryInitializeNoPicture(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress)
|
||||
{
|
||||
return frameBufferAddress != 0 &&
|
||||
outputInfoAddress != 0 &&
|
||||
ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) &&
|
||||
ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture);
|
||||
}
|
||||
|
||||
internal static bool TryWriteDecodedFrame(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress,
|
||||
Videodec2DecodedFrame decodedFrame)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) ||
|
||||
!ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) ||
|
||||
pixelsAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ulong)decodedFrame.Nv12.Length > slotBytes ||
|
||||
!ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputInfoBytes >= 0x38 &&
|
||||
(!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) ||
|
||||
!ctx.Memory.TryWrite(
|
||||
outputInfoAddress + 0x08,
|
||||
decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PictureInfos[pixelsAddress] = decodedFrame;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var output = new byte[(int)size];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(output, size);
|
||||
return ctx.Memory.TryWrite(address, output);
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void TraceDecode(long ordinal, string message)
|
||||
{
|
||||
if (ShouldTrace(ordinal))
|
||||
{
|
||||
Trace($"decode call={ordinal} {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,6 +28,42 @@ internal static class DccWriterTrace
|
|||
|
||||
private static readonly HashSet<(ulong Address, ulong Size, ulong Program)> _reported = new();
|
||||
private static readonly HashSet<(ulong Hash, string Reason)> _refusals = new();
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, (ulong Size, ulong Program, string Kind)> _writes = new();
|
||||
private static readonly HashSet<ulong> _syncReported = new();
|
||||
|
||||
[ThreadStatic]
|
||||
public static string? CurrentKind;
|
||||
|
||||
public static void RecordWrite(ulong address, ulong size)
|
||||
{
|
||||
if (DccEnabled)
|
||||
{
|
||||
_writes[address] = (size, CurrentProgram, CurrentKind ?? "?");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReportSync(ulong metadataAddress, ulong size)
|
||||
{
|
||||
if (!DccEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_syncReported)
|
||||
{
|
||||
if (_syncReported.Count >= 64 || !_syncReported.Add(metadataAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var writers = _writes
|
||||
.Where(pair => pair.Key < metadataAddress + size && metadataAddress < pair.Key + pair.Value.Size)
|
||||
.Take(8)
|
||||
.Select(pair => $"0x{pair.Key:X}+0x{pair.Value.Size:X}:{pair.Value.Kind}:0x{pair.Value.Program:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU][DCC_SYNC_WRITER] metadata=0x{metadataAddress:X16} size=0x{size:X} writers={string.Join(" ", writers)}");
|
||||
}
|
||||
|
||||
private static ulong ParseAddress(string? value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ public sealed unsafe class BdaFaultProcessor : IDisposable
|
|||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, _pipeline);
|
||||
var set = _sets[_currentArea];
|
||||
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, _pipelineLayout, 0, 1, &set, 0, null);
|
||||
NativeBindingEpoch.Advance();
|
||||
var threads = _pageCount / 32;
|
||||
vk.CmdDispatch(command, (uint)((threads + 63) / 64), 1, 1);
|
||||
VulkanSynchronization.PipelineBarrier(vk,
|
||||
|
|
|
|||
|
|
@ -172,16 +172,39 @@ public unsafe class GpuBuffer : IDisposable
|
|||
// A readback of it only has to wait for that tick, not for all queued work.
|
||||
public ulong LastGpuWriteTick { get; private set; }
|
||||
|
||||
private readonly WriteTickMap _writeTicks = new();
|
||||
private int _writeTickPruneCount = 64;
|
||||
|
||||
// Called by every path that records a GPU write into this buffer.
|
||||
public void NoteGpuWrite()
|
||||
public void NoteGpuWrite() => NoteGpuWrite(0, Size);
|
||||
|
||||
public void NoteGpuWrite(ulong offset, ulong size)
|
||||
{
|
||||
var tick = _scheduler.CurrentTick;
|
||||
if (tick > LastGpuWriteTick)
|
||||
{
|
||||
LastGpuWriteTick = tick;
|
||||
}
|
||||
|
||||
if (size == 0 || offset >= Size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_writeTicks.Assign(offset, Math.Min(size, Size - offset), tick);
|
||||
if (_writeTicks.Count >= _writeTickPruneCount)
|
||||
{
|
||||
if (Volatile.Read(ref _foreignReads) == 0)
|
||||
{
|
||||
_writeTicks.Prune(_scheduler.Timeline.CompletedTick + 1);
|
||||
}
|
||||
|
||||
_writeTickPruneCount = Math.Max(64, _writeTicks.Count * 2);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong LastGpuWriteTickIn(ulong offset, ulong size) => _writeTicks.MaxTick(offset, size);
|
||||
|
||||
public void Write(ulong offset, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (_mapped == null || offset > Size || (ulong)source.Length > Size - offset)
|
||||
|
|
@ -243,7 +266,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
}
|
||||
|
||||
command.EndRendering();
|
||||
NoteGpuWrite();
|
||||
NoteGpuWrite(destinationOffset, size);
|
||||
var vk = _device.Vk;
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var before = stackalloc BufferMemoryBarrier2[2];
|
||||
|
|
@ -271,7 +294,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
var command = _scheduler.Current;
|
||||
command.EndRendering();
|
||||
NoteGpuWrite();
|
||||
NoteGpuWrite(offset, size);
|
||||
var vk = _device.Vk;
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var before = CreateBarrier(offset, size, MemoryAccess, AccessFlags.TransferWriteBit);
|
||||
|
|
|
|||
|
|
@ -240,10 +240,11 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_ = SynchronizeBuffer(buffer, guestAddress, size, isWritten, isTexelBuffer, preserveCpuWriteHotPages: false);
|
||||
if (isWritten)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
buffer.NoteGpuWrite(buffer.Offset(guestAddress), size);
|
||||
if (Diagnostics.DccWriterTrace.Enabled)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.Record(RequireImageCache(), guestAddress, size);
|
||||
Diagnostics.DccWriterTrace.RecordWrite(guestAddress, size);
|
||||
}
|
||||
|
||||
if (!_gpuModifiedRanges.Contains(guestAddress, size))
|
||||
|
|
@ -333,6 +334,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
throw SubmissionScheduler.Fatal("The host DMA write range is invalid.");
|
||||
}
|
||||
|
||||
GpuWritebackEpoch.Advance();
|
||||
if (!_backing.TryWriteBacking(guestAddress, data))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{guestAddress:X16} size=0x{data.Length:X16}");
|
||||
|
|
@ -359,7 +361,10 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public void FillBuffer(ulong guestAddress, ulong size, uint value, bool isGds)
|
||||
public void FillBuffer(ulong guestAddress, ulong size, uint value, bool isGds) =>
|
||||
FillBuffer(guestAddress, size, value, isGds, publishFill: false);
|
||||
|
||||
private void FillBuffer(ulong guestAddress, ulong size, uint value, bool isGds, bool publishFill)
|
||||
{
|
||||
if ((guestAddress & 3) != 0 || size == 0 || (size & 3) != 0 || size > ulong.MaxValue - guestAddress)
|
||||
{
|
||||
|
|
@ -408,56 +413,116 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
images.InvalidateMemoryFromGpu(guestAddress, size);
|
||||
var bufferIdentifier = FindBuffer(guestAddress, size);
|
||||
if (Diagnostics.DccWriterTrace.Enabled)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.CurrentKind = "fill";
|
||||
}
|
||||
|
||||
var (destination, destinationOffset) = ObtainBuffer(guestAddress, size, true, true, bufferIdentifier);
|
||||
destination.Fill(destinationOffset, size, value);
|
||||
if (images.OverlapsDccMetadata(guestAddress, size) && TryWriteFillToBacking(guestAddress, size, value))
|
||||
if ((publishFill || images.OverlapsDccMetadata(guestAddress, size)) && TryWriteFillToBacking(guestAddress, size, value))
|
||||
{
|
||||
_gpuModifiedRanges.Remove(guestAddress, size);
|
||||
var firstPage = guestAddress & ~(TrackerLayout.PageBytes - 1);
|
||||
for (var page = firstPage; page < guestAddress + size; page += TrackerLayout.PageBytes)
|
||||
{
|
||||
if (!_gpuModifiedRanges.Overlaps(page, TrackerLayout.PageBytes))
|
||||
{
|
||||
_tracker.ClearGpuDirtyPages(page, TrackerLayout.PageBytes);
|
||||
}
|
||||
}
|
||||
Interlocked.Increment(ref _gpuModifiedVersion);
|
||||
ClearGpuDirtyPagesOfRemovedRange(guestAddress, size);
|
||||
}
|
||||
}
|
||||
|
||||
// After a range left the GPU-modified set: the pages inside it are clean in one tracker walk,
|
||||
// and only a page the range shares at either end can still hold another GPU-modified range.
|
||||
private void ClearGpuDirtyPagesOfRemovedRange(ulong guestAddress, ulong size)
|
||||
{
|
||||
const ulong pageBytes = TrackerLayout.PageBytes;
|
||||
var end = guestAddress + size;
|
||||
var firstPage = guestAddress & ~(pageBytes - 1);
|
||||
var lastPage = (end - 1) & ~(pageBytes - 1);
|
||||
var startPartial = guestAddress != firstPage;
|
||||
var endPartial = end != lastPage + pageBytes;
|
||||
var innerBegin = startPartial ? firstPage + pageBytes : firstPage;
|
||||
var innerEnd = endPartial ? lastPage : end;
|
||||
if (innerBegin < innerEnd)
|
||||
{
|
||||
_tracker.ClearGpuDirtyPages(innerBegin, innerEnd - innerBegin);
|
||||
}
|
||||
|
||||
if (startPartial)
|
||||
{
|
||||
ClearGpuDirtyPageIfUnmodified(firstPage);
|
||||
}
|
||||
|
||||
if (endPartial && (lastPage != firstPage || !startPartial))
|
||||
{
|
||||
ClearGpuDirtyPageIfUnmodified(lastPage);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearGpuDirtyPageIfUnmodified(ulong page)
|
||||
{
|
||||
if (!_gpuModifiedRanges.Overlaps(page, TrackerLayout.PageBytes))
|
||||
{
|
||||
_tracker.ClearGpuDirtyPages(page, TrackerLayout.PageBytes);
|
||||
}
|
||||
}
|
||||
|
||||
public void FillDccMetadata(ulong guestAddress, ulong size, uint value)
|
||||
{
|
||||
if (guestAddress == 0 || (guestAddress & 3) != 0 || size == 0 || (size & 3) != 0 || size > ulong.MaxValue - guestAddress ||
|
||||
HasGpuDirtyBytes(guestAddress, size) || RequireImageCache().QueryRegion(guestAddress, size).ImageBytes)
|
||||
HasGpuDirtyBytes(guestAddress, size))
|
||||
{
|
||||
FillBuffer(guestAddress, size, value, false);
|
||||
FillBuffer(guestAddress, size, value, false, publishFill: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Images over the range read it again; only GPU-owned image bytes need the general fill.
|
||||
// The buffer copy is filled on the GPU rather than uploaded from the written bytes.
|
||||
var images = RequireImageCache();
|
||||
var region = images.QueryRegion(guestAddress, size);
|
||||
if (region.GpuImageBytes)
|
||||
{
|
||||
FillBuffer(guestAddress, size, value, false, publishFill: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (region.ImageBytes)
|
||||
{
|
||||
images.InvalidateMemory(guestAddress, size);
|
||||
}
|
||||
|
||||
var (destination, destinationOffset) = ObtainBuffer(guestAddress, size, false, false, FindBuffer(guestAddress, size));
|
||||
destination.Fill(destinationOffset, size, value);
|
||||
if (!TryWriteFillToBacking(guestAddress, size, value))
|
||||
{
|
||||
FillBuffer(guestAddress, size, value, false);
|
||||
FillBuffer(guestAddress, size, value, false, publishFill: true);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryWriteFillToBacking(ulong guestAddress, ulong size, uint value)
|
||||
{
|
||||
var values = new uint[(int)Math.Min(size / sizeof(uint), 4096)];
|
||||
Array.Fill(values, value);
|
||||
var bytes = MemoryMarshal.AsBytes<uint>(values);
|
||||
for (ulong offset = 0; offset < size;)
|
||||
// Metadata fills repeat every frame: the pattern buffer is pooled.
|
||||
var count = (int)Math.Min(size / sizeof(uint), 4096);
|
||||
var values = System.Buffers.ArrayPool<uint>.Shared.Rent(count);
|
||||
try
|
||||
{
|
||||
var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length);
|
||||
if (!_backing.TryWriteBacking(guestAddress + offset, bytes[..chunk]))
|
||||
values.AsSpan(0, count).Fill(value);
|
||||
var bytes = MemoryMarshal.AsBytes(values.AsSpan(0, count));
|
||||
for (ulong offset = 0; offset < size;)
|
||||
{
|
||||
return false;
|
||||
var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length);
|
||||
GpuWritebackEpoch.Advance();
|
||||
if (!_backing.TryWriteBacking(guestAddress + offset, bytes[..chunk]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += (ulong)chunk;
|
||||
}
|
||||
|
||||
offset += (ulong)chunk;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<uint>.Shared.Return(values);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public const ulong MaxHostCopyWords = 64 * 1024;
|
||||
|
|
@ -583,6 +648,11 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var srcId = srcMemory ? FindBuffer(srcVaddr, size) : default;
|
||||
var dstId = dstMemory ? FindBuffer(dstVaddr, size) : default;
|
||||
var (src, srcOffset) = srcMemory ? ObtainBuffer(srcVaddr, size, false, true, srcId) : (_gds, srcVaddr);
|
||||
if (Diagnostics.DccWriterTrace.Enabled)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.CurrentKind = $"copy_from_0x{srcVaddr:X}";
|
||||
}
|
||||
|
||||
var (dst, dstOffset) = dstMemory ? ObtainBuffer(dstVaddr, size, true, true, dstId) : (_gds, dstVaddr);
|
||||
if (ReferenceEquals(src, dst) && srcOffset < dstOffset + size && dstOffset < srcOffset + size)
|
||||
{
|
||||
|
|
@ -898,22 +968,34 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
for (var attempt = 0; ; attempt++)
|
||||
{
|
||||
var allowPending = attempt < MaxAsyncReadbackAttempts;
|
||||
var markHot = attempt == 0 && !isWrite;
|
||||
PendingDownload? pending = null;
|
||||
if (!_relay.TryRunOnGpuQueue(() =>
|
||||
pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending, markHot)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (pending is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
readback.Wait(pending.Ticket);
|
||||
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: true)))
|
||||
var complete = false;
|
||||
if (!_relay.TryRunOnGpuQueue(() => complete = CompleteReadMemoryOnGpu(pending, consumed: true)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (complete)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
var onQueue = _relay.IsGpuQueueThread;
|
||||
|
|
@ -934,6 +1016,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private const ulong ReadbackWindowBytes = 512 * 1024;
|
||||
private const long HotWindowLifetime = 512;
|
||||
private const int MaxEagerReadbacks = 4;
|
||||
private const int MaxAsyncReadbackAttempts = 4;
|
||||
private const int EagerStrikeLimit = 3;
|
||||
private const long EagerMuteBatches = 4096;
|
||||
private readonly Dictionary<ulong, HotWindow> _hotWindows = new();
|
||||
private readonly HashSet<ulong> _eagerCandidates = new();
|
||||
private readonly List<PendingDownload> _eagerDownloads = new();
|
||||
|
|
@ -941,8 +1026,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private static long _reportedEagerStarted;
|
||||
private static long _reportedEagerUsed;
|
||||
private static long _reportedEagerStale;
|
||||
private static long _reportedEagerMuted;
|
||||
|
||||
private readonly record struct HotWindow(ulong Address, ulong Size, long LastHit);
|
||||
private readonly record struct HotWindow(ulong Address, ulong Size, long LastHit, int Strikes = 0, long MutedUntil = 0);
|
||||
|
||||
internal long EagerReadbacksStarted { get; private set; }
|
||||
|
||||
|
|
@ -950,7 +1036,44 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
private void NoteHotReadback(ulong guestAddress, ulong size)
|
||||
{
|
||||
_hotWindows[guestAddress & ~(ReadbackWindowBytes - 1)] = new HotWindow(guestAddress, size, _batchSerial);
|
||||
var key = guestAddress & ~(ReadbackWindowBytes - 1);
|
||||
_hotWindows[key] = _hotWindows.TryGetValue(key, out var hot)
|
||||
? hot with { Address = guestAddress, Size = size, LastHit = _batchSerial }
|
||||
: new HotWindow(guestAddress, size, _batchSerial);
|
||||
}
|
||||
|
||||
internal long EagerReadbacksWasted { get; private set; }
|
||||
|
||||
private void NoteEagerOutcome(ulong key, bool wasted)
|
||||
{
|
||||
if (wasted)
|
||||
{
|
||||
EagerReadbacksWasted++;
|
||||
}
|
||||
|
||||
if (!_hotWindows.TryGetValue(key, out var hot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wasted)
|
||||
{
|
||||
if (hot.Strikes != 0)
|
||||
{
|
||||
_hotWindows[key] = hot with { Strikes = 0 };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var strikes = hot.Strikes + 1;
|
||||
_hotWindows[key] = strikes >= EagerStrikeLimit
|
||||
? hot with { Strikes = 0, MutedUntil = _batchSerial + EagerMuteBatches }
|
||||
: hot with { Strikes = strikes };
|
||||
if (strikes >= EagerStrikeLimit)
|
||||
{
|
||||
Interlocked.Increment(ref _reportedEagerMuted);
|
||||
}
|
||||
}
|
||||
|
||||
private void NoteHotWindowWrite(ulong guestAddress, ulong size)
|
||||
|
|
@ -963,10 +1086,11 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var last = (guestAddress + size - 1) & ~(ReadbackWindowBytes - 1);
|
||||
for (var key = guestAddress & ~(ReadbackWindowBytes - 1); key <= last; key += ReadbackWindowBytes)
|
||||
{
|
||||
if (_hotWindows.TryGetValue(key, out var hot) && _batchSerial - hot.LastHit <= HotWindowLifetime)
|
||||
if (_hotWindows.TryGetValue(key, out var hot) && _batchSerial - hot.LastHit <= HotWindowLifetime &&
|
||||
_batchSerial >= hot.MutedUntil)
|
||||
{
|
||||
_eagerCandidates.Add(key);
|
||||
HotWritePending = true;
|
||||
HotWritePending |= hot.Strikes == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -988,16 +1112,14 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
if (readback.IsComplete(pending.Ticket))
|
||||
{
|
||||
_eagerDownloads.RemoveAt(index);
|
||||
if (!CompleteReadMemoryOnGpu(pending, retrySynchronously: false))
|
||||
{
|
||||
Interlocked.Increment(ref _reportedEagerStale);
|
||||
}
|
||||
_ = CompleteReadMemoryOnGpu(pending, consumed: false);
|
||||
}
|
||||
}
|
||||
|
||||
if ((_batchSerial & 1023) == 0)
|
||||
{
|
||||
foreach (var key in _hotWindows.Where(pair => _batchSerial - pair.Value.LastHit > HotWindowLifetime).Select(pair => pair.Key).ToList())
|
||||
foreach (var key in _hotWindows.Where(pair => _batchSerial - pair.Value.LastHit > HotWindowLifetime && _batchSerial >= pair.Value.MutedUntil)
|
||||
.Select(pair => pair.Key).ToList())
|
||||
{
|
||||
_hotWindows.Remove(key);
|
||||
}
|
||||
|
|
@ -1008,6 +1130,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
List<ulong>? settled = null;
|
||||
foreach (var key in _eagerCandidates)
|
||||
{
|
||||
if (_eagerDownloads.Count >= MaxEagerReadbacks)
|
||||
|
|
@ -1015,26 +1138,52 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
break;
|
||||
}
|
||||
|
||||
if (_hotWindows.TryGetValue(key, out var hot))
|
||||
if (!_hotWindows.TryGetValue(key, out var hot) || _batchSerial - hot.LastHit > HotWindowLifetime ||
|
||||
_batchSerial < hot.MutedUntil || StartEagerReadback(hot) != EagerStart.Deferred)
|
||||
{
|
||||
StartEagerReadback(hot);
|
||||
(settled ??= []).Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
_eagerCandidates.Clear();
|
||||
}
|
||||
|
||||
private void StartEagerReadback(HotWindow hot)
|
||||
{
|
||||
if (FindOwner(hot.Address, hot.Size) is not { } buffer)
|
||||
if (settled is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var copies = CollectReadbackWindow(buffer, hot.Address, hot.Size, out var windowBegin, out var windowEnd);
|
||||
if (copies.Count == 0 || !TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
|
||||
foreach (var key in settled)
|
||||
{
|
||||
return;
|
||||
_eagerCandidates.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private enum EagerStart
|
||||
{
|
||||
Started,
|
||||
Deferred,
|
||||
Nothing,
|
||||
}
|
||||
|
||||
private EagerStart StartEagerReadback(HotWindow hot)
|
||||
{
|
||||
if (FindOwner(hot.Address, hot.Size) is not { } buffer)
|
||||
{
|
||||
return EagerStart.Nothing;
|
||||
}
|
||||
|
||||
if (OverlapsEagerReadback(hot.Address, hot.Size))
|
||||
{
|
||||
return EagerStart.Deferred;
|
||||
}
|
||||
|
||||
var copies = CollectReadbackWindow(buffer, hot.Address, hot.Size, out var windowBegin, out var windowEnd);
|
||||
if (copies.Count == 0)
|
||||
{
|
||||
return EagerStart.Nothing;
|
||||
}
|
||||
|
||||
if (!TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
|
||||
{
|
||||
return EagerStart.Deferred;
|
||||
}
|
||||
|
||||
foreach (var copy in copies)
|
||||
|
|
@ -1044,6 +1193,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
_eagerDownloads.Add(new PendingDownload
|
||||
{
|
||||
HotKey = hot.Address & ~(ReadbackWindowBytes - 1),
|
||||
Ticket = ticket,
|
||||
Copies = copies,
|
||||
WriteTicks = writeTicks,
|
||||
|
|
@ -1052,11 +1202,26 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
GuestAddress = hot.Address,
|
||||
Size = hot.Size,
|
||||
IsWrite = false,
|
||||
Eager = true,
|
||||
Source = GuestMemoryProfile.ReadbackSource.ShaderResourceRead,
|
||||
Started = System.Diagnostics.Stopwatch.GetTimestamp(),
|
||||
});
|
||||
EagerReadbacksStarted++;
|
||||
Interlocked.Increment(ref _reportedEagerStarted);
|
||||
return EagerStart.Started;
|
||||
}
|
||||
|
||||
private bool OverlapsEagerReadback(ulong guestAddress, ulong size)
|
||||
{
|
||||
foreach (var pending in _eagerDownloads)
|
||||
{
|
||||
if (guestAddress < pending.WindowEnd && pending.WindowBegin < guestAddress + size)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private PendingDownload? TakeEagerReadback(ulong guestAddress, ulong size)
|
||||
|
|
@ -1068,6 +1233,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
guestAddress - pending.WindowBegin <= pending.WindowEnd - pending.WindowBegin - size)
|
||||
{
|
||||
_eagerDownloads.RemoveAt(index);
|
||||
NoteHotReadback(guestAddress, size);
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
|
|
@ -1083,15 +1249,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
readback.Wait(pending.Ticket);
|
||||
if (!CompleteReadMemoryOnGpu(pending, retrySynchronously: false))
|
||||
{
|
||||
Interlocked.Increment(ref _reportedEagerStale);
|
||||
return false;
|
||||
}
|
||||
|
||||
EagerReadbacksUsed++;
|
||||
Interlocked.Increment(ref _reportedEagerUsed);
|
||||
return !HasGpuDirtyBytes(guestAddress, size);
|
||||
return CompleteReadMemoryOnGpu(pending with { GuestAddress = guestAddress, Size = size }, consumed: true);
|
||||
}
|
||||
|
||||
private void AbandonEagerReadbacks()
|
||||
|
|
@ -1131,9 +1289,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source) =>
|
||||
_ = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: false);
|
||||
|
||||
internal long PendingReadbacksApplied { get; private set; }
|
||||
|
||||
internal long PendingReadbacksRetried { get; private set; }
|
||||
|
|
@ -1141,8 +1296,13 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private static long _reportedApplied;
|
||||
private static long _reportedRetried;
|
||||
|
||||
private static long _reportedStalePieces;
|
||||
private static long _reportedPublishedPieces;
|
||||
|
||||
internal long StalePiecesKept { get; private set; }
|
||||
|
||||
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
|
||||
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)}");
|
||||
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} published_pieces={Interlocked.Exchange(ref _reportedPublishedPieces, 0)} stale_pieces={Interlocked.Exchange(ref _reportedStalePieces, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)} eager_muted={Interlocked.Exchange(ref _reportedEagerMuted, 0)}");
|
||||
|
||||
private sealed record PendingDownload
|
||||
{
|
||||
|
|
@ -1154,6 +1314,8 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public required ulong GuestAddress { get; init; }
|
||||
public required ulong Size { get; init; }
|
||||
public required bool IsWrite { get; init; }
|
||||
public bool Eager { get; init; }
|
||||
public ulong HotKey { get; init; }
|
||||
public required GuestMemoryProfile.ReadbackSource Source { get; init; }
|
||||
public required long Started { get; init; }
|
||||
}
|
||||
|
|
@ -1169,7 +1331,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
if (allowPending && TakeEagerReadback(guestAddress, size) is { } eager)
|
||||
if (!isWrite && allowPending && TakeEagerReadback(guestAddress, size) is { } eager)
|
||||
{
|
||||
return eager with { GuestAddress = guestAddress, Size = size, IsWrite = isWrite, Source = source };
|
||||
}
|
||||
|
|
@ -1216,20 +1378,87 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool retrySynchronously)
|
||||
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool consumed)
|
||||
{
|
||||
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
|
||||
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
var copies = pending.Copies;
|
||||
var current = new bool[copies.Count];
|
||||
var published = 0;
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
readback.Complete(pending.Ticket, null);
|
||||
if (retrySynchronously)
|
||||
current[index] = IsPieceCurrent(pending, index);
|
||||
if (current[index])
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
published++;
|
||||
}
|
||||
}
|
||||
|
||||
readback.Complete(pending.Ticket, published == 0 ? null : (index, bytes) =>
|
||||
{
|
||||
if (!current[index])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GpuWritebackEpoch.Advance();
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{(ulong)bytes.Length:X16}");
|
||||
}
|
||||
});
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
if (current[index])
|
||||
{
|
||||
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
|
||||
Interlocked.Increment(ref _gpuModifiedVersion);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
if (current[index])
|
||||
{
|
||||
ClearGpuDirtyPagesOfRemovedRange(copies[index].Address, copies[index].Size);
|
||||
}
|
||||
}
|
||||
|
||||
var stale = copies.Count - published;
|
||||
if (pending.Eager)
|
||||
{
|
||||
NoteEagerOutcome(pending.HotKey, stale != 0);
|
||||
}
|
||||
|
||||
if (stale != 0)
|
||||
{
|
||||
StalePiecesKept += stale;
|
||||
Interlocked.Add(ref _reportedStalePieces, stale);
|
||||
}
|
||||
|
||||
Interlocked.Add(ref _reportedPublishedPieces, published);
|
||||
var clean = pending.IsWrite
|
||||
? !_tracker.HasGpuDirtyPages(pending.GuestAddress, pending.Size)
|
||||
: !HasGpuDirtyBytes(pending.GuestAddress, pending.Size);
|
||||
if (!consumed)
|
||||
{
|
||||
if (pending.Eager && stale != 0)
|
||||
{
|
||||
Interlocked.Increment(ref _reportedEagerStale);
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
if (!clean)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
if (pending.Eager)
|
||||
{
|
||||
Interlocked.Increment(ref _reportedEagerStale);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -1237,21 +1466,12 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
|
||||
var copies = pending.Copies;
|
||||
readback.Complete(pending.Ticket, (index, bytes) =>
|
||||
if (pending.Eager)
|
||||
{
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{(ulong)bytes.Length:X16}");
|
||||
}
|
||||
});
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
EagerReadbacksUsed++;
|
||||
Interlocked.Increment(ref _reportedEagerUsed);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin);
|
||||
if (pending.IsWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
|
||||
|
|
@ -1269,29 +1489,12 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsPendingDownloadCurrent(PendingDownload pending)
|
||||
private bool IsPieceCurrent(PendingDownload pending, int index)
|
||||
{
|
||||
var total = 0UL;
|
||||
for (var index = 0; index < pending.Copies.Count; index++)
|
||||
{
|
||||
var copy = pending.Copies[index];
|
||||
if (copy.Buffer.LastGpuWriteTick != pending.WriteTicks[index] ||
|
||||
!_gpuModifiedRanges.Contains(copy.Address, copy.Size) ||
|
||||
!ReferenceEquals(FindOwner(copy.Address, copy.Size), copy.Buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
total += copy.Size;
|
||||
}
|
||||
|
||||
var current = 0UL;
|
||||
foreach (var range in _gpuModifiedRanges.GetOverlappingRanges(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin))
|
||||
{
|
||||
current += range.Size;
|
||||
}
|
||||
|
||||
return current == total;
|
||||
var copy = pending.Copies[index];
|
||||
return copy.Buffer.LastGpuWriteTickIn(copy.SourceOffset, copy.Size) == pending.WriteTicks[index] &&
|
||||
_gpuModifiedRanges.Contains(copy.Address, copy.Size) &&
|
||||
ReferenceEquals(FindOwner(copy.Address, copy.Size), copy.Buffer);
|
||||
}
|
||||
|
||||
private static void RecordReadback(ulong windowBegin, ulong windowEnd, bool isWrite, List<DownloadPiece> copies, long started,
|
||||
|
|
@ -1456,6 +1659,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
foreach (var copy in copies)
|
||||
{
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
Interlocked.Increment(ref _gpuModifiedVersion);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -1492,6 +1696,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
foreach (var copy in copies)
|
||||
{
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
Interlocked.Increment(ref _gpuModifiedVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1509,7 +1714,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var ticks = new ulong[copies.Count];
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var written = copies[index].Buffer.LastGpuWriteTick;
|
||||
var written = copies[index].Buffer.LastGpuWriteTickIn(copies[index].SourceOffset, copies[index].Size);
|
||||
if (written == 0)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -1559,7 +1764,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var total = 0UL;
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
var written = copy.Buffer.LastGpuWriteTick;
|
||||
var written = copy.Buffer.LastGpuWriteTickIn(copy.SourceOffset, copy.Size);
|
||||
// A GPU-modified range with no recorded writer: keep the conservative path.
|
||||
if (written == 0)
|
||||
{
|
||||
|
|
@ -1590,6 +1795,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
readback.Read(pieces, waitTick, (index, bytes) =>
|
||||
{
|
||||
GpuWritebackEpoch.Advance();
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{(ulong)bytes.Length:X16}");
|
||||
|
|
@ -1622,6 +1828,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var placement = copy.Placement;
|
||||
var offset = baseOffset + placement.DataOffset;
|
||||
_download.Invalidate(offset, placement.DataSize);
|
||||
GpuWritebackEpoch.Advance();
|
||||
if (!_backing.TryWriteBacking(copy.Address, _download.Mapped.Slice((int)offset, (int)placement.DataSize)))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copy.Address:X16} size=0x{placement.DataSize:X16}");
|
||||
|
|
@ -1702,34 +1909,99 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.BufferDirtySynchronization);
|
||||
var startedAt = BufferUploadProfile.Enabled ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
|
||||
if ((!preserveCpuWriteHotPages && !isWritten && !isTexelBuffer && !_tracker.MayHaveCpuDirtyPages(guestAddress, size)) ||
|
||||
(isWritten && !isTexelBuffer && _gpuModifiedRanges.Contains(guestAddress, size)))
|
||||
var nothingToUpload = isWritten
|
||||
? _gpuModifiedRanges.Contains(guestAddress, size)
|
||||
: !_tracker.MayHaveCpuDirtyPages(guestAddress, size);
|
||||
if (nothingToUpload)
|
||||
{
|
||||
if (BufferUploadProfile.Enabled)
|
||||
BufferUploadProfile.Record(guestAddress, size, 0, 0, 0, System.Diagnostics.Stopwatch.GetTimestamp() - startedAt);
|
||||
return false;
|
||||
return isTexelBuffer && SynchronizeTexelFromImage(buffer, guestAddress, size);
|
||||
}
|
||||
|
||||
profileScope.SwitchPhase(isWritten
|
||||
? RenderPhaseProfile.Phase.BufferDirtySyncWritten
|
||||
: isTexelBuffer ? RenderPhaseProfile.Phase.BufferDirtySyncTexel : RenderPhaseProfile.Phase.BufferDirtySyncUpload);
|
||||
var copies = new List<BufferCopy>();
|
||||
var totalSize = 0UL;
|
||||
GpuBuffer? source = null;
|
||||
_tracker.ForEachUploadRange(
|
||||
guestAddress,
|
||||
size,
|
||||
isWritten,
|
||||
(address, bytes) =>
|
||||
var collector = _uploadCollector;
|
||||
if (collector is null || collector.InUse)
|
||||
{
|
||||
// The first call on a thread keeps its collector; a nested one gets its own.
|
||||
collector = new UploadCollector();
|
||||
_uploadCollector ??= collector;
|
||||
}
|
||||
|
||||
collector.Begin(this, buffer, guestAddress, size);
|
||||
try
|
||||
{
|
||||
return SynchronizeBufferCore(collector, buffer, guestAddress, size, isWritten, isTexelBuffer, preserveCpuWriteHotPages, startedAt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
collector.End();
|
||||
}
|
||||
}
|
||||
|
||||
// The ranges of one synchronization. The delegates are bound once, so a call that has dirty
|
||||
// pages to upload allocates no closure and no list.
|
||||
private sealed class UploadCollector
|
||||
{
|
||||
private GuestBufferCache _cache = null!;
|
||||
private GpuBuffer _buffer = null!;
|
||||
private ulong _guestAddress;
|
||||
private ulong _size;
|
||||
public readonly List<BufferCopy> Copies = new();
|
||||
public ulong TotalSize;
|
||||
public GpuBuffer? Source;
|
||||
public bool InUse;
|
||||
public readonly Action<ulong, ulong> Collect;
|
||||
public readonly Action Upload;
|
||||
|
||||
public UploadCollector()
|
||||
{
|
||||
Collect = (address, bytes) =>
|
||||
{
|
||||
copies.Add(new BufferCopy(totalSize, buffer.Offset(address), bytes));
|
||||
totalSize += bytes;
|
||||
},
|
||||
() => source = _uploader.PrepareSource(buffer.CpuAddress, CollectionsMarshal.AsSpan(copies), totalSize, guestAddress, size),
|
||||
preserveCpuWriteHotPages);
|
||||
Copies.Add(new BufferCopy(TotalSize, _buffer.Offset(address), bytes));
|
||||
TotalSize += bytes;
|
||||
};
|
||||
Upload = () => Source = _cache._uploader.PrepareSource(_buffer.CpuAddress, CollectionsMarshal.AsSpan(Copies), TotalSize, _guestAddress, _size);
|
||||
}
|
||||
|
||||
public void Begin(GuestBufferCache cache, GpuBuffer buffer, ulong guestAddress, ulong size)
|
||||
{
|
||||
InUse = true;
|
||||
_cache = cache;
|
||||
_buffer = buffer;
|
||||
_guestAddress = guestAddress;
|
||||
_size = size;
|
||||
}
|
||||
|
||||
public void End()
|
||||
{
|
||||
InUse = false;
|
||||
_cache = null!;
|
||||
_buffer = null!;
|
||||
Copies.Clear();
|
||||
TotalSize = 0;
|
||||
Source = null;
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic] private static UploadCollector? _uploadCollector;
|
||||
|
||||
private bool SynchronizeBufferCore(UploadCollector collector, GpuBuffer buffer, ulong guestAddress, ulong size, bool isWritten,
|
||||
bool isTexelBuffer, bool preserveCpuWriteHotPages, long startedAt)
|
||||
{
|
||||
var copies = collector.Copies;
|
||||
_tracker.ForEachUploadRange(guestAddress, size, isWritten, collector.Collect, collector.Upload, preserveCpuWriteHotPages);
|
||||
var source = collector.Source;
|
||||
var totalSize = collector.TotalSize;
|
||||
if (source != null)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
buffer.NoteGpuWrite(copy.DstOffset, copy.Size);
|
||||
}
|
||||
|
||||
var command = _scheduler.Current;
|
||||
command.EndRendering();
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
|
|
@ -1771,18 +2043,18 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
|
||||
}
|
||||
|
||||
if (isTexelBuffer)
|
||||
{
|
||||
var copiedFromImage = RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
|
||||
if (copiedFromImage)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
}
|
||||
return isTexelBuffer && SynchronizeTexelFromImage(buffer, guestAddress, size);
|
||||
}
|
||||
|
||||
return copiedFromImage;
|
||||
private bool SynchronizeTexelFromImage(GpuBuffer buffer, ulong guestAddress, ulong size)
|
||||
{
|
||||
var copiedFromImage = RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
|
||||
if (copiedFromImage)
|
||||
{
|
||||
buffer.NoteGpuWrite(buffer.Offset(guestAddress), size);
|
||||
}
|
||||
|
||||
return false;
|
||||
return copiedFromImage;
|
||||
}
|
||||
|
||||
private GpuBuffer? FindOwner(ulong guestAddress, ulong size)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
public sealed class WriteTickMap
|
||||
{
|
||||
private readonly List<Entry> _entries = [];
|
||||
private ulong _floor;
|
||||
|
||||
private readonly record struct Entry(ulong Begin, ulong End, ulong Tick);
|
||||
|
||||
public int Count => _entries.Count;
|
||||
|
||||
public ulong Floor => _floor;
|
||||
|
||||
public void Assign(ulong begin, ulong size, ulong tick)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var end = begin + size;
|
||||
var index = FirstEndingAfter(begin);
|
||||
var removed = 0;
|
||||
while (index + removed < _entries.Count && _entries[index + removed].Begin < end)
|
||||
{
|
||||
removed++;
|
||||
}
|
||||
|
||||
Entry? left = null;
|
||||
Entry? right = null;
|
||||
if (removed != 0)
|
||||
{
|
||||
var first = _entries[index];
|
||||
if (first.Begin < begin)
|
||||
{
|
||||
left = first with { End = begin };
|
||||
}
|
||||
|
||||
var last = _entries[index + removed - 1];
|
||||
if (last.End > end)
|
||||
{
|
||||
right = last with { Begin = end };
|
||||
}
|
||||
|
||||
_entries.RemoveRange(index, removed);
|
||||
}
|
||||
|
||||
var insert = index;
|
||||
if (left is { } leftPiece)
|
||||
{
|
||||
_entries.Insert(insert++, leftPiece);
|
||||
}
|
||||
|
||||
_entries.Insert(insert, new Entry(begin, end, tick));
|
||||
var middle = insert++;
|
||||
if (right is { } rightPiece)
|
||||
{
|
||||
_entries.Insert(insert, rightPiece);
|
||||
}
|
||||
|
||||
if (middle + 1 < _entries.Count)
|
||||
{
|
||||
_ = TryMerge(middle, middle + 1);
|
||||
}
|
||||
|
||||
if (middle > 0)
|
||||
{
|
||||
_ = TryMerge(middle - 1, middle);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong MaxTick(ulong begin, ulong size)
|
||||
{
|
||||
var tick = _floor;
|
||||
if (size == 0)
|
||||
{
|
||||
return tick;
|
||||
}
|
||||
|
||||
var end = begin + size;
|
||||
for (var index = FirstEndingAfter(begin); index < _entries.Count && _entries[index].Begin < end; index++)
|
||||
{
|
||||
tick = Math.Max(tick, _entries[index].Tick);
|
||||
}
|
||||
|
||||
return tick;
|
||||
}
|
||||
|
||||
public void Prune(ulong below)
|
||||
{
|
||||
var kept = 0;
|
||||
for (var index = 0; index < _entries.Count; index++)
|
||||
{
|
||||
var entry = _entries[index];
|
||||
if (entry.Tick < below)
|
||||
{
|
||||
_floor = Math.Max(_floor, entry.Tick);
|
||||
continue;
|
||||
}
|
||||
|
||||
_entries[kept++] = entry;
|
||||
}
|
||||
|
||||
_entries.RemoveRange(kept, _entries.Count - kept);
|
||||
}
|
||||
|
||||
private bool TryMerge(int left, int right)
|
||||
{
|
||||
var a = _entries[left];
|
||||
var b = _entries[right];
|
||||
if (a.End != b.Begin || a.Tick != b.Tick)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_entries[left] = a with { End = b.End };
|
||||
_entries.RemoveAt(right);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int FirstEndingAfter(ulong address)
|
||||
{
|
||||
var low = 0;
|
||||
var high = _entries.Count;
|
||||
while (low < high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (_entries[middle].End <= address)
|
||||
{
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
|
||||
return low;
|
||||
}
|
||||
}
|
||||
|
|
@ -529,7 +529,7 @@ public sealed class CommandStreamQueue
|
|||
if (submission.Kind == CommandSubmissionKind.FrameBoundary)
|
||||
{
|
||||
SlicesRun++;
|
||||
_host.Flush();
|
||||
_host.FlushFrame();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
}
|
||||
|
||||
// A compare-swap loop applies once per try and suspends while the compare fails.
|
||||
if (!atomic.TryApply(_host.TryReadGuest, _host.Memory, out var priorValue, out _, out var comparePassed))
|
||||
if (!atomic.TryApply(_host.TryReadGuestOperand, _host.Memory, out var priorValue, out _, out var comparePassed))
|
||||
{
|
||||
throw _host.Fatal($"The atomic address cannot be accessed: address=0x{atomic.Address:X16} size={atomic.ByteCount}.");
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
|
||||
if (semaphore.IsSignal)
|
||||
{
|
||||
if (!Packets.MemorySemaphorePacket.TrySignal(_host.TryReadGuest, _host.Memory, semaphore.Address, semaphore.WriteSignal, out _))
|
||||
if (!Packets.MemorySemaphorePacket.TrySignal(_host.TryReadGuestOperand, _host.Memory, semaphore.Address, semaphore.WriteSignal, out _))
|
||||
{
|
||||
throw _host.Fatal($"The semaphore cannot be signaled: address=0x{semaphore.Address:X16}.");
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
}
|
||||
|
||||
// A wait takes one token, or suspends until a signal adds one.
|
||||
if (!Packets.MemorySemaphorePacket.TryConsume(_host.TryReadGuest, _host.Memory, semaphore.Address, out var priorValue))
|
||||
if (!Packets.MemorySemaphorePacket.TryConsume(_host.TryReadGuestOperand, _host.Memory, semaphore.Address, out var priorValue))
|
||||
{
|
||||
throw _host.Fatal($"The semaphore cannot be read: address=0x{semaphore.Address:X16}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ namespace SharpEmu.Libs.Gpu.GpuCommands;
|
|||
public sealed partial class GpuCommandInterpreter
|
||||
{
|
||||
private const uint DmaSkippedDestination = 0x3022C;
|
||||
private const uint LodStatsWithoutCounters = 1;
|
||||
|
||||
// Destination selectors 1, 2, 4 and 5 are memory; registers and GDS are not supported.
|
||||
internal void WriteData(ulong destination, ReadOnlySpan<uint> source, uint writeControl)
|
||||
|
|
@ -367,15 +366,16 @@ public sealed partial class GpuCommandInterpreter
|
|||
|
||||
var bufferSize = payload[0];
|
||||
var destination = (payload[1] & 0xFFFF_FFC0u) | ((ulong)payload[2] << 32);
|
||||
if (destination != 0 && bufferSize != 0 && _host.TryRecordLodStatistics(destination, bufferSize))
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (destination != 0 && bufferSize != 0)
|
||||
{
|
||||
var report = new byte[bufferSize];
|
||||
var hasCounters = MipStatistics.Shared.TryWriteReport(report);
|
||||
MipStatistics.Shared.WriteReport(report);
|
||||
WriteBytes(destination, report);
|
||||
if (bufferSize >= sizeof(uint))
|
||||
{
|
||||
WriteDword(destination, hasCounters ? MipStatistics.FirstCounterOffset : LodStatsWithoutCounters);
|
||||
}
|
||||
}
|
||||
|
||||
return 4;
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ public sealed partial class GpuCommandInterpreter
|
|||
var next = packet.PacketAddress + (ulong)packet.Length * sizeof(uint);
|
||||
var expectedHeader = PacketHeader.Make(centroidPacketDwords, PacketOpcode.SetContextRegister);
|
||||
if (packet.Remaining < packet.Length + centroidPacketDwords ||
|
||||
ReadDword(next) != expectedHeader ||
|
||||
RegisterBankLayout.Normalize(ReadDword(next + sizeof(uint))) != ContextRegisterOffset.PaScCentroidPriority0)
|
||||
ReadDword(next, RenderPhaseProfile.CommandReadKind.Payload) != expectedHeader ||
|
||||
RegisterBankLayout.Normalize(ReadDword(next + sizeof(uint), RenderPhaseProfile.CommandReadKind.Payload)) != ContextRegisterOffset.PaScCentroidPriority0)
|
||||
{
|
||||
throw _host.Fatal($"The sample location packet is not followed by the centroid priority packet: header=0x{packet.Header:X8} address=0x{packet.PacketAddress:X16} next=0x{next:X16}.");
|
||||
}
|
||||
|
|
@ -102,16 +102,16 @@ public sealed partial class GpuCommandInterpreter
|
|||
|
||||
var address = packet.PacketAddress;
|
||||
var registerHeader = PacketHeader.Make(3, PacketOpcode.SetContextRegister);
|
||||
if (ReadDword(address + 40) != registerHeader || ReadDword(address + 44) != RegisterBankLayout.DepthInfo ||
|
||||
ReadDword(address + 52) != registerHeader || ReadDword(address + 56) != RegisterBankLayout.DepthView ||
|
||||
ReadDword(address + 64) != registerHeader || ReadDword(address + 68) != RegisterBankLayout.HtileDataBase ||
|
||||
ReadDword(address + 76) != registerHeader || ReadDword(address + 80) != RegisterBankLayout.HtileSurface ||
|
||||
ReadDword(address + 88) != PacketHeader.Make(2, PacketOpcode.Nop))
|
||||
if (ReadDword(address + 40, RenderPhaseProfile.CommandReadKind.Payload) != registerHeader || ReadDword(address + 44, RenderPhaseProfile.CommandReadKind.Payload) != RegisterBankLayout.DepthInfo ||
|
||||
ReadDword(address + 52, RenderPhaseProfile.CommandReadKind.Payload) != registerHeader || ReadDword(address + 56, RenderPhaseProfile.CommandReadKind.Payload) != RegisterBankLayout.DepthView ||
|
||||
ReadDword(address + 64, RenderPhaseProfile.CommandReadKind.Payload) != registerHeader || ReadDword(address + 68, RenderPhaseProfile.CommandReadKind.Payload) != RegisterBankLayout.HtileDataBase ||
|
||||
ReadDword(address + 76, RenderPhaseProfile.CommandReadKind.Payload) != registerHeader || ReadDword(address + 80, RenderPhaseProfile.CommandReadKind.Payload) != RegisterBankLayout.HtileSurface ||
|
||||
ReadDword(address + 88, RenderPhaseProfile.CommandReadKind.Payload) != PacketHeader.Make(2, PacketOpcode.Nop))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sizeXy = ReadDword(address + 92);
|
||||
var sizeXy = ReadDword(address + 92, RenderPhaseProfile.CommandReadKind.Payload);
|
||||
if (sizeXy != 0)
|
||||
{
|
||||
TypedRegisters.CompositeDepthSizeXy = sizeXy;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
return;
|
||||
}
|
||||
|
||||
var value = is64Bit ? ReadQword(address) : ReadDword(address);
|
||||
var value = ReadWaitOperand(address, is64Bit);
|
||||
if (!WaitOperation.TryCompare(value, reference, mask, compareFunction, out var satisfied))
|
||||
{
|
||||
throw _host.Fatal($"The wait compare function is unknown: function={compareFunction} address=0x{address:X16}.");
|
||||
|
|
|
|||
|
|
@ -394,7 +394,12 @@ public sealed partial class GpuCommandInterpreter
|
|||
{
|
||||
RenderPhaseProfile.RecordCommandRead(readKind, sizeof(uint));
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!_host.TryReadGuest(address, bytes))
|
||||
// The packets themselves are command data; anything else is an operand earlier work may write.
|
||||
var read = readKind is RenderPhaseProfile.CommandReadKind.Header or RenderPhaseProfile.CommandReadKind.Payload or
|
||||
RenderPhaseProfile.CommandReadKind.RegisterTable
|
||||
? _host.TryReadGuest(address, bytes)
|
||||
: _host.TryReadGuestOperand(address, bytes);
|
||||
if (!read)
|
||||
{
|
||||
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size=4.");
|
||||
}
|
||||
|
|
@ -406,7 +411,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
{
|
||||
RenderPhaseProfile.RecordCommandRead(RenderPhaseProfile.CommandReadKind.Operand64, sizeof(ulong));
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
if (!_host.TryReadGuest(address, bytes))
|
||||
if (!_host.TryReadGuestOperand(address, bytes))
|
||||
{
|
||||
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size=8.");
|
||||
}
|
||||
|
|
@ -414,10 +419,24 @@ public sealed partial class GpuCommandInterpreter
|
|||
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
}
|
||||
|
||||
private ulong ReadWaitOperand(ulong address, bool is64Bit)
|
||||
{
|
||||
RenderPhaseProfile.RecordCommandRead(is64Bit ? RenderPhaseProfile.CommandReadKind.Operand64 : RenderPhaseProfile.CommandReadKind.Operand32,
|
||||
is64Bit ? sizeof(ulong) : sizeof(uint));
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
var size = is64Bit ? sizeof(ulong) : sizeof(uint);
|
||||
if (!_host.TryReadGuestWaitOperand(address, bytes[..size]))
|
||||
{
|
||||
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size={size}.");
|
||||
}
|
||||
|
||||
return is64Bit ? BinaryPrimitives.ReadUInt64LittleEndian(bytes) : BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
}
|
||||
|
||||
internal void ReadBytes(ulong address, Span<byte> destination)
|
||||
{
|
||||
RenderPhaseProfile.RecordCommandRead(RenderPhaseProfile.CommandReadKind.Other, destination.Length);
|
||||
if (!_host.TryReadGuest(address, destination))
|
||||
if (!_host.TryReadGuestOperand(address, destination))
|
||||
{
|
||||
throw _host.Fatal($"The command stream cannot read guest memory: address=0x{address:X16} size={destination.Length}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,9 +81,19 @@ public interface ICommandStreamHost
|
|||
{
|
||||
ICpuMemory Memory { get; }
|
||||
|
||||
bool TryRecordLodStatistics(ulong destination, uint size) => false;
|
||||
|
||||
// Reads guest memory the GPU may have written; the host synchronizes GPU-owned pages first.
|
||||
bool TryReadGuest(ulong address, Span<byte> destination);
|
||||
|
||||
// Reads a packet operand (a label, a predicate, indirect arguments): memory earlier work may
|
||||
// write, unlike the packets themselves. A host that defers work completes it first.
|
||||
bool TryReadGuestOperand(ulong address, Span<byte> destination) => TryReadGuest(address, destination);
|
||||
|
||||
// Reads the label a wait polls. A stale value only suspends the wait until a retry, so a host
|
||||
// that runs work behind the stream need not complete it first.
|
||||
bool TryReadGuestWaitOperand(ulong address, Span<byte> destination) => TryReadGuestOperand(address, destination);
|
||||
|
||||
// Runs commands other threads posted to this worker. Called before every packet.
|
||||
void RunPendingCommands();
|
||||
|
||||
|
|
@ -92,6 +102,10 @@ public interface ICommandStreamHost
|
|||
|
||||
void Flush();
|
||||
|
||||
// A frame boundary: submits what the frame recorded. The submitter resumes once it returns, so a
|
||||
// host that runs the stream ahead of its recording returns only after the frame is recorded.
|
||||
void FlushFrame() => Flush();
|
||||
|
||||
void FlushAndWait();
|
||||
|
||||
void SynchronizeGpu();
|
||||
|
|
|
|||
|
|
@ -338,8 +338,15 @@ public sealed class ScreenViewportRegisters
|
|||
|
||||
public ScreenViewportRegisters Copy()
|
||||
{
|
||||
var copy = (ScreenViewportRegisters)MemberwiseClone();
|
||||
var copy = CopySharingViewports();
|
||||
copy.Viewports = (ViewportRegisters[])Viewports.Clone();
|
||||
return copy;
|
||||
}
|
||||
|
||||
// A copy that shares the viewport array, the largest part, until RegisterBanks copies it.
|
||||
public ScreenViewportRegisters CopySharingViewports()
|
||||
{
|
||||
var copy = (ScreenViewportRegisters)MemberwiseClone();
|
||||
copy.ClipRectangleLeft = (int[])ClipRectangleLeft.Clone();
|
||||
copy.ClipRectangleTop = (int[])ClipRectangleTop.Clone();
|
||||
copy.ClipRectangleRight = (int[])ClipRectangleRight.Clone();
|
||||
|
|
@ -388,6 +395,9 @@ public sealed class ContextRegisters
|
|||
public EnhancedQualityAntialiasingRegisters EnhancedQualityAntialiasing;
|
||||
public ShaderInterfaceRegisters ShaderInterface = new();
|
||||
|
||||
// A copy that shares every nested part; RegisterBanks copies a part before writing it.
|
||||
public ContextRegisters ShallowCopy() => (ContextRegisters)MemberwiseClone();
|
||||
|
||||
public uint RenderTargetMaskForSlot(uint slot) => (RenderTargetMask >> (int)(slot * 4)) & 0xFu;
|
||||
|
||||
public ContextRegisters Copy()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
|
||||
// The three typed register banks of one queue, with the state the register writers carry between packets.
|
||||
|
|
@ -9,15 +11,31 @@ public sealed class RegisterBanks
|
|||
private ContextRegisters? _savedContext;
|
||||
private uint? _savedCompositeDepthSizeXy;
|
||||
|
||||
public RegisterBanks(Func<string, Exception> fatal) => Fatal = fatal;
|
||||
public RegisterBanks(Func<string, Exception> fatal)
|
||||
: this(fatal, new ContextRegisters(), new ShaderProgramRegisters(), new UserConfigRegisters())
|
||||
{
|
||||
}
|
||||
|
||||
// Banks over given parts, so a copy or a snapshot does not first build empty ones.
|
||||
private RegisterBanks(
|
||||
Func<string, Exception> fatal,
|
||||
ContextRegisters context,
|
||||
ShaderProgramRegisters shader,
|
||||
UserConfigRegisters userConfig)
|
||||
{
|
||||
Fatal = fatal;
|
||||
Context = context;
|
||||
Shader = shader;
|
||||
UserConfig = userConfig;
|
||||
}
|
||||
|
||||
public Func<string, Exception> Fatal { get; }
|
||||
|
||||
public ContextRegisters Context { get; private set; } = new();
|
||||
public ContextRegisters Context { get; private set; }
|
||||
|
||||
public ShaderProgramRegisters Shader { get; private set; } = new();
|
||||
public ShaderProgramRegisters Shader { get; private set; }
|
||||
|
||||
public UserConfigRegisters UserConfig { get; private set; } = new();
|
||||
public UserConfigRegisters UserConfig { get; private set; }
|
||||
|
||||
public bool ContextPushed => _savedContext is not null;
|
||||
|
||||
|
|
@ -31,11 +49,8 @@ public sealed class RegisterBanks
|
|||
// A copy of the live banks; the saved context and the draw-time markers are not part of it.
|
||||
public RegisterBanks Clone()
|
||||
{
|
||||
var clone = new RegisterBanks(Fatal)
|
||||
var clone = new RegisterBanks(Fatal, Context.Copy(), Shader.Copy(), UserConfig.Copy())
|
||||
{
|
||||
Context = Context.Copy(),
|
||||
Shader = Shader.Copy(),
|
||||
UserConfig = UserConfig.Copy(),
|
||||
CompositeDepthSizeXy = CompositeDepthSizeXy,
|
||||
UserDataMarker = UserDataMarker,
|
||||
IndexTypeAndSize = IndexTypeAndSize,
|
||||
|
|
@ -43,9 +58,281 @@ public sealed class RegisterBanks
|
|||
return clone;
|
||||
}
|
||||
|
||||
// The parts a snapshot shares with these banks; each is copied before its next write here.
|
||||
private bool _contextShared;
|
||||
private bool _vertexShared;
|
||||
private bool _pixelShared;
|
||||
private bool _computeShared;
|
||||
private bool _userConfigShared;
|
||||
|
||||
// A read-only copy that shares every part with the live banks until they next write it: a
|
||||
// deferred draw takes one per draw, and between draws only a few parts change.
|
||||
public RegisterBanks Snapshot()
|
||||
{
|
||||
var snapshot = new RegisterBanks(Fatal, Context, Shader.ShareStages(), UserConfig)
|
||||
{
|
||||
CompositeDepthSizeXy = CompositeDepthSizeXy,
|
||||
UserDataMarker = UserDataMarker,
|
||||
IndexTypeAndSize = IndexTypeAndSize,
|
||||
};
|
||||
_contextShared = _vertexShared = _pixelShared = _computeShared = _userConfigShared = true;
|
||||
_pixelScalarsShared = _hullScalarsShared = _geometryScalarsShared = _legacyVertexScalarsShared = _exportScalarsShared = _computeScalarsShared = true;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// Called by the register writers before they store into a part.
|
||||
public void PrepareContextWrite()
|
||||
{
|
||||
if (_contextShared)
|
||||
{
|
||||
Context = Context.ShallowCopy();
|
||||
_contextShared = false;
|
||||
SetContextPartsShared(true);
|
||||
}
|
||||
}
|
||||
|
||||
// The context is copied without its nested parts, and a part only when a writer asks for it
|
||||
// through these accessors: a draw usually rewrites a few plain fields.
|
||||
private bool _screenViewportShared;
|
||||
private bool _viewportsShared;
|
||||
private bool _sampleLocationsShared;
|
||||
private bool _shaderInterfaceShared;
|
||||
private bool _blendControlsShared;
|
||||
private bool _colorTargetsShared;
|
||||
private bool _colorClearWord1Shared;
|
||||
private bool _unmodeledShared;
|
||||
|
||||
private void SetContextPartsShared(bool shared) =>
|
||||
_screenViewportShared = _viewportsShared = _sampleLocationsShared = _shaderInterfaceShared =
|
||||
_blendControlsShared = _colorTargetsShared = _colorClearWord1Shared = _unmodeledShared = shared;
|
||||
|
||||
private void OwnContext(ContextRegisters context)
|
||||
{
|
||||
Context = context;
|
||||
_contextShared = false;
|
||||
SetContextPartsShared(false);
|
||||
}
|
||||
|
||||
public ScreenViewportRegisters ScreenViewportForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_screenViewportShared)
|
||||
{
|
||||
Context.ScreenViewport = Context.ScreenViewport.CopySharingViewports();
|
||||
_screenViewportShared = false;
|
||||
}
|
||||
|
||||
return Context.ScreenViewport;
|
||||
}
|
||||
}
|
||||
|
||||
public ViewportRegisters[] ViewportsForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
var screenViewport = ScreenViewportForWrite;
|
||||
if (_viewportsShared)
|
||||
{
|
||||
screenViewport.Viewports = (ViewportRegisters[])screenViewport.Viewports.Clone();
|
||||
_viewportsShared = false;
|
||||
}
|
||||
|
||||
return screenViewport.Viewports;
|
||||
}
|
||||
}
|
||||
|
||||
public SampleLocationRegisters SampleLocationsForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_sampleLocationsShared)
|
||||
{
|
||||
Context.SampleLocations = Context.SampleLocations.Copy();
|
||||
_sampleLocationsShared = false;
|
||||
}
|
||||
|
||||
return Context.SampleLocations;
|
||||
}
|
||||
}
|
||||
|
||||
public ShaderInterfaceRegisters ShaderInterfaceForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_shaderInterfaceShared)
|
||||
{
|
||||
Context.ShaderInterface = Context.ShaderInterface.Copy();
|
||||
_shaderInterfaceShared = false;
|
||||
}
|
||||
|
||||
return Context.ShaderInterface;
|
||||
}
|
||||
}
|
||||
|
||||
public BlendRegisters[] BlendControlsForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_blendControlsShared)
|
||||
{
|
||||
Context.BlendControls = (BlendRegisters[])Context.BlendControls.Clone();
|
||||
_blendControlsShared = false;
|
||||
}
|
||||
|
||||
return Context.BlendControls;
|
||||
}
|
||||
}
|
||||
|
||||
public ColorTargetWords[] ColorTargetsForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_colorTargetsShared)
|
||||
{
|
||||
Context.ColorTargets = (ColorTargetWords[])Context.ColorTargets.Clone();
|
||||
_colorTargetsShared = false;
|
||||
}
|
||||
|
||||
return Context.ColorTargets;
|
||||
}
|
||||
}
|
||||
|
||||
public uint[] ColorClearWord1ForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_colorClearWord1Shared)
|
||||
{
|
||||
Context.ColorClearWord1 = (uint[])Context.ColorClearWord1.Clone();
|
||||
_colorClearWord1Shared = false;
|
||||
}
|
||||
|
||||
return Context.ColorClearWord1;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<uint, uint> UnmodeledTableRegistersForWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
PrepareContextWrite();
|
||||
if (_unmodeledShared)
|
||||
{
|
||||
Context.UnmodeledTableRegisters = new Dictionary<uint, uint>(Context.UnmodeledTableRegisters);
|
||||
_unmodeledShared = false;
|
||||
}
|
||||
|
||||
return Context.UnmodeledTableRegisters;
|
||||
}
|
||||
}
|
||||
|
||||
public void PrepareUserConfigWrite()
|
||||
{
|
||||
if (_userConfigShared)
|
||||
{
|
||||
UserConfig = UserConfig.Copy();
|
||||
_userConfigShared = false;
|
||||
}
|
||||
}
|
||||
|
||||
// COMPUTE_DISPATCH_INITIATOR, the first register of the compute range.
|
||||
private const uint ComputeRangeStart = 0x200;
|
||||
|
||||
// A stage is copied without its user scalar sets, and a set only when its window is written:
|
||||
// a draw usually rewrites one set of one or two stages.
|
||||
private bool _pixelScalarsShared;
|
||||
private bool _hullScalarsShared;
|
||||
private bool _geometryScalarsShared;
|
||||
private bool _legacyVertexScalarsShared;
|
||||
private bool _exportScalarsShared;
|
||||
private bool _computeScalarsShared;
|
||||
private const uint UserScalarWindow = UserScalarRegisters.Capacity;
|
||||
|
||||
private static bool Overlaps(uint first, uint last, uint windowStart) =>
|
||||
first < windowStart + UserScalarWindow && last >= windowStart;
|
||||
|
||||
// The shader bank's offsets: pixel below SPI_SHADER_PGM_LO_VS, the geometry stages up to the
|
||||
// compute range, compute from there.
|
||||
public void PrepareShaderWrite(uint offset, uint count)
|
||||
{
|
||||
var last = offset + Math.Max(count, 1) - 1;
|
||||
if (offset < ShaderRegisterOffset.SpiShaderPgmLoVs)
|
||||
{
|
||||
if (_pixelShared)
|
||||
{
|
||||
Shader.Pixel = Shader.Pixel.ShallowCopy();
|
||||
_pixelShared = false;
|
||||
}
|
||||
|
||||
if (_pixelScalarsShared && Overlaps(offset, last, ShaderRegisterOffset.SpiShaderUserDataPs0))
|
||||
{
|
||||
Shader.Pixel.UserScalars = Shader.Pixel.UserScalars.Copy();
|
||||
_pixelScalarsShared = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < ComputeRangeStart && last >= ShaderRegisterOffset.SpiShaderPgmLoVs)
|
||||
{
|
||||
if (_vertexShared)
|
||||
{
|
||||
Shader.Vertex = Shader.Vertex.ShallowCopy();
|
||||
_vertexShared = false;
|
||||
}
|
||||
|
||||
var vertex = Shader.Vertex;
|
||||
if (_hullScalarsShared && Overlaps(offset, last, ShaderRegisterOffset.SpiShaderUserDataHs0))
|
||||
{
|
||||
vertex.HullUserScalars = vertex.HullUserScalars.Copy();
|
||||
_hullScalarsShared = false;
|
||||
}
|
||||
|
||||
if (_geometryScalarsShared && Overlaps(offset, last, ShaderRegisterOffset.SpiShaderUserDataGs0))
|
||||
{
|
||||
vertex.GeometryUserScalars = vertex.GeometryUserScalars.Copy();
|
||||
_geometryScalarsShared = false;
|
||||
}
|
||||
|
||||
if (_legacyVertexScalarsShared && Overlaps(offset, last, ShaderRegisterOffset.SpiShaderUserDataVs0))
|
||||
{
|
||||
vertex.LegacyVertexUserScalars = vertex.LegacyVertexUserScalars.Copy();
|
||||
_legacyVertexScalarsShared = false;
|
||||
}
|
||||
|
||||
if (_exportScalarsShared && Overlaps(offset, last, ShaderRegisterOffset.SpiShaderUserDataEs0))
|
||||
{
|
||||
vertex.ExportUserScalars = vertex.ExportUserScalars.Copy();
|
||||
_exportScalarsShared = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (last >= ComputeRangeStart)
|
||||
{
|
||||
if (_computeShared)
|
||||
{
|
||||
Shader.Compute = Shader.Compute.ShallowCopy();
|
||||
_computeShared = false;
|
||||
}
|
||||
|
||||
if (_computeScalarsShared && Overlaps(offset, last, ShaderRegisterOffset.ComputeUserData0))
|
||||
{
|
||||
Shader.Compute.UserScalars = Shader.Compute.UserScalars.Copy();
|
||||
_computeScalarsShared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Context = new ContextRegisters();
|
||||
_contextShared = _vertexShared = _pixelShared = _computeShared = _userConfigShared = false;
|
||||
_pixelScalarsShared = _hullScalarsShared = _geometryScalarsShared = _legacyVertexScalarsShared = _exportScalarsShared = _computeScalarsShared = false;
|
||||
OwnContext(new ContextRegisters());
|
||||
Shader = new ShaderProgramRegisters();
|
||||
UserConfig = new UserConfigRegisters();
|
||||
_savedContext = null;
|
||||
|
|
@ -61,7 +348,7 @@ public sealed class RegisterBanks
|
|||
switch (operation)
|
||||
{
|
||||
case ContextStateOperation.Clear:
|
||||
Context = new ContextRegisters();
|
||||
OwnContext(new ContextRegisters());
|
||||
CompositeDepthSizeXy = null;
|
||||
break;
|
||||
case ContextStateOperation.Push:
|
||||
|
|
@ -73,14 +360,14 @@ public sealed class RegisterBanks
|
|||
throw Fatal("The context state is not pushed.");
|
||||
}
|
||||
|
||||
Context = _savedContext;
|
||||
OwnContext(_savedContext);
|
||||
CompositeDepthSizeXy = _savedCompositeDepthSizeXy;
|
||||
_savedContext = null;
|
||||
_savedCompositeDepthSizeXy = null;
|
||||
break;
|
||||
case ContextStateOperation.PushClear:
|
||||
Push();
|
||||
Context = new ContextRegisters();
|
||||
OwnContext(new ContextRegisters());
|
||||
CompositeDepthSizeXy = null;
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public static class RegisterWriteTable
|
|||
|
||||
public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values)
|
||||
{
|
||||
banks.PrepareContextWrite();
|
||||
var consumed = WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true);
|
||||
if (Rendering.RenderTrace.Enabled)
|
||||
{
|
||||
|
|
@ -43,6 +44,7 @@ public static class RegisterWriteTable
|
|||
// A shader packet without values has no writer; the other banks accept it.
|
||||
public static uint WriteShaderPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values)
|
||||
{
|
||||
banks.PrepareShaderWrite(offset, (uint)values.Length);
|
||||
var consumed = WritePacket(banks, in packet, offset, values, Shader, ShaderIndirect, "shader", emptyIsHandled: false);
|
||||
if (Rendering.RenderTrace.Enabled)
|
||||
{
|
||||
|
|
@ -52,15 +54,19 @@ public static class RegisterWriteTable
|
|||
return consumed;
|
||||
}
|
||||
|
||||
public static uint WriteUserConfigPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
|
||||
WritePacket(banks, in packet, offset, values, UserConfig, UserConfigIndirect, "user-config", emptyIsHandled: true);
|
||||
public static uint WriteUserConfigPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values)
|
||||
{
|
||||
banks.PrepareUserConfigWrite();
|
||||
return WritePacket(banks, in packet, offset, values, UserConfig, UserConfigIndirect, "user-config", emptyIsHandled: true);
|
||||
}
|
||||
|
||||
public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
|
||||
{
|
||||
banks.PrepareContextWrite();
|
||||
if (offset < ContextIndirect.Length && ContextIndirect[offset] is null)
|
||||
{
|
||||
// Keep unknown in-bank table values so a later renderer implementation can decode them.
|
||||
banks.Context.UnmodeledTableRegisters[offset] = value;
|
||||
banks.UnmodeledTableRegistersForWrite[offset] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -88,6 +94,7 @@ public static class RegisterWriteTable
|
|||
|
||||
public static void WriteShaderEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
|
||||
{
|
||||
banks.PrepareShaderWrite(offset, 1);
|
||||
WriteEntry(banks, offset, value, ShaderIndirect, "shader", tableAddress);
|
||||
if (Rendering.RenderTrace.Enabled)
|
||||
TracePixelRegisterWrite(banks, offset, value, tableAddress, "table");
|
||||
|
|
@ -104,8 +111,11 @@ public static class RegisterWriteTable
|
|||
$"value=0x{value:X8} shader=0x{banks.Shader.Pixel.Address:X16}");
|
||||
}
|
||||
|
||||
public static void WriteUserConfigEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) =>
|
||||
public static void WriteUserConfigEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
|
||||
{
|
||||
banks.PrepareUserConfigWrite();
|
||||
WriteEntry(banks, offset, value, UserConfigIndirect, "user-config", tableAddress);
|
||||
}
|
||||
|
||||
private static uint WritePacket(
|
||||
RegisterBanks banks,
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ internal static partial class RegisterWriters
|
|||
{
|
||||
indirect[offset] = static (banks, register, value) =>
|
||||
{
|
||||
banks.Context.ShaderInterface.PixelInterpolatorSettings[register - SpiPsInputCntl0] = value;
|
||||
banks.Context.ShaderInterface.PixelInterpolatorWritten |= 1u << (int)(register - SpiPsInputCntl0);
|
||||
banks.ShaderInterfaceForWrite.PixelInterpolatorSettings[register - SpiPsInputCntl0] = value;
|
||||
banks.ShaderInterfaceForWrite.PixelInterpolatorWritten |= 1u << (int)(register - SpiPsInputCntl0);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ internal static partial class RegisterWriters
|
|||
|
||||
for (var offset = PaScAaSampleLocations0; offset < PaScAaSampleLocations0 + SampleLocationRegisters.LocationCount; offset++)
|
||||
{
|
||||
indirect[offset] = static (banks, register, value) => banks.Context.SampleLocations.Locations[register - PaScAaSampleLocations0] = value;
|
||||
indirect[offset] = static (banks, register, value) => banks.SampleLocationsForWrite.Locations[register - PaScAaSampleLocations0] = value;
|
||||
}
|
||||
|
||||
foreach (var offset in new[]
|
||||
|
|
@ -175,36 +175,36 @@ internal static partial class RegisterWriters
|
|||
indirect[offset] = IgnoreEntry;
|
||||
}
|
||||
|
||||
indirect[SpiVsOutConfig] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputConfiguration = value;
|
||||
indirect[SpiVsOutConfig] = static (banks, _, value) => banks.ShaderInterfaceForWrite.VertexOutputConfiguration = value;
|
||||
indirect[DbRenderOverride] = static (banks, _, value) => banks.Context.DepthRenderOverride = DepthRenderOverrideRegisters.Decode(value);
|
||||
indirect[SpiShaderPosFormat] = static (banks, _, value) => banks.Context.ShaderInterface.PositionExportFormat = value;
|
||||
indirect[SpiShaderIdxFormat] = static (banks, _, value) => banks.Context.ShaderInterface.IndexExportFormat = value;
|
||||
indirect[PaClVsOutCntl] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputControl = value;
|
||||
indirect[GeNggSubgroupCntl] = static (banks, _, value) => banks.Context.ShaderInterface.PrimitiveShaderSubgroupControl = value;
|
||||
indirect[VgtGsInstanceCnt] = static (banks, _, value) => banks.Context.ShaderInterface.GeometryInstanceCount = value;
|
||||
indirect[VgtGsOnchipCntl] = static (banks, _, value) => banks.Context.ShaderInterface.GeometryOnChipControl = value;
|
||||
indirect[VgtHosMaxTessLevel] = static (banks, _, value) => banks.Context.ShaderInterface.MaxTessellationLevel = value;
|
||||
indirect[VgtHosMinTessLevel] = static (banks, _, value) => banks.Context.ShaderInterface.MinTessellationLevel = value;
|
||||
indirect[GeMaxOutputPerSubgroup] = static (banks, _, value) => banks.Context.ShaderInterface.MaxOutputPerSubgroup = value;
|
||||
indirect[VgtEsgsRingItemSize] = static (banks, _, value) => banks.Context.ShaderInterface.ExportRingItemSize = value;
|
||||
indirect[VgtGsMaxVertOut] = static (banks, _, value) => banks.Context.ShaderInterface.GeometryMaxVerticesOut = value;
|
||||
indirect[VgtPrimitiveIdEn] = static (banks, _, value) => banks.Context.ShaderInterface.PrimitiveIdEnable = value;
|
||||
indirect[VgtReuseOff] = static (banks, _, value) => banks.Context.ShaderInterface.VertexReuseOff = value;
|
||||
indirect[SpiShaderPosFormat] = static (banks, _, value) => banks.ShaderInterfaceForWrite.PositionExportFormat = value;
|
||||
indirect[SpiShaderIdxFormat] = static (banks, _, value) => banks.ShaderInterfaceForWrite.IndexExportFormat = value;
|
||||
indirect[PaClVsOutCntl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.VertexOutputControl = value;
|
||||
indirect[GeNggSubgroupCntl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.PrimitiveShaderSubgroupControl = value;
|
||||
indirect[VgtGsInstanceCnt] = static (banks, _, value) => banks.ShaderInterfaceForWrite.GeometryInstanceCount = value;
|
||||
indirect[VgtGsOnchipCntl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.GeometryOnChipControl = value;
|
||||
indirect[VgtHosMaxTessLevel] = static (banks, _, value) => banks.ShaderInterfaceForWrite.MaxTessellationLevel = value;
|
||||
indirect[VgtHosMinTessLevel] = static (banks, _, value) => banks.ShaderInterfaceForWrite.MinTessellationLevel = value;
|
||||
indirect[GeMaxOutputPerSubgroup] = static (banks, _, value) => banks.ShaderInterfaceForWrite.MaxOutputPerSubgroup = value;
|
||||
indirect[VgtEsgsRingItemSize] = static (banks, _, value) => banks.ShaderInterfaceForWrite.ExportRingItemSize = value;
|
||||
indirect[VgtGsMaxVertOut] = static (banks, _, value) => banks.ShaderInterfaceForWrite.GeometryMaxVerticesOut = value;
|
||||
indirect[VgtPrimitiveIdEn] = static (banks, _, value) => banks.ShaderInterfaceForWrite.PrimitiveIdEnable = value;
|
||||
indirect[VgtReuseOff] = static (banks, _, value) => banks.ShaderInterfaceForWrite.VertexReuseOff = value;
|
||||
indirect[VgtMultiPrimIbResetIndex] = static (banks, _, value) => banks.Context.PrimitiveResetIndex = value;
|
||||
indirect[VgtTessDistribution] = static (banks, _, value) => banks.Context.ShaderInterface.TessellationDistribution = value;
|
||||
indirect[VgtTessDistribution] = static (banks, _, value) => banks.ShaderInterfaceForWrite.TessellationDistribution = value;
|
||||
indirect[VgtShaderStagesEn] = static (banks, _, value) => banks.Context.ShaderStages = value;
|
||||
indirect[VgtLsHsConfig] = static (banks, _, value) => banks.Context.ShaderInterface.LocalHullConfiguration = value;
|
||||
indirect[VgtGsOutPrimType] = static (banks, _, value) => banks.Context.ShaderInterface.GeometryOutputPrimitiveType = value;
|
||||
indirect[VgtTfParam] = static (banks, _, value) => banks.Context.ShaderInterface.TessellationFactorParameter = value;
|
||||
indirect[SpiShaderZFormat] = static (banks, _, value) => banks.Context.ShaderInterface.DepthExportFormat = value;
|
||||
indirect[VgtLsHsConfig] = static (banks, _, value) => banks.ShaderInterfaceForWrite.LocalHullConfiguration = value;
|
||||
indirect[VgtGsOutPrimType] = static (banks, _, value) => banks.ShaderInterfaceForWrite.GeometryOutputPrimitiveType = value;
|
||||
indirect[VgtTfParam] = static (banks, _, value) => banks.ShaderInterfaceForWrite.TessellationFactorParameter = value;
|
||||
indirect[SpiShaderZFormat] = static (banks, _, value) => banks.ShaderInterfaceForWrite.DepthExportFormat = value;
|
||||
indirect[SpiShaderColFormat] = ShaderColorFormatEntry;
|
||||
indirect[SpiPsInputEna] = static (banks, _, value) => banks.Context.ShaderInterface.PixelInputEnable = value;
|
||||
indirect[SpiPsInputAddr] = static (banks, _, value) => banks.Context.ShaderInterface.PixelInputAddress = value;
|
||||
indirect[SpiPsInControl] = static (banks, _, value) => banks.Context.ShaderInterface.PixelInputControl = value;
|
||||
indirect[SpiBarycCntl] = static (banks, _, value) => banks.Context.ShaderInterface.BarycentricControl = value;
|
||||
indirect[DbShaderControl] = static (banks, _, value) => banks.Context.ShaderInterface.DepthShaderControl = DepthShaderControlRegisters.Decode(value);
|
||||
indirect[CbShaderMask] = static (banks, _, value) => banks.Context.ShaderInterface.ColorShaderMask = value;
|
||||
indirect[PaScShaderControl] = static (banks, _, value) => banks.Context.ShaderInterface.ScanShaderControl = value;
|
||||
indirect[SpiPsInputEna] = static (banks, _, value) => banks.ShaderInterfaceForWrite.PixelInputEnable = value;
|
||||
indirect[SpiPsInputAddr] = static (banks, _, value) => banks.ShaderInterfaceForWrite.PixelInputAddress = value;
|
||||
indirect[SpiPsInControl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.PixelInputControl = value;
|
||||
indirect[SpiBarycCntl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.BarycentricControl = value;
|
||||
indirect[DbShaderControl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.DepthShaderControl = DepthShaderControlRegisters.Decode(value);
|
||||
indirect[CbShaderMask] = static (banks, _, value) => banks.ShaderInterfaceForWrite.ColorShaderMask = value;
|
||||
indirect[PaScShaderControl] = static (banks, _, value) => banks.ShaderInterfaceForWrite.ScanShaderControl = value;
|
||||
indirect[CbTargetMask] = static (banks, _, value) => banks.Context.RenderTargetMask = value;
|
||||
indirect[PaScScreenScissorTl] = ScreenScissorTopLeftEntry;
|
||||
indirect[PaScScreenScissorBr] = ScreenScissorBottomRightEntry;
|
||||
|
|
@ -248,7 +248,7 @@ internal static partial class RegisterWriters
|
|||
indirect[DbStencilRefMaskBack] = StencilMaskBackEntry;
|
||||
indirect[PaClClipCntl] = static (banks, _, value) => banks.Context.Clip = ClipControlRegisters.Decode(value);
|
||||
indirect[PaSuLineCntl] = LineControlEntry;
|
||||
indirect[PaClVteCntl] = static (banks, _, value) => banks.Context.ScreenViewport.TransformControl = value;
|
||||
indirect[PaClVteCntl] = static (banks, _, value) => banks.ScreenViewportForWrite.TransformControl = value;
|
||||
indirect[PaScModeCntl0] = static (banks, _, value) => banks.Context.ScanMode = ScanModeRegisters.Decode(value);
|
||||
indirect[PaScAaConfig] = static (banks, _, value) => banks.Context.AntialiasingConfig = AntialiasingConfigRegisters.Decode(value);
|
||||
indirect[PaScCentroidPriority0] = CentroidPriorityEntry;
|
||||
|
|
@ -369,8 +369,8 @@ internal static partial class RegisterWriters
|
|||
throw PacketError(banks, "The pixel interpolator packet has an invalid count", in packet, offset, values.Length);
|
||||
}
|
||||
|
||||
values.CopyTo(banks.Context.ShaderInterface.PixelInterpolatorSettings);
|
||||
banks.Context.ShaderInterface.PixelInterpolatorWritten |= (uint)((1ul << values.Length) - 1);
|
||||
values.CopyTo(banks.ShaderInterfaceForWrite.PixelInterpolatorSettings);
|
||||
banks.ShaderInterfaceForWrite.PixelInterpolatorWritten |= (uint)((1ul << values.Length) - 1);
|
||||
return (uint)values.Length;
|
||||
}
|
||||
|
||||
|
|
@ -410,7 +410,7 @@ internal static partial class RegisterWriters
|
|||
// One location at any slot, or all sixteen from the first slot.
|
||||
private static uint SampleLocationsPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values)
|
||||
{
|
||||
var locations = banks.Context.SampleLocations.Locations;
|
||||
var locations = banks.SampleLocationsForWrite.Locations;
|
||||
if (values.Length == 1)
|
||||
{
|
||||
locations[offset - PaScAaSampleLocations0] = values[0];
|
||||
|
|
@ -506,19 +506,19 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void HardwareScreenOffsetEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
banks.Context.ScreenViewport.HardwareOffsetX = RegisterField.Get(value, 0, 0x1FF);
|
||||
banks.Context.ScreenViewport.HardwareOffsetY = RegisterField.Get(value, 16, 0x1FF);
|
||||
banks.ScreenViewportForWrite.HardwareOffsetX = RegisterField.Get(value, 0, 0x1FF);
|
||||
banks.ScreenViewportForWrite.HardwareOffsetY = RegisterField.Get(value, 16, 0x1FF);
|
||||
}
|
||||
|
||||
private static void WindowOffsetEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
banks.Context.ScreenViewport.WindowOffsetX = RegisterField.Coordinate16(value);
|
||||
banks.Context.ScreenViewport.WindowOffsetY = RegisterField.Coordinate16(value >> 16);
|
||||
banks.ScreenViewportForWrite.WindowOffsetX = RegisterField.Coordinate16(value);
|
||||
banks.ScreenViewportForWrite.WindowOffsetY = RegisterField.Coordinate16(value >> 16);
|
||||
}
|
||||
|
||||
private static void ClipRectangleEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var viewport = banks.Context.ScreenViewport;
|
||||
var viewport = banks.ScreenViewportForWrite;
|
||||
if (offset == PaScClipRectRule)
|
||||
{
|
||||
viewport.ClipRectangleRule = (ushort)(value & 0xFFFFu);
|
||||
|
|
@ -598,7 +598,7 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void CentroidPriorityEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var locations = banks.Context.SampleLocations;
|
||||
var locations = banks.SampleLocationsForWrite;
|
||||
switch (offset)
|
||||
{
|
||||
case PaScCentroidPriority0:
|
||||
|
|
@ -612,7 +612,7 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void ShaderColorFormatEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var modes = banks.Context.ShaderInterface.TargetOutputModes;
|
||||
var modes = banks.ShaderInterfaceForWrite.TargetOutputModes;
|
||||
for (var index = 0; index < modes.Length; index++)
|
||||
{
|
||||
modes[index] = (byte)((value >> (index * 4)) & 0xFu);
|
||||
|
|
@ -621,19 +621,19 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void ScreenScissorTopLeftEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
banks.Context.ScreenViewport.ScreenScissorLeft = RegisterField.Coordinate16(value);
|
||||
banks.Context.ScreenViewport.ScreenScissorTop = RegisterField.Coordinate16(value >> 16);
|
||||
banks.ScreenViewportForWrite.ScreenScissorLeft = RegisterField.Coordinate16(value);
|
||||
banks.ScreenViewportForWrite.ScreenScissorTop = RegisterField.Coordinate16(value >> 16);
|
||||
}
|
||||
|
||||
private static void ScreenScissorBottomRightEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
banks.Context.ScreenViewport.ScreenScissorRight = RegisterField.Coordinate16(value);
|
||||
banks.Context.ScreenViewport.ScreenScissorBottom = RegisterField.Coordinate16(value >> 16);
|
||||
banks.ScreenViewportForWrite.ScreenScissorRight = RegisterField.Coordinate16(value);
|
||||
banks.ScreenViewportForWrite.ScreenScissorBottom = RegisterField.Coordinate16(value >> 16);
|
||||
}
|
||||
|
||||
private static void GenericScissorTopLeftEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var viewport = banks.Context.ScreenViewport;
|
||||
var viewport = banks.ScreenViewportForWrite;
|
||||
viewport.GenericScissorLeft = RegisterField.Coordinate15(value);
|
||||
viewport.GenericScissorTop = RegisterField.Coordinate15(value >> 16);
|
||||
viewport.GenericScissorWindowOffsetEnable = !RegisterField.Bit(value, 31);
|
||||
|
|
@ -641,13 +641,13 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void GenericScissorBottomRightEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
banks.Context.ScreenViewport.GenericScissorRight = RegisterField.Coordinate15(value);
|
||||
banks.Context.ScreenViewport.GenericScissorBottom = RegisterField.Coordinate15(value >> 16);
|
||||
banks.ScreenViewportForWrite.GenericScissorRight = RegisterField.Coordinate15(value);
|
||||
banks.ScreenViewportForWrite.GenericScissorBottom = RegisterField.Coordinate15(value >> 16);
|
||||
}
|
||||
|
||||
private static void WindowScissorTopLeftEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var viewport = banks.Context.ScreenViewport;
|
||||
var viewport = banks.ScreenViewportForWrite;
|
||||
viewport.WindowScissorLeft = RegisterField.Coordinate15(value);
|
||||
viewport.WindowScissorTop = RegisterField.Coordinate15(value >> 16);
|
||||
viewport.WindowScissorWindowOffsetEnable = !RegisterField.Bit(value, 31);
|
||||
|
|
@ -655,13 +655,13 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void WindowScissorBottomRightEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
banks.Context.ScreenViewport.WindowScissorRight = RegisterField.Coordinate15(value);
|
||||
banks.Context.ScreenViewport.WindowScissorBottom = RegisterField.Coordinate15(value >> 16);
|
||||
banks.ScreenViewportForWrite.WindowScissorRight = RegisterField.Coordinate15(value);
|
||||
banks.ScreenViewportForWrite.WindowScissorBottom = RegisterField.Coordinate15(value >> 16);
|
||||
}
|
||||
|
||||
private static void ViewportScissorTopLeftEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
ref var viewport = ref banks.Context.ScreenViewport.Viewports[(offset - PaScViewportScissor0Tl) / 2];
|
||||
ref var viewport = ref banks.ViewportsForWrite[(offset - PaScViewportScissor0Tl) / 2];
|
||||
viewport.ScissorLeft = RegisterField.Coordinate15(value);
|
||||
viewport.ScissorTop = RegisterField.Coordinate15(value >> 16);
|
||||
viewport.ScissorWindowOffsetEnable = !RegisterField.Bit(value, 31);
|
||||
|
|
@ -669,21 +669,21 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void ViewportScissorBottomRightEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
ref var viewport = ref banks.Context.ScreenViewport.Viewports[(offset - PaScViewportScissor0Br) / 2];
|
||||
ref var viewport = ref banks.ViewportsForWrite[(offset - PaScViewportScissor0Br) / 2];
|
||||
viewport.ScissorRight = RegisterField.Coordinate15(value);
|
||||
viewport.ScissorBottom = RegisterField.Coordinate15(value >> 16);
|
||||
}
|
||||
|
||||
private static void ViewportZMinEntry(RegisterBanks banks, uint offset, uint value) =>
|
||||
banks.Context.ScreenViewport.Viewports[(offset - PaScViewportZMin0) / 2].MinDepth = RegisterField.AsFloat(value);
|
||||
banks.ViewportsForWrite[(offset - PaScViewportZMin0) / 2].MinDepth = RegisterField.AsFloat(value);
|
||||
|
||||
private static void ViewportZMaxEntry(RegisterBanks banks, uint offset, uint value) =>
|
||||
banks.Context.ScreenViewport.Viewports[(offset - PaScViewportZMax0) / 2].MaxDepth = RegisterField.AsFloat(value);
|
||||
banks.ViewportsForWrite[(offset - PaScViewportZMax0) / 2].MaxDepth = RegisterField.AsFloat(value);
|
||||
|
||||
private static void ViewportScaleOffsetEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var relative = offset - PaClViewportXScale;
|
||||
ref var viewport = ref banks.Context.ScreenViewport.Viewports[relative / ViewportFieldCount];
|
||||
ref var viewport = ref banks.ViewportsForWrite[relative / ViewportFieldCount];
|
||||
var number = RegisterField.AsFloat(value);
|
||||
switch (relative % ViewportFieldCount)
|
||||
{
|
||||
|
|
@ -718,7 +718,7 @@ internal static partial class RegisterWriters
|
|||
|
||||
private static void GuardBandEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
var viewport = banks.Context.ScreenViewport;
|
||||
var viewport = banks.ScreenViewportForWrite;
|
||||
var number = RegisterField.AsFloat(value);
|
||||
switch (offset)
|
||||
{
|
||||
|
|
@ -760,7 +760,7 @@ internal static partial class RegisterWriters
|
|||
}
|
||||
|
||||
private static void BlendControlEntry(RegisterBanks banks, uint offset, uint value) =>
|
||||
banks.Context.BlendControls[offset - CbBlend0Control] = BlendRegisters.Decode(value);
|
||||
banks.BlendControlsForWrite[offset - CbBlend0Control] = BlendRegisters.Decode(value);
|
||||
|
||||
private static void DepthZInfoEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
|
|
@ -804,7 +804,7 @@ internal static partial class RegisterWriters
|
|||
}
|
||||
|
||||
private static ref ColorTargetWords ColorTarget(RegisterBanks banks, uint offset, uint first, uint stride) =>
|
||||
ref banks.Context.ColorTargets[(offset - first) / stride];
|
||||
ref banks.ColorTargetsForWrite[(offset - first) / stride];
|
||||
|
||||
private static void ColorBaseLowEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
|
|
@ -878,7 +878,7 @@ internal static partial class RegisterWriters
|
|||
}
|
||||
|
||||
private static void ColorClearWord1Entry(RegisterBanks banks, uint offset, uint value) =>
|
||||
banks.Context.ColorClearWord1[(offset - CbColor0ClearWord1) / ColorSlotStride] = value;
|
||||
banks.ColorClearWord1ForWrite[(offset - CbColor0ClearWord1) / ColorSlotStride] = value;
|
||||
|
||||
private static void ColorDccBaseLowEntry(RegisterBanks banks, uint offset, uint value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@ public sealed class VertexStageRegisters
|
|||
public UserScalarRegisters LegacyVertexUserScalars = new();
|
||||
public UserScalarRegisters ExportUserScalars = new();
|
||||
|
||||
// The same registers sharing the user scalar sets, which a writer then copies one by one.
|
||||
public VertexStageRegisters ShallowCopy() => (VertexStageRegisters)MemberwiseClone();
|
||||
|
||||
public VertexStageRegisters Copy()
|
||||
{
|
||||
var copy = (VertexStageRegisters)MemberwiseClone();
|
||||
|
|
@ -217,6 +220,8 @@ public sealed class PixelStageRegisters
|
|||
public PixelResource2 Resource2;
|
||||
public UserScalarRegisters UserScalars = new();
|
||||
|
||||
public PixelStageRegisters ShallowCopy() => (PixelStageRegisters)MemberwiseClone();
|
||||
|
||||
public PixelStageRegisters Copy()
|
||||
{
|
||||
var copy = (PixelStageRegisters)MemberwiseClone();
|
||||
|
|
@ -253,6 +258,8 @@ public sealed class ComputeStageRegisters
|
|||
public byte SharedVectorRegisters;
|
||||
public UserScalarRegisters UserScalars = new();
|
||||
|
||||
public ComputeStageRegisters ShallowCopy() => (ComputeStageRegisters)MemberwiseClone();
|
||||
|
||||
public ComputeStageRegisters Copy()
|
||||
{
|
||||
var copy = (ComputeStageRegisters)MemberwiseClone();
|
||||
|
|
@ -264,6 +271,9 @@ public sealed class ComputeStageRegisters
|
|||
// The shader bank: the program registers of every stage and their user scalars.
|
||||
public sealed class ShaderProgramRegisters
|
||||
{
|
||||
// A bank holding the same stage objects, for a snapshot that shares them.
|
||||
public ShaderProgramRegisters ShareStages() => (ShaderProgramRegisters)MemberwiseClone();
|
||||
|
||||
public VertexStageRegisters Vertex = new();
|
||||
public PixelStageRegisters Pixel = new();
|
||||
public ComputeStageRegisters Compute = new();
|
||||
|
|
|
|||
|
|
@ -31,10 +31,13 @@ public sealed unsafe partial class CachedImage
|
|||
SubresourceRange = new ImageSubresourceRange(ViewFormatRules.FullAspects(Backing.Format), baseLevel, levelCount, baseLayer, layerCount),
|
||||
};
|
||||
|
||||
// What a state that needs no barrier returns, so the common case allocates nothing. Callers only read it.
|
||||
private static readonly List<ImageMemoryBarrier2> NoBarriers = [];
|
||||
|
||||
// Barriers for the requested state; a repeated write always gets a barrier.
|
||||
public (List<ImageMemoryBarrier2> Barriers, PipelineStageFlags SourceStages) GetBarriers(ImageLayout layout, AccessFlags access, PipelineStageFlags stage, SubresourceRange? range)
|
||||
{
|
||||
var barriers = new List<ImageMemoryBarrier2>();
|
||||
List<ImageMemoryBarrier2>? barriers = null;
|
||||
PipelineStageFlags sourceStages = 0;
|
||||
if ((access & (WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit)) != 0)
|
||||
{
|
||||
|
|
@ -83,7 +86,7 @@ public sealed unsafe partial class CachedImage
|
|||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
{
|
||||
barriers.Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
(barriers ??= []).Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
sourceStages |= state.Stage;
|
||||
states[index] = new ImageAccessState(stage, access, layout);
|
||||
}
|
||||
|
|
@ -101,15 +104,15 @@ public sealed unsafe partial class CachedImage
|
|||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (state.Layout == layout && state.Access == access && !repeatedWrite)
|
||||
{
|
||||
return (barriers, sourceStages);
|
||||
return (NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
barriers.Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
(barriers ??= []).Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
sourceStages |= state.Stage;
|
||||
}
|
||||
|
||||
Backing.State = new ImageAccessState(stage, access, layout);
|
||||
return (barriers, sourceStages);
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
public void Transition(ImageLayout layout, AccessFlags access, SubresourceRange? range, CommandBuffer command)
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ public sealed unsafe class ColorToMultisampleDepthBlit : IDisposable
|
|||
vk.CmdBeginRenderPass(command, &begin, SubpassContents.Inline);
|
||||
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Graphics, _pipelineLayout, 0, 1, &set, 0, null);
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Graphics, cached.Pipeline);
|
||||
Vulkan.GraphicsDynamicStateEpoch.Advance();
|
||||
Vulkan.NativeBindingEpoch.Advance();
|
||||
var viewport = new Viewport(0.0f, 0.0f, extent.Width, extent.Height, 0.0f, 1.0f);
|
||||
var scissor = new Rect2D(new Offset2D(0, 0), extent);
|
||||
vk.CmdSetViewport(command, 0, 1, &viewport);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -540,8 +540,11 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
return set;
|
||||
}
|
||||
|
||||
private void BindDescriptorSet(CommandBuffer command, DescriptorSet set) =>
|
||||
private void BindDescriptorSet(CommandBuffer command, DescriptorSet set)
|
||||
{
|
||||
_device.Vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, _pipelineLayout, 0, 1, &set, 0, null);
|
||||
Vulkan.NativeBindingEpoch.Advance();
|
||||
}
|
||||
|
||||
private void Record(bool tile, VkBuffer source, ulong sourceOffset, ulong sourceCapacity, VkBuffer target, ulong targetOffset, ulong targetCapacity, List<TransferDispatch> dispatches, bool clearTarget)
|
||||
{
|
||||
|
|
@ -587,6 +590,7 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
infos[2] = new DescriptorBufferInfo(dispatch.ArgumentsBuffer, dispatch.ArgumentsOffset, TileTransferArguments.Size);
|
||||
BindDescriptorSet(command, WriteDescriptorSet(infos, 2));
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, GetPipeline(dispatch.PipelineSlot));
|
||||
Vulkan.NativeBindingEpoch.Advance();
|
||||
vk.CmdDispatch(command, (dispatch.Arguments.Width + 7) / 8, (dispatch.Arguments.Height + 7) / 8, dispatch.Arguments.Depth);
|
||||
}
|
||||
|
||||
|
|
@ -697,6 +701,7 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
fixed (TileTransferArguments* pointer = &arguments)
|
||||
{
|
||||
_device.Vk.CmdPushConstants(command, _pipelineLayout, ShaderStageFlags.ComputeBit, 0, TileTransferArguments.Size, pointer);
|
||||
Vulkan.NativeBindingEpoch.Advance();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -743,6 +748,7 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
AccessFlags.ShaderReadBit | AccessFlags.ShaderWriteBit);
|
||||
VulkanSynchronization.PipelineBarrier(vk,command, PipelineStageFlags.AllCommandsBit | PipelineStageFlags.HostBit, PipelineStageFlags.ComputeShaderBit, 0, 0, null, 2, barriers, 0, null);
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
|
||||
Vulkan.NativeBindingEpoch.Advance();
|
||||
|
||||
var descriptorAlignment = StorageAlignment;
|
||||
var groupLimit = _device.MaxComputeWorkGroupCount;
|
||||
|
|
@ -830,6 +836,7 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
var command = new CommandBuffer(_scheduler.Current.Handle);
|
||||
VulkanSynchronization.PipelineBarrier(vk,command, PipelineStageFlags.AllCommandsBit | PipelineStageFlags.HostBit, PipelineStageFlags.ComputeShaderBit, 0, 0, null, 2, barriers, 0, null);
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, _swapBgra16);
|
||||
Vulkan.NativeBindingEpoch.Advance();
|
||||
BindDescriptorSet(command, WriteDescriptorSet(infos, -1));
|
||||
PushArguments(command, new TileTransferArguments { SourceBase = inputBinding.Base, DestinationBase = outputBinding.Base, Width = pixels });
|
||||
vk.CmdDispatch(command, (pixels + 63) / 64, 1, 1);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,25 @@ public sealed partial class GuestImageCache
|
|||
|
||||
private void InvalidateLookups() => _lookupGeneration++;
|
||||
|
||||
// Changes whenever an image a lookup could find is created, removed or replaced, so a caller
|
||||
// that remembers a lookup's result can tell when it may have changed.
|
||||
public ulong LookupGeneration => _lookupGeneration;
|
||||
|
||||
// What a remembered lookup still does on reuse: the image counts as used now. False when the
|
||||
// image is gone.
|
||||
public bool TryTouchRemembered(ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
if (_slots.TryGet(imageIdentifier) is not { Registered: true } image)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
image.LastAccessTick = _scheduler.CurrentTick;
|
||||
TouchImage(image);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int LookupSlot(in ImageRequest request, bool exactFormat)
|
||||
{
|
||||
var hash = HashCode.Combine(request.Description.Data.Address, request.Description.Data.Size, request.Description.PixelFormat,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,283 @@
|
|||
// 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 static long _dccSyncs;
|
||||
private static long _dccSlicesUndecodable;
|
||||
private static long _dccSlicesMixed;
|
||||
private static long _dccSlicesCleared;
|
||||
|
||||
public static string TakeDccReport() => FormattableString.Invariant(
|
||||
$"[PERF][DCC_SYNC] gpu_syncs={Interlocked.Exchange(ref _dccSyncs, 0)} cleared={Interlocked.Exchange(ref _dccSlicesCleared, 0)} mixed={Interlocked.Exchange(ref _dccSlicesMixed, 0)} undecodable={Interlocked.Exchange(ref _dccSlicesUndecodable, 0)}");
|
||||
|
||||
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))
|
||||
{
|
||||
Interlocked.Increment(ref _dccSlicesUndecodable);
|
||||
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)
|
||||
{
|
||||
Interlocked.Increment(ref _dccSlicesMixed);
|
||||
continue;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _dccSlicesCleared);
|
||||
|
||||
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();
|
||||
|
|
@ -29,13 +299,20 @@ public sealed partial class GuestImageCache
|
|||
|
||||
public bool IsMetadataCleared(ulong address, uint slice) => IsMetadataCleared(address, slice, out _);
|
||||
|
||||
// DCC metadata of a bound target, or the range of a DCC fill seen before its target: both are
|
||||
// filled on the host, which keeps them CPU-readable instead of GPU-written.
|
||||
public bool OverlapsDccMetadata(ulong address, ulong size)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
foreach (var (start, metadata) in _surfaceMetadata)
|
||||
{
|
||||
if (metadata.Kind == SurfaceMetadataKind.Dcc && metadata.Size != 0 &&
|
||||
address < start + metadata.Size && start < address + size)
|
||||
var extent = metadata.Kind switch
|
||||
{
|
||||
SurfaceMetadataKind.Dcc => metadata.Size,
|
||||
SurfaceMetadataKind.PendingDcc => metadata.FillSize,
|
||||
_ => 0UL,
|
||||
};
|
||||
if (extent != 0 && address < start + extent && start < address + size)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -57,7 +334,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))
|
||||
|
|
@ -159,6 +435,12 @@ public sealed partial class GuestImageCache
|
|||
var size = (ulong)layerCount * sliceSize;
|
||||
if (IsValidRange(address, size) && _bufferCache.HasGpuDirtyBytes(address, size))
|
||||
{
|
||||
Interlocked.Increment(ref _dccSyncs);
|
||||
if (Diagnostics.DccWriterTrace.Enabled)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.ReportSync(address, size);
|
||||
}
|
||||
|
||||
_ = _bufferCache.TrySynchronizeCpuRead(address, size);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public sealed partial class GuestImageCache
|
|||
private static ColorTransferPlan? PieceHashPlan(CachedImage image, in ImageRequest request)
|
||||
{
|
||||
ref readonly var info = ref image.Description;
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
|
||||
info.Data.Size < PieceHashMinimumImageSize)
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -761,6 +761,7 @@ public sealed unsafe partial class GuestImageCache
|
|||
_scheduler.QueuePriorityCompletionAction(() =>
|
||||
{
|
||||
download.Invalidate(offset, range.Size);
|
||||
GpuWritebackEpoch.Advance();
|
||||
if (!backing.TryWriteBacking(range.Address, download.Mapped.Slice((int)offset, (int)range.Size)))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"The image readback could not be written to guest memory: address=0x{range.Address:X16} size=0x{range.Size:X}.");
|
||||
|
|
@ -833,7 +834,7 @@ public sealed unsafe partial class GuestImageCache
|
|||
byte stencilClear = 0;
|
||||
if (aspect == ImageAspectFlags.ColorBit)
|
||||
{
|
||||
if (!PackedClearValue.TryDecodeColor(image.Description.PixelFormat, packedClear, out colorClear))
|
||||
if (!PackedClearValue.TryDecodeBufferColor(image.Description.PixelFormat, packedClear, out colorClear))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,28 +123,51 @@ 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);
|
||||
CompleteLookup(result, in request, metadataBaseLayer);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void CompleteLookup(ResourceSlotIdentifier result, in ImageRequest request, uint metadataBaseLayer)
|
||||
{
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
|
||||
|
|
@ -231,17 +254,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 +365,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
image.MarkGpuModified();
|
||||
image.Uses.RenderTarget = true;
|
||||
RefreshFromGuest(imageIdentifier, request);
|
||||
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
var address = request.Description.Metadata.Range.Address;
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ public struct MetadataDescription
|
|||
public GuestSpan Range;
|
||||
public MetadataKind Kind;
|
||||
public uint Control;
|
||||
public uint DccClearWord;
|
||||
public DisplayCompression Compression;
|
||||
public bool StencilCompressed;
|
||||
public bool DccClearRegisterValid;
|
||||
public bool DccAlphaMsb;
|
||||
}
|
||||
|
||||
public readonly record struct SubresourceCount(uint Levels, uint Layers) : IComparable<SubresourceCount>
|
||||
|
|
@ -366,7 +369,8 @@ public struct ImageDescription
|
|||
{
|
||||
case MetadataKind.None:
|
||||
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
|
||||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
|
||||
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
|
||||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
|
||||
}
|
||||
|
|
@ -378,6 +382,11 @@ public struct ImageDescription
|
|||
throw SubmissionScheduler.Fatal($"The HTile metadata is invalid: address=0x{Metadata.Range.Address:X16} size=0x{Metadata.Range.Size:X16}.");
|
||||
}
|
||||
|
||||
if (Metadata.DccClearWord != 0 || Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("HTile metadata carries DCC clear state.");
|
||||
}
|
||||
|
||||
break;
|
||||
case MetadataKind.Dcc:
|
||||
if (Metadata.Range.Address == 0 || Metadata.Range.Address >= TrackerLayout.SpaceBytes ||
|
||||
|
|
@ -507,6 +516,111 @@ 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,
|
||||
};
|
||||
|
||||
// A buffer fill repeats the word in every texel, unlike a DCC clear register.
|
||||
public static bool TryDecodeBufferColor(Format format, uint packed, out ClearColorValue clear)
|
||||
{
|
||||
clear = default;
|
||||
switch (format)
|
||||
{
|
||||
case Format.R16G16Sfloat:
|
||||
case Format.R16G16B16A16Sfloat:
|
||||
var low = BitConverter.UInt16BitsToHalf((ushort)packed);
|
||||
var high = BitConverter.UInt16BitsToHalf((ushort)(packed >> 16));
|
||||
// A float conversion may change a NaN payload; keep the raw fill in that case.
|
||||
if (Half.IsNaN(low) || Half.IsNaN(high)) return false;
|
||||
clear.Float32_0 = clear.Float32_2 = (float)low;
|
||||
clear.Float32_1 = clear.Float32_3 = (float)high;
|
||||
return true;
|
||||
case Format.R32G32Uint:
|
||||
case Format.R32G32B32A32Uint:
|
||||
clear.Uint32_0 = clear.Uint32_1 = clear.Uint32_2 = clear.Uint32_3 = packed;
|
||||
return true;
|
||||
default:
|
||||
return TryDecodeColor(format, packed, out clear);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryDecodeColor(Format format, uint packed, out ClearColorValue clear)
|
||||
{
|
||||
static float Unorm8(uint value) => (value & 0xff) / 255.0f;
|
||||
|
|
|
|||
|
|
@ -43,27 +43,89 @@ 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
|
||||
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
|
||||
|
||||
// Every draw rebuilds its targets' requests (a whole mip layout) from the same register words,
|
||||
// twice: once for the attachments and once for the pixel outputs. The build reads nothing but
|
||||
// its arguments, so a result is reused while they are equal.
|
||||
private struct ColorTargetMemo
|
||||
{
|
||||
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 bool Valid;
|
||||
public ColorTargetWords Words;
|
||||
public uint TargetMask;
|
||||
public uint DrawLayerOffset;
|
||||
public bool IgnoreTargetMask;
|
||||
public ColorTargetResolution? Result;
|
||||
}
|
||||
|
||||
private const int TargetMemoCount = 8;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ColorTargetMemo[]? _colorTargetMemos;
|
||||
|
||||
private static int TargetMemoSlot(ulong address) => (int)((address >> 12) ^ (address >> 20)) & (TargetMemoCount - 1);
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (Rendering.RenderTrace.Enabled)
|
||||
{
|
||||
return ColorTargetCore(in words, targetMask, drawLayerOffset, ignoreTargetMask);
|
||||
}
|
||||
|
||||
var memos = _colorTargetMemos ??= new ColorTargetMemo[TargetMemoCount];
|
||||
ref var memo = ref memos[TargetMemoSlot(words.BaseAddress)];
|
||||
if (memo.Valid && memo.TargetMask == targetMask && memo.DrawLayerOffset == drawLayerOffset &&
|
||||
memo.IgnoreTargetMask == ignoreTargetMask && memo.Words.Equals(words))
|
||||
{
|
||||
return memo.Result;
|
||||
}
|
||||
|
||||
var result = ColorTargetCore(in words, targetMask, drawLayerOffset, ignoreTargetMask);
|
||||
memo = new ColorTargetMemo
|
||||
{
|
||||
Valid = true,
|
||||
Words = words,
|
||||
TargetMask = targetMask,
|
||||
DrawLayerOffset = drawLayerOffset,
|
||||
IgnoreTargetMask = ignoreTargetMask,
|
||||
Result = result,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ColorTargetResolution? ColorTargetCore(in ColorTargetWords words, uint targetMask, uint drawLayerOffset, bool ignoreTargetMask)
|
||||
{
|
||||
var mask = targetMask & 0xF;
|
||||
if (ignoreTargetMask && words.BaseAddress != 0 && mask == 0)
|
||||
|
|
@ -216,11 +278,12 @@ 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];
|
||||
var mipPadded = new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
|
||||
var (mipSpans, mipPadded) = MipLayoutScratch.Rent();
|
||||
TiledSurfaceLayout? volumeLayout = null;
|
||||
ulong size;
|
||||
ulong backingSize = 0;
|
||||
|
|
@ -324,8 +387,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++)
|
||||
|
|
|
|||
|
|
@ -59,8 +59,28 @@ public static partial class ImageRequestBuilders
|
|||
}
|
||||
|
||||
// Builds the request for the bound depth target. Null when no depth or stencil state is active.
|
||||
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) =>
|
||||
DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
|
||||
// Reused while the words and the device are the same, as ColorTarget's requests are.
|
||||
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device)
|
||||
{
|
||||
if (Rendering.RenderTrace.Enabled)
|
||||
{
|
||||
return DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
|
||||
}
|
||||
|
||||
if (_depthTargetMemo is { } memo && ReferenceEquals(memo.Device, device) && memo.Words.Equals(depthWords))
|
||||
{
|
||||
return memo.Result;
|
||||
}
|
||||
|
||||
var result = DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
|
||||
_depthTargetMemo = new DepthTargetMemo(depthWords, device, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed record DepthTargetMemo(DepthTargetWords Words, IImageFormatSupport Device, DepthTargetResolution? Result);
|
||||
|
||||
[ThreadStatic]
|
||||
private static DepthTargetMemo? _depthTargetMemo;
|
||||
|
||||
// Builds the source or destination image used by DB_RENDER_OVERRIDE depth/stencil copies.
|
||||
public static DepthTargetResolution? DepthTargetCopy(in DepthTargetWords depthWords, IImageFormatSupport device, bool writeBuffer) =>
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ public static partial class ImageRequestBuilders
|
|||
return;
|
||||
}
|
||||
|
||||
var spans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
|
||||
var padded = new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
|
||||
var (spans, padded) = MipLayoutScratch.Rent();
|
||||
TileGeometry.TryGetTextureSize(
|
||||
description.GuestFormat, description.Extent.Width, description.Extent.Height, description.Resources.Levels, description.TileMode,
|
||||
out _, spans, padded);
|
||||
|
|
@ -300,12 +299,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)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ public sealed class MipStatistics
|
|||
public const uint FirstCounterOffset = 64;
|
||||
public const int ReportBytes = (int)FirstCounterOffset + SlotCount * CounterBytes;
|
||||
public const uint NoMipSampled = 15;
|
||||
public const uint CounterWords = 257;
|
||||
public const ulong CounterBufferBytes = 2 * CounterWords * sizeof(uint);
|
||||
public const uint NoCounterField = 0x8000;
|
||||
public const uint ReportWithoutCounters = 1;
|
||||
|
||||
private const int CounterBytes = 2 * sizeof(uint);
|
||||
private const uint FinestMip = 0;
|
||||
|
|
@ -29,6 +33,53 @@ public sealed class MipStatistics
|
|||
}
|
||||
}
|
||||
|
||||
public static uint ImageField(ReadOnlySpan<uint> texture)
|
||||
{
|
||||
if (((texture[5] >> 25) & 1) == 0)
|
||||
{
|
||||
return NoCounterField;
|
||||
}
|
||||
|
||||
var counter = texture[6] & 0xFF;
|
||||
var baseLevel = (texture[3] >> 12) & 0xF;
|
||||
var minLod = (texture[1] >> 8) & 0xFFF;
|
||||
return counter | (baseLevel << 8) | (minLod << 16);
|
||||
}
|
||||
|
||||
public static void PackReport(ReadOnlySpan<uint> words, Span<byte> report)
|
||||
{
|
||||
report.Clear();
|
||||
if (report.Length >= sizeof(uint))
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(report, FirstCounterOffset);
|
||||
}
|
||||
|
||||
for (var slot = 0; slot < SlotCount; slot++)
|
||||
{
|
||||
var start = (int)FirstCounterOffset + slot * CounterBytes;
|
||||
if (start + CounterBytes > report.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var finest = words[slot];
|
||||
var count = Math.Min(words[(int)CounterWords + slot], MaxSamples);
|
||||
var level = finest == uint.MaxValue ? NoMipSampled : Math.Min(finest, 14u);
|
||||
var counter = report.Slice(start, CounterBytes);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(counter, count | ((uint)slot << 24));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(counter[sizeof(uint)..], level << 24);
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteReport(Span<byte> report)
|
||||
{
|
||||
var hasCounters = TryWriteReport(report);
|
||||
if (report.Length >= sizeof(uint))
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(report, hasCounters ? FirstCounterOffset : ReportWithoutCounters);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWriteReport(Span<byte> report)
|
||||
{
|
||||
if (report.Length < ReportBytes)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public enum SamplerBorderColor : uint
|
|||
public sealed unsafe class SamplerStore : IDisposable
|
||||
{
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly Dictionary<(uint, uint, uint, uint, bool), Sampler> _samplers = new();
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<(uint, uint, uint, uint, bool), Sampler> _samplers = new();
|
||||
private readonly object _gate = new();
|
||||
|
||||
public SamplerStore(GpuDeviceInfo device) => _device = device;
|
||||
|
|
@ -57,16 +57,22 @@ public sealed unsafe class SamplerStore : IDisposable
|
|||
|
||||
public Sampler GetSampler(in SamplerDescriptorWords words, bool integerView)
|
||||
{
|
||||
var key = (words[0], words[1], words[2], words[3], integerView);
|
||||
// Every sampler binding of every draw asks; only the first of each kind creates.
|
||||
if (_samplers.TryGetValue(key, out var found))
|
||||
{
|
||||
return found;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
var key = (words[0], words[1], words[2], words[3], integerView);
|
||||
if (_samplers.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var sampler = CreateSampler(words, integerView);
|
||||
_samplers.Add(key, sampler);
|
||||
_samplers[key] = sampler;
|
||||
return sampler;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,8 +233,7 @@ public sealed class TextureTransferLayout
|
|||
layout.Pitch = TileGeometry.TexturePitch(format, width, tile);
|
||||
if (tile == GuestTileMode.Linear)
|
||||
{
|
||||
var levelSpans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
|
||||
var paddedSizes = new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
|
||||
var (levelSpans, paddedSizes) = MipLayoutScratch.Rent();
|
||||
TileGeometry.TryGetTextureSize(format, width, height, levels, tile, out _, levelSpans, paddedSizes);
|
||||
for (uint level = 0; level < levels; level++)
|
||||
{
|
||||
|
|
@ -452,7 +451,9 @@ public sealed class TextureTransferLayout
|
|||
transfer.Width = Math.Max((region.ImageExtent.Width + texture.TexelWidth - 1) / texture.TexelWidth, 1);
|
||||
transfer.Height = Math.Max((logicalHeight + texture.TexelHeight - 1) / texture.TexelHeight, 1);
|
||||
transfer.Depth = 1;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB ? region.ImageSubresource.BaseArrayLayer : 0;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
|
||||
? region.ImageSubresource.BaseArrayLayer
|
||||
: 0;
|
||||
transfer.Pitch = Math.Max((pitch + texture.TexelWidth - 1) / texture.TexelWidth, 1);
|
||||
transfer.Tail = tail;
|
||||
transfer.TailX = tail ? mip.TailX : 0;
|
||||
|
|
|
|||
|
|
@ -230,6 +230,75 @@ public static partial class TileGeometry
|
|||
}
|
||||
}
|
||||
|
||||
private static uint Gen5RenderTargetOffset(uint x, uint y, uint bytesPerElement)
|
||||
{
|
||||
uint offset = 0;
|
||||
switch (bytesPerElement)
|
||||
{
|
||||
case 1:
|
||||
offset ^= (y << 2) & 0x0008;
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 3) & 0x00a0;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 6) & 0x1000;
|
||||
offset ^= (y << 7) & 0x4000;
|
||||
offset ^= x & 0x0007;
|
||||
offset ^= (x << 3) & 0x0040;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
return offset;
|
||||
case 2:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 8) & 0x5000;
|
||||
offset ^= (x << 1) & 0x000e;
|
||||
offset ^= (x << 4) & 0x0480;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
return offset;
|
||||
case 4:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 9) & 0x1000;
|
||||
offset ^= (y << 8) & 0x4000;
|
||||
offset ^= (x << 2) & 0x000c;
|
||||
offset ^= (x << 5) & 0x0380;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 9) & 0xa000;
|
||||
return offset;
|
||||
case 8:
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 10) & 0x5000;
|
||||
offset ^= (x << 3) & 0x0008;
|
||||
offset ^= (x << 4) & 0x0460;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 10) & 0x2000;
|
||||
offset ^= (x << 9) & 0x8000;
|
||||
return offset;
|
||||
case 16:
|
||||
offset ^= (x << 4) & 0x0410;
|
||||
offset ^= (x << 5) & 0x0340;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 11) & 0xa000;
|
||||
offset ^= (y << 5) & 0x0f20;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 10) & 0x1000;
|
||||
offset ^= (y << 11) & 0x4000;
|
||||
return offset;
|
||||
default:
|
||||
throw SubmissionScheduler.Fatal($"The Gen5 render-target tile block does not support this element size: bytes={bytesPerElement}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static uint Depth64KB8X(uint x) =>
|
||||
(x & 0x0001) ^ ((x << 1) & 0x0004) ^ ((x << 2) & 0x0010) ^ ((x << 3) & 0x0040) ^ ((x << 5) & 0x0300) ^
|
||||
((x << 4) & 0x0400) ^ ((x << 6) & 0x0800) ^ ((x << 7) & 0x2000) ^ ((x << 8) & 0x8000);
|
||||
|
|
@ -319,6 +388,14 @@ public static partial class TileGeometry
|
|||
|
||||
offset = RenderTargetOffset(x, y, bytes);
|
||||
break;
|
||||
case TileBlockKind.RenderTarget64KBGen5:
|
||||
if (bytes > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = Gen5RenderTargetOffset(x, y, bytes);
|
||||
break;
|
||||
case TileBlockKind.Depth64KB:
|
||||
switch (bytes)
|
||||
{
|
||||
|
|
@ -364,17 +441,19 @@ public static partial class TileGeometry
|
|||
|
||||
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
|
||||
}
|
||||
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
|
||||
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
|
||||
{
|
||||
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
|
||||
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
|
||||
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
}
|
||||
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
|
||||
{
|
||||
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,24 @@ public struct TileLevelSpan
|
|||
|
||||
public readonly record struct TilePaddedSize(uint Width, uint Height);
|
||||
|
||||
// The per-level output arrays of the layout queries, kept per thread: every draw that resolves a
|
||||
// target or a texture needs them for a moment and reads them before the next query.
|
||||
internal static class MipLayoutScratch
|
||||
{
|
||||
[ThreadStatic] private static TileLevelSpan[]? _spans;
|
||||
[ThreadStatic] private static TilePaddedSize[]? _padded;
|
||||
|
||||
// Zeroed, as a new array would be, so a level the query leaves out reads as empty.
|
||||
public static (TileLevelSpan[] Spans, TilePaddedSize[] Padded) Rent()
|
||||
{
|
||||
var spans = _spans ??= new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
|
||||
var padded = _padded ??= new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
|
||||
Array.Clear(spans);
|
||||
Array.Clear(padded);
|
||||
return (spans, padded);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TileBlockKind : uint
|
||||
{
|
||||
Standard256B,
|
||||
|
|
@ -32,6 +50,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 +128,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 +267,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 +318,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 +329,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 +626,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;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ public static class DescriptorWriter
|
|||
{
|
||||
DescriptorBindingKind.Samplers => Silk.NET.Vulkan.DescriptorType.Sampler,
|
||||
DescriptorBindingKind.Buffers or DescriptorBindingKind.GlobalDataShare or DescriptorBindingKind.DeviceAddressPageTable or
|
||||
DescriptorBindingKind.FaultBuffer or DescriptorBindingKind.FlattenedResourceTable or DescriptorBindingKind.ShaderData =>
|
||||
DescriptorBindingKind.FaultBuffer or DescriptorBindingKind.FlattenedResourceTable or DescriptorBindingKind.ShaderData or
|
||||
DescriptorBindingKind.MipStatistics =>
|
||||
Silk.NET.Vulkan.DescriptorType.StorageBuffer,
|
||||
_ => throw SubmissionScheduler.Fatal($"The descriptor binding kind is invalid: kind={kind}."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,11 +36,19 @@ internal interface IShaderPipelineHost
|
|||
|
||||
bool ComputeWave64Supported { get; }
|
||||
|
||||
// True when a 64-invocation wave64 compute workgroup runs as one 64-lane host subgroup.
|
||||
bool ComputeWave64SubgroupNative => false;
|
||||
|
||||
bool ComputeRunsOn32LaneSubgroups(ComputeInputInfo input) => false;
|
||||
|
||||
bool MipStatisticsSubgroupReduction => false;
|
||||
|
||||
bool GraphicsSubgroupOperationsEnabled { get; }
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,13 @@ public sealed class PipelineStaticParameters : IEquatable<PipelineStaticParamete
|
|||
|
||||
public PipelineStaticParameters()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
// The state of a new instance, for a lookup object reused across draws.
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_bytes);
|
||||
DepthClipEnable = true;
|
||||
Samples = 1;
|
||||
ColorCount = 1;
|
||||
|
|
@ -146,6 +153,15 @@ public sealed class PipelineRenderingState : IEquatable<PipelineRenderingState>
|
|||
public Format StencilFormat { get; set; } = Format.Undefined;
|
||||
public uint ColorCount { get; set; }
|
||||
|
||||
// The state of a new instance, for a lookup object reused across draws.
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(ColorFormats);
|
||||
DepthFormat = Format.Undefined;
|
||||
StencilFormat = Format.Undefined;
|
||||
ColorCount = 0;
|
||||
}
|
||||
|
||||
public bool Equals(PipelineRenderingState? other) =>
|
||||
other is not null && ColorCount == other.ColorCount && DepthFormat == other.DepthFormat && StencilFormat == other.StencilFormat &&
|
||||
ColorFormats.AsSpan().SequenceEqual(other.ColorFormats);
|
||||
|
|
@ -179,6 +195,15 @@ public sealed class PipelineVertexInputState : IEquatable<PipelineVertexInputSta
|
|||
public byte BindingCount { get; set; }
|
||||
public byte AttributeCount { get; set; }
|
||||
|
||||
// The state of a new instance, for a lookup object reused across draws.
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(Bindings);
|
||||
Array.Clear(Attributes);
|
||||
BindingCount = 0;
|
||||
AttributeCount = 0;
|
||||
}
|
||||
|
||||
public bool Equals(PipelineVertexInputState? other) =>
|
||||
other is not null && BindingCount == other.BindingCount && AttributeCount == other.AttributeCount &&
|
||||
Bindings.AsSpan().SequenceEqual(other.Bindings) && Attributes.AsSpan().SequenceEqual(other.Attributes);
|
||||
|
|
@ -207,8 +232,9 @@ public sealed class PipelineVertexInputState : IEquatable<PipelineVertexInputSta
|
|||
public sealed class GraphicsPipelineKey : IEquatable<GraphicsPipelineKey>
|
||||
{
|
||||
public required PipelineRenderingState Rendering { get; init; }
|
||||
public ulong VertexProgramId { get; init; }
|
||||
public ulong PixelProgramId { get; init; }
|
||||
// Settable for the lookup key reused across draws; a key in the cache is never changed.
|
||||
public ulong VertexProgramId { get; set; }
|
||||
public ulong PixelProgramId { get; set; }
|
||||
public required PipelineVertexInputState VertexInput { get; init; }
|
||||
public required PipelineStaticParameters StaticParameters { get; init; }
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
private readonly Dictionary<GraphicsPipelineKey, PipelineHandle> _graphicsPipelines = new();
|
||||
private readonly Dictionary<ComputePipelineKey, PipelineHandle> _computePipelines = new();
|
||||
private readonly object _gate = new();
|
||||
// The pipeline maps: a lookup takes this lock or _gate, an addition both; a cached draw so does
|
||||
// not wait for the program preparation a resolving thread holds _gate for.
|
||||
private readonly object _pipelineGate = new();
|
||||
private readonly bool _strictShaders = Environment.GetEnvironmentVariable("SHARPEMU_STRICT_COMPUTE") != "0";
|
||||
private readonly HashSet<(ShaderStage Stage, ulong Hash, uint CodeSize)> _reportedShaderSkips = [];
|
||||
|
||||
|
|
@ -70,6 +73,11 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
return new ShaderSource(registered, hash, userData, userDataBase, stage);
|
||||
}
|
||||
|
||||
// Program resolution runs on the render thread and on a thread resolving draws ahead of it;
|
||||
// the program, decode and materialization caches it updates are used under this lock only.
|
||||
// A pipeline lookup takes _pipelineGate instead, so it does not wait for a resolution.
|
||||
private readonly object _resolveGate = new();
|
||||
|
||||
public GraphicsPrograms GetGraphicsPrograms(
|
||||
VertexStageRegisters vertex,
|
||||
PixelStageRegisters pixel,
|
||||
|
|
@ -78,6 +86,21 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
ReadOnlySpan<ColorComponentMap> targetExportMapping,
|
||||
bool pixelActive,
|
||||
bool depthBound)
|
||||
{
|
||||
lock (_resolveGate)
|
||||
{
|
||||
return GetGraphicsProgramsCore(vertex, pixel, shaderInterface, context, targetExportMapping, pixelActive, depthBound);
|
||||
}
|
||||
}
|
||||
|
||||
private GraphicsPrograms GetGraphicsProgramsCore(
|
||||
VertexStageRegisters vertex,
|
||||
PixelStageRegisters pixel,
|
||||
ShaderInterfaceRegisters shaderInterface,
|
||||
ContextRegisters context,
|
||||
ReadOnlySpan<ColorComponentMap> targetExportMapping,
|
||||
bool pixelActive,
|
||||
bool depthBound)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramPreparation);
|
||||
var vertexSource = PrepareSource(
|
||||
|
|
@ -209,44 +232,53 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
shaderInterface.VertexOutputControl, clipSpace);
|
||||
}
|
||||
|
||||
// One past the highest parameter location the pixel program reads, resolved as its translator does.
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = pixelProgram.Instructions
|
||||
// The interpolated attributes a pixel program reads, ascending. A program never changes, and this
|
||||
// was rebuilt with LINQ on every draw.
|
||||
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Gen5ShaderProgram, uint[]> InterpolatedAttributeCache = new();
|
||||
|
||||
private static uint[] InterpolatedAttributesOf(Gen5ShaderProgram program) =>
|
||||
InterpolatedAttributeCache.GetValue(program, static program => program.Instructions
|
||||
.Select(static instruction => instruction.Control)
|
||||
.OfType<Gen5InterpolationControl>()
|
||||
.Select(static control => control.Attribute)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
.ToArray());
|
||||
|
||||
// One past the highest parameter location the pixel program reads, resolved as its translator does.
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = InterpolatedAttributesOf(pixelProgram);
|
||||
if (attributes.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var controls = new uint[32];
|
||||
Span<uint> controls = stackalloc uint[32];
|
||||
for (var index = 0u; index < (uint)controls.Length; index++)
|
||||
{
|
||||
controls[index] = index < info.InputCount && index < (uint)info.InterpolatorSettings.Length
|
||||
controls[(int)index] = index < info.InputCount && index < (uint)info.InterpolatorSettings.Length
|
||||
? info.InterpolatorSettings[index]
|
||||
: index;
|
||||
}
|
||||
|
||||
return Gen5PixelInputMapping.ResolveLocations(controls, attributes).Max() + 1;
|
||||
}
|
||||
|
||||
private static uint InterpolatedAttributeCount(Gen5ShaderProgram program)
|
||||
{
|
||||
var maxAttribute = -1;
|
||||
foreach (var instruction in program.Instructions)
|
||||
// Attributes are distinct and below 32, so a program never has more than 32 of them.
|
||||
Span<uint> locations = attributes.Length <= 32 ? stackalloc uint[attributes.Length] : new uint[attributes.Length];
|
||||
Gen5PixelInputMapping.ResolveLocations(controls, attributes, locations);
|
||||
var highest = 0u;
|
||||
foreach (var location in locations)
|
||||
{
|
||||
if (instruction.Control is Gen5InterpolationControl interpolation)
|
||||
{
|
||||
maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute);
|
||||
}
|
||||
highest = Math.Max(highest, location);
|
||||
}
|
||||
|
||||
return (uint)(maxAttribute + 1);
|
||||
return highest + 1;
|
||||
}
|
||||
|
||||
// The attribute list is ascending, so its last entry is the highest.
|
||||
private static uint InterpolatedAttributeCount(Gen5ShaderProgram program)
|
||||
{
|
||||
var attributes = InterpolatedAttributesOf(program);
|
||||
return attributes.Length == 0 ? 0 : attributes[^1] + 1;
|
||||
}
|
||||
|
||||
// The bound colour slots in order; each output mode names the kind the pixel program exports.
|
||||
|
|
@ -321,6 +353,20 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
uint dimensionX,
|
||||
uint dimensionY,
|
||||
uint dimensionZ)
|
||||
{
|
||||
lock (_resolveGate)
|
||||
{
|
||||
return GetComputeProgramCore(compute, shaderInterface, dispatchInitiator, dimensionX, dimensionY, dimensionZ);
|
||||
}
|
||||
}
|
||||
|
||||
private ComputeProgram GetComputeProgramCore(
|
||||
ComputeStageRegisters compute,
|
||||
ShaderInterfaceRegisters shaderInterface,
|
||||
uint dispatchInitiator,
|
||||
uint dimensionX,
|
||||
uint dimensionY,
|
||||
uint dimensionZ)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramPreparation);
|
||||
var source = PrepareSource(compute.Address, ShaderStage.Compute, "compute", compute.UserScalars, compute.UserScalarCount, probeWrittenRegisters: false, userDataBase: 0);
|
||||
|
|
@ -417,6 +463,28 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
ShaderProgram pixelProgram)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.PipelineCreation);
|
||||
// The lookup fills objects this thread reuses for every draw; only a new pipeline gets its own.
|
||||
var lookup = _graphicsLookup ??= new GraphicsPipelineKey
|
||||
{
|
||||
Rendering = new PipelineRenderingState(),
|
||||
VertexInput = new PipelineVertexInputState(),
|
||||
StaticParameters = new PipelineStaticParameters(),
|
||||
};
|
||||
lookup.Rendering.Reset();
|
||||
lookup.VertexInput.Reset();
|
||||
lookup.StaticParameters.Reset();
|
||||
FillGraphicsState(colors, in depth, vertexInput, pixelInput, context, in rendering, topology, primitiveRestartEnabled, disableBlending,
|
||||
vertexProgram, pixelProgram, _host.NoAttachmentSampleCounts, lookup.Rendering, lookup.StaticParameters, lookup.VertexInput);
|
||||
lookup.VertexProgramId = vertexProgram.Id;
|
||||
lookup.PixelProgramId = pixelInput is not null ? pixelProgram.Id : 0;
|
||||
lock (_pipelineGate)
|
||||
{
|
||||
if (_graphicsPipelines.TryGetValue(lookup, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
var description = BuildGraphicsDescription(
|
||||
colors, in depth, vertexInput, pixelInput, context, in rendering, topology, primitiveRestartEnabled, disableBlending,
|
||||
vertexProgram, pixelProgram, _host.NoAttachmentSampleCounts);
|
||||
|
|
@ -437,12 +505,19 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
var created = _host.CreateGraphicsPipeline(description);
|
||||
_graphicsPipelines.Add(key, created);
|
||||
lock (_pipelineGate)
|
||||
{
|
||||
_graphicsPipelines.Add(key, created);
|
||||
}
|
||||
|
||||
ShaderCacheCounters.CountGraphicsPipeline();
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static GraphicsPipelineKey? _graphicsLookup;
|
||||
|
||||
// The full static state of one graphics pipeline from the draw's targets, registers and programs.
|
||||
internal static GraphicsPipelineDescription BuildGraphicsDescription(
|
||||
ReadOnlySpan<ColorTargetState> colors,
|
||||
|
|
@ -457,6 +532,43 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
ShaderProgram vertexProgram,
|
||||
ShaderProgram pixelProgram,
|
||||
SampleCountFlags noAttachmentSampleCounts)
|
||||
{
|
||||
var renderingState = new PipelineRenderingState();
|
||||
var parameters = new PipelineStaticParameters();
|
||||
var vertexState = new PipelineVertexInputState();
|
||||
var (vertexStage, pixelStage) = FillGraphicsState(colors, in depth, vertexInput, pixelInput, context, in rendering, topology,
|
||||
primitiveRestartEnabled, disableBlending, vertexProgram, pixelProgram, noAttachmentSampleCounts, renderingState, parameters, vertexState);
|
||||
return new GraphicsPipelineDescription
|
||||
{
|
||||
Rendering = renderingState,
|
||||
VertexInput = vertexState,
|
||||
VertexInfo = vertexInput,
|
||||
VertexProgram = vertexProgram,
|
||||
VertexStage = vertexStage,
|
||||
PixelInfo = pixelInput,
|
||||
PixelProgram = pixelProgram,
|
||||
PixelStage = pixelStage,
|
||||
StaticParameters = parameters,
|
||||
};
|
||||
}
|
||||
|
||||
// Writes the pipeline state into objects in their initial state; returns the stage programs.
|
||||
private static (ShaderProgramInfo VertexStage, ShaderProgramInfo? PixelStage) FillGraphicsState(
|
||||
ReadOnlySpan<ColorTargetState> colors,
|
||||
in DepthAttachmentState depth,
|
||||
VertexInputInfo vertexInput,
|
||||
PixelInputInfo? pixelInput,
|
||||
ContextRegisters context,
|
||||
in RenderingState rendering,
|
||||
PrimitiveTopology topology,
|
||||
bool primitiveRestartEnabled,
|
||||
bool disableBlending,
|
||||
ShaderProgram vertexProgram,
|
||||
ShaderProgram pixelProgram,
|
||||
SampleCountFlags noAttachmentSampleCounts,
|
||||
PipelineRenderingState renderingState,
|
||||
PipelineStaticParameters parameters,
|
||||
PipelineVertexInputState vertexState)
|
||||
{
|
||||
if (colors.Length > PipelineStaticParameters.ColorAttachmentCount)
|
||||
{
|
||||
|
|
@ -477,8 +589,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
var vertexStage = vertexInput.Stage.Program ?? throw SubmissionScheduler.Fatal("The vertex stage has no program.");
|
||||
var pixelStage = pixelActive ? pixelInput!.Stage.Program ?? throw SubmissionScheduler.Fatal("The pixel stage has no program.") : null;
|
||||
var colorCount = (uint)colors.Length;
|
||||
var parameters = new PipelineStaticParameters { ColorCount = colorCount };
|
||||
var renderingState = new PipelineRenderingState { ColorCount = colorCount };
|
||||
parameters.ColorCount = colorCount;
|
||||
renderingState.ColorCount = colorCount;
|
||||
for (var index = 0; index < colors.Length; index++)
|
||||
{
|
||||
ref readonly var color = ref colors[index];
|
||||
|
|
@ -564,18 +676,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
parameters.SetBlendBypass(index, ((context.ColorTargets[slot].Info >> 16) & 0x1) != 0);
|
||||
}
|
||||
|
||||
return new GraphicsPipelineDescription
|
||||
{
|
||||
Rendering = renderingState,
|
||||
VertexInput = BuildVertexInputState(vertexInput),
|
||||
VertexInfo = vertexInput,
|
||||
VertexProgram = vertexProgram,
|
||||
VertexStage = vertexStage,
|
||||
PixelInfo = pixelInput,
|
||||
PixelProgram = pixelProgram,
|
||||
PixelStage = pixelStage,
|
||||
StaticParameters = parameters,
|
||||
};
|
||||
FillVertexInputState(vertexInput, vertexState);
|
||||
return (vertexStage, pixelStage);
|
||||
}
|
||||
|
||||
internal static GraphicsPipelineKey KeyOf(GraphicsPipelineDescription description) => new()
|
||||
|
|
@ -587,18 +689,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
StaticParameters = description.StaticParameters,
|
||||
};
|
||||
|
||||
private static PipelineVertexInputState BuildVertexInputState(VertexInputInfo info)
|
||||
private static void FillVertexInputState(VertexInputInfo info, PipelineVertexInputState state)
|
||||
{
|
||||
if (info.Buffers.Length > VertexInputInfo.MaxBuffers || info.Attributes.Length > VertexInputInfo.MaxBuffers)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"The vertex input is too large: buffers={info.Buffers.Length} attributes={info.Attributes.Length}.");
|
||||
}
|
||||
|
||||
var state = new PipelineVertexInputState
|
||||
{
|
||||
BindingCount = (byte)info.Buffers.Length,
|
||||
AttributeCount = (byte)info.Attributes.Length,
|
||||
};
|
||||
state.BindingCount = (byte)info.Buffers.Length;
|
||||
state.AttributeCount = (byte)info.Attributes.Length;
|
||||
for (var binding = 0; binding < info.Buffers.Length; binding++)
|
||||
{
|
||||
var buffer = info.Buffers[binding];
|
||||
|
|
@ -615,8 +714,6 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
|
||||
state.Attributes[index] = new PipelineVertexAttribute(attribute.OffsetBytes, (byte)attribute.BufferIndex);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public PipelineHandle CreateComputePipeline(ComputeInputInfo input, ShaderProgram program)
|
||||
|
|
@ -629,6 +726,14 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
|
||||
var stage = input.Stage.Program ?? throw SubmissionScheduler.Fatal("The compute stage has no program.");
|
||||
var key = new ComputePipelineKey(program.Id);
|
||||
lock (_pipelineGate)
|
||||
{
|
||||
if (_computePipelines.TryGetValue(key, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_computePipelines.TryGetValue(key, out var cached))
|
||||
|
|
@ -642,7 +747,11 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
var created = _host.CreateComputePipeline(new ComputePipelineDescription { Input = input, Program = program, Stage = stage });
|
||||
_computePipelines.Add(key, created);
|
||||
lock (_pipelineGate)
|
||||
{
|
||||
_computePipelines.Add(key, created);
|
||||
}
|
||||
|
||||
ShaderCacheCounters.CountComputePipeline();
|
||||
return created;
|
||||
}
|
||||
|
|
@ -659,6 +768,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
|
||||
var stage = input.Stage.Program ?? throw SubmissionScheduler.Fatal("The compute stage has no program.");
|
||||
var key = new ComputePipelineKey(program.Id);
|
||||
lock (_pipelineGate)
|
||||
{
|
||||
if (_computePipelines.TryGetValue(key, out var cached))
|
||||
{
|
||||
handle = cached;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_computePipelines.TryGetValue(key, out var cached))
|
||||
|
|
@ -674,7 +792,11 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
return false;
|
||||
}
|
||||
|
||||
_computePipelines.Add(key, created);
|
||||
lock (_pipelineGate)
|
||||
{
|
||||
_computePipelines.Add(key, created);
|
||||
}
|
||||
|
||||
ShaderCacheCounters.CountComputePipeline();
|
||||
handle = created;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu.Rendering;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
|
|
@ -68,6 +69,37 @@ public sealed class ProgramKey : IEquatable<ProgramKey>
|
|||
public override int GetHashCode() => HashCode.Combine(Stage, Hash, UserDataCount, CodeSize, StaticState.Length);
|
||||
}
|
||||
|
||||
// A program key as a lookup sees it: the static state words stay in the caller's list, so a draw
|
||||
// that finds its program does not build a key object or copy the words.
|
||||
internal readonly ref struct ProgramKeyProbe(ShaderStage stage, ulong hash, uint userDataCount, uint codeSize, ReadOnlySpan<uint> staticState)
|
||||
{
|
||||
public readonly ShaderStage Stage = stage;
|
||||
public readonly ulong Hash = hash;
|
||||
public readonly uint UserDataCount = userDataCount;
|
||||
public readonly uint CodeSize = codeSize;
|
||||
public readonly ReadOnlySpan<uint> StaticState = staticState;
|
||||
}
|
||||
|
||||
internal sealed class ProgramKeyComparer : IEqualityComparer<ProgramKey>, IAlternateEqualityComparer<ProgramKeyProbe, ProgramKey>
|
||||
{
|
||||
public static readonly ProgramKeyComparer Instance = new();
|
||||
|
||||
public bool Equals(ProgramKey? x, ProgramKey? y) => x is null ? y is null : x.Equals(y);
|
||||
|
||||
public int GetHashCode(ProgramKey key) => key.GetHashCode();
|
||||
|
||||
public bool Equals(ProgramKeyProbe alternate, ProgramKey key) =>
|
||||
alternate.Stage == key.Stage && alternate.Hash == key.Hash && alternate.UserDataCount == key.UserDataCount &&
|
||||
alternate.CodeSize == key.CodeSize && alternate.StaticState.SequenceEqual(key.StaticState);
|
||||
|
||||
// The same value as ProgramKey.GetHashCode.
|
||||
public int GetHashCode(ProgramKeyProbe alternate) =>
|
||||
HashCode.Combine(alternate.Stage, alternate.Hash, alternate.UserDataCount, alternate.CodeSize, alternate.StaticState.Length);
|
||||
|
||||
public ProgramKey Create(ProgramKeyProbe alternate) =>
|
||||
new(alternate.Stage, alternate.Hash, alternate.UserDataCount, alternate.CodeSize, alternate.StaticState.ToArray());
|
||||
}
|
||||
|
||||
// One compiled module of a program entry for one specialization and push-data start.
|
||||
internal sealed class ProgramPermutation
|
||||
{
|
||||
|
|
@ -102,7 +134,7 @@ internal sealed class ShaderProgramCache
|
|||
private readonly CpuContext _context;
|
||||
private readonly IGuestGpuBackend _compiler;
|
||||
private readonly IShaderPipelineHost _host;
|
||||
private readonly Dictionary<ProgramKey, ProgramSourceEntry> _programs = new();
|
||||
private readonly Dictionary<ProgramKey, ProgramSourceEntry> _programs = new(ProgramKeyComparer.Instance);
|
||||
private readonly Dictionary<(ulong Hash, uint CodeSize), Gen5ShaderProgram> _decoded = new();
|
||||
private readonly Dictionary<(ulong Hash, uint CodeSize), ShaderCodeCapture> _codeCaptures = new();
|
||||
private readonly List<uint> _staticState = new(StageStaticKey.MaxWords);
|
||||
|
|
@ -216,13 +248,14 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
private ShaderProgram GetOrCompileCore(ShaderSource source, StageCompileOptions options, ref uint pushDataCursor, out ShaderStageResources stage)
|
||||
{
|
||||
ProgramKey key;
|
||||
ProgramKey? key = null;
|
||||
ProgramSourceEntry? entry;
|
||||
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramCacheLookup))
|
||||
{
|
||||
BuildStaticState(source.Stage, options);
|
||||
key = new ProgramKey(source.Stage, source.Hash, (uint)source.UserData.Length, source.CodeSize, _staticState.ToArray());
|
||||
_programs.TryGetValue(key, out entry);
|
||||
var probe = new ProgramKeyProbe(source.Stage, source.Hash, (uint)source.UserData.Length, source.CodeSize,
|
||||
CollectionsMarshal.AsSpan(_staticState));
|
||||
_programs.GetAlternateLookup<ProgramKeyProbe>().TryGetValue(probe, out entry);
|
||||
}
|
||||
var sourceWasCached = entry is not null;
|
||||
if (RenderTrace.Enabled && RenderTrace.Pipeline())
|
||||
|
|
@ -246,12 +279,14 @@ internal sealed class ShaderProgramCache
|
|||
if (entry is null)
|
||||
{
|
||||
entry = CreateEntry(source, options);
|
||||
key = BuildProgramKey(source);
|
||||
_programs.Add(key, entry);
|
||||
ShaderCacheCounters.CountProgram();
|
||||
}
|
||||
|
||||
var snapshot = new ResourceSnapshot();
|
||||
var specialization = new ResourceSpecialization();
|
||||
// Both are assigned by the materialization that follows; a failed one throws.
|
||||
ResourceSnapshot snapshot = null!;
|
||||
ResourceSpecialization specialization = null!;
|
||||
var captureIndirectImageFailure = _spirvDumpEnabled ? ShaderPermutationDump.CreateFailureCapture(source) : null;
|
||||
if (Diagnostics.GpuReadTrace.Enabled)
|
||||
{
|
||||
|
|
@ -295,7 +330,7 @@ internal sealed class ShaderProgramCache
|
|||
}
|
||||
}
|
||||
|
||||
var permutation = CompilePermutation(source, options, entry, specialization, pushDataCursor, key, sourceWasCached);
|
||||
var permutation = CompilePermutation(source, options, entry, specialization, pushDataCursor, key ?? BuildProgramKey(source), sourceWasCached);
|
||||
entry.Permutations.Add(permutation);
|
||||
ShaderCacheCounters.CountPermutation();
|
||||
stage = CreateStageResources(permutation.Program, snapshot, source, options);
|
||||
|
|
@ -308,6 +343,10 @@ internal sealed class ShaderProgramCache
|
|||
return permutation.Handle;
|
||||
}
|
||||
|
||||
// The key of the entry the static state was just built for.
|
||||
private ProgramKey BuildProgramKey(ShaderSource source) =>
|
||||
new(source.Stage, source.Hash, (uint)source.UserData.Length, source.CodeSize, _staticState.ToArray());
|
||||
|
||||
private static ShaderStageResources CreateStageResources(
|
||||
ShaderProgramInfo program, ResourceSnapshot snapshot, ShaderSource source, StageCompileOptions options)
|
||||
{
|
||||
|
|
@ -593,6 +632,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 +645,7 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
|
|
@ -632,18 +673,21 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
SupportsPerVertexPixelInputs = _host.PerVertexPixelInputsSupported,
|
||||
PixelInputAddress = options.PixelInputAddress,
|
||||
PixelInputCntl = interpolators,
|
||||
MipStatisticsSubgroupReduction = _host.MipStatisticsSubgroupReduction,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled);
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve, _host.ComputeWave64SubgroupNative,
|
||||
_host.ComputeRunsOn32LaneSubgroups(options.ComputeInfo!));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -659,14 +703,18 @@ 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, bool hostWave64Supported = false, bool hostComputeSubgroup32 = false) =>
|
||||
new(plan, resources, layout)
|
||||
{
|
||||
WaveSize = info.WaveSize,
|
||||
HostWave64Supported = info.WaveSize == 64 && hostWave64Supported,
|
||||
HostComputeSubgroup32 = info.WaveSize == 64 && !hostWave64Supported && hostComputeSubgroup32,
|
||||
EnableExecGuardElision = info.WaveSize != 64 || execGuardElision,
|
||||
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 +723,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, bool hostWave64Supported = false, bool hostComputeSubgroup32 = false)
|
||||
{
|
||||
compiled = null;
|
||||
layout = null;
|
||||
|
|
@ -692,7 +741,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, hostWave64Supported, hostComputeSubgroup32);
|
||||
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,282 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Rendering;
|
||||
|
||||
// Resolves the programs of deferred draws on a worker thread, in issue order, while the render
|
||||
// thread interprets and records. The render thread takes a result for the draw it runs only when
|
||||
// the resolution completed, used the inputs the draw derives and still matches the host's
|
||||
// GPU-ownership state; otherwise it resolves the draw itself.
|
||||
public sealed class ParallelProgramPrefetch : IProgramPrefetch, IDisposable
|
||||
{
|
||||
public sealed class Job(RegisterBanks banks, long gate)
|
||||
{
|
||||
internal readonly RegisterBanks Banks = banks;
|
||||
// The count of stream items other than draws that must have run before the job resolves.
|
||||
internal readonly long Gate = gate;
|
||||
internal GraphicsProgramInputs Inputs;
|
||||
internal GraphicsPrograms? Programs;
|
||||
internal object? Assumptions;
|
||||
// 0 queued, 1 resolved, 2 abandoned.
|
||||
internal volatile int State;
|
||||
}
|
||||
|
||||
private readonly RenderExecutor _executor;
|
||||
private readonly IRenderHost _host;
|
||||
private readonly IConcurrentResolveHost _concurrent;
|
||||
private readonly object _lock = new();
|
||||
private readonly Queue<Job> _queue = new();
|
||||
private readonly Thread _thread;
|
||||
private int _outstanding;
|
||||
private long _finished;
|
||||
private long _executedGate;
|
||||
// The gate the worker sleeps on; long.MaxValue while it does not.
|
||||
private long _sleepingOnGate = long.MaxValue;
|
||||
private bool _paused;
|
||||
private bool _running;
|
||||
private bool _stopping;
|
||||
|
||||
public ParallelProgramPrefetch(RenderExecutor executor, IRenderHost host, IConcurrentResolveHost concurrent)
|
||||
{
|
||||
_executor = executor;
|
||||
_host = host;
|
||||
_concurrent = concurrent;
|
||||
_thread = new Thread(Run) { IsBackground = true, Name = "SharpEmu program resolve" };
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
// The job of the draw the render thread runs now; set around each deferred draw.
|
||||
public Job? Current { get; set; }
|
||||
|
||||
public long Submitted { get; private set; }
|
||||
|
||||
// A gate above zero holds the job until AdvanceGate reaches it: with the command stream on its own
|
||||
// thread, draws are submitted ahead of the work before them, which may change what they read.
|
||||
public Job Submit(RegisterBanks banks, long gate = 0)
|
||||
{
|
||||
var job = new Job(banks, gate);
|
||||
lock (_lock)
|
||||
{
|
||||
_queue.Enqueue(job);
|
||||
Submitted++;
|
||||
_outstanding++;
|
||||
Monitor.PulseAll(_lock);
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
// Returns once the worker has finished every submitted job: it reads guest memory and cache
|
||||
// state, which the render thread changes only with the worker idle.
|
||||
public void WaitIdle()
|
||||
{
|
||||
if (Volatile.Read(ref _outstanding) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
while (_outstanding != 0)
|
||||
{
|
||||
Monitor.Wait(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The render thread ran this many non-draw items: jobs gated on them may resolve.
|
||||
public void AdvanceGate(long executed)
|
||||
{
|
||||
Interlocked.Exchange(ref _executedGate, executed);
|
||||
// Only a worker asleep on a gate this reaches needs a wake: one per item costs the render
|
||||
// thread a kernel call each.
|
||||
if (Volatile.Read(ref _sleepingOnGate) <= executed)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
Monitor.PulseAll(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns once the worker finished the first count jobs it was given, so a non-draw item runs
|
||||
// with no resolution of the draws before it still reading.
|
||||
public void WaitFinished(long count)
|
||||
{
|
||||
if (Volatile.Read(ref _finished) >= count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
while (_finished < count)
|
||||
{
|
||||
Monitor.Wait(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the worker between jobs, for host work that changes what a resolution reads.
|
||||
public void Pause()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_paused = true;
|
||||
while (_running)
|
||||
{
|
||||
Monitor.Wait(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_paused = false;
|
||||
Monitor.PulseAll(_lock);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryTakeGraphics(RegisterBanks banks, in GraphicsProgramInputs inputs, out GraphicsPrograms programs)
|
||||
{
|
||||
programs = null!;
|
||||
var job = Current;
|
||||
if (job is null || !ReferenceEquals(job.Banks, banks))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Current = null;
|
||||
WaitFor(job);
|
||||
if (job.State != 1 || !job.Inputs.Equals(inputs) || !_concurrent.IsStillCurrent(job.Assumptions!))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
programs = job.Programs!;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void WaitFor(Job job)
|
||||
{
|
||||
var spinner = new SpinWait();
|
||||
for (var spin = 0; spin < 64 && job.State == 0; spin++)
|
||||
{
|
||||
spinner.SpinOnce(sleep1Threshold: -1);
|
||||
}
|
||||
|
||||
if (job.State != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
while (job.State == 0)
|
||||
{
|
||||
Monitor.Wait(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
SubmissionScheduler.ThrowOnFatal = true;
|
||||
while (true)
|
||||
{
|
||||
Job job;
|
||||
lock (_lock)
|
||||
{
|
||||
while (_queue.Count == 0 && !_stopping)
|
||||
{
|
||||
Monitor.Wait(_lock);
|
||||
}
|
||||
|
||||
if (_stopping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
job = _queue.Peek();
|
||||
if (job.Gate > Volatile.Read(ref _executedGate))
|
||||
{
|
||||
// The gate usually opens within a few items: spin before sleeping.
|
||||
Monitor.Exit(_lock);
|
||||
try
|
||||
{
|
||||
var spinner = new SpinWait();
|
||||
for (var spin = 0; spin < 50 && job.Gate > Volatile.Read(ref _executedGate); spin++)
|
||||
{
|
||||
spinner.SpinOnce(sleep1Threshold: -1);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Enter(_lock);
|
||||
}
|
||||
}
|
||||
|
||||
while ((_paused || job.Gate > Volatile.Read(ref _executedGate)) && !_stopping)
|
||||
{
|
||||
Interlocked.Exchange(ref _sleepingOnGate, _paused ? long.MaxValue : job.Gate);
|
||||
if (!_paused && job.Gate <= Volatile.Read(ref _executedGate))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Monitor.Wait(_lock, 50);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _sleepingOnGate, long.MaxValue);
|
||||
if (_stopping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_queue.Dequeue();
|
||||
_running = true;
|
||||
}
|
||||
|
||||
Resolve(job);
|
||||
lock (_lock)
|
||||
{
|
||||
_outstanding--;
|
||||
_finished++;
|
||||
_running = false;
|
||||
Monitor.PulseAll(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Resolve(Job job)
|
||||
{
|
||||
_concurrent.BeginConcurrentResolve();
|
||||
try
|
||||
{
|
||||
job.Inputs = RenderExecutor.ProgramInputsOf(job.Banks, _host.FormatSupport, _host.Fatal);
|
||||
job.Programs = _executor.ResolveGraphicsPrograms(job.Banks, in job.Inputs);
|
||||
job.Assumptions = _concurrent.EndConcurrentResolve();
|
||||
job.State = 1;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// The render thread resolves the draw itself, and fails there if the error is real.
|
||||
job.Assumptions = _concurrent.EndConcurrentResolve();
|
||||
job.State = 2;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_stopping = true;
|
||||
Monitor.PulseAll(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ public sealed partial class RenderExecutor
|
|||
if (Diagnostics.DccWriterTrace.Enabled)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.CurrentProgram = program.Hash;
|
||||
Diagnostics.DccWriterTrace.CurrentKind = "compute";
|
||||
}
|
||||
|
||||
if (RenderTrace.Enabled)
|
||||
|
|
@ -714,15 +715,19 @@ public sealed partial class RenderExecutor
|
|||
var hash = input.Stage.Program!.Hash;
|
||||
if (!_host.TryClearImageFromBuffer(address, clear.Size, clear.PackedClear))
|
||||
{
|
||||
// A metadata fill may run before its target is bound; the store keeps it pending and the dispatch runs.
|
||||
// A metadata fill may run before its target is bound; the store keeps it pending. The fill
|
||||
// itself then runs on the host when the range is known DCC metadata: run as a dispatch it
|
||||
// leaves the metadata GPU-written, and every later bind of the surface waits for the GPU to
|
||||
// read it back (Astro Bot fills several surfaces this way each frame).
|
||||
var registered = _host.TryAbsorbDccFill(address, clear.Size, clear.PackedClear);
|
||||
var filledOnHost = !registered && _host.TryFillDccMetadata(address, clear.Size, clear.PackedClear);
|
||||
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
|
||||
{
|
||||
RenderTrace.Write(
|
||||
$"{(registered ? "Tracked" : "Deferred")} a metadata clear: shader=0x{hash:X16} address=0x{address:X16} size=0x{clear.Size:X16} value=0x{clear.PackedClear:X8}");
|
||||
$"{(registered ? "Tracked" : filledOnHost ? "Filled on the host" : "Deferred")} a metadata clear: shader=0x{hash:X16} address=0x{address:X16} size=0x{clear.Size:X16} value=0x{clear.PackedClear:X8}");
|
||||
}
|
||||
|
||||
return registered;
|
||||
return registered || filledOnHost;
|
||||
}
|
||||
|
||||
if (RenderTrace.Enabled && RenderTrace.ImageClear())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Rendering;
|
||||
|
||||
// What a draw's program resolution reads besides the stage and context registers: the export
|
||||
// mapping of every bound color slot, whether the pixel stage runs and whether a depth target is
|
||||
// bound. All three follow from the register banks alone.
|
||||
public struct GraphicsProgramInputs : IEquatable<GraphicsProgramInputs>
|
||||
{
|
||||
[System.Runtime.CompilerServices.InlineArray(RenderingState.ColorAttachmentCapacity)]
|
||||
public struct ExportMappings
|
||||
{
|
||||
private ColorComponentMap _element0;
|
||||
}
|
||||
|
||||
public ExportMappings Mapping;
|
||||
public bool PixelActive;
|
||||
public bool DepthBound;
|
||||
|
||||
public static GraphicsProgramInputs Create()
|
||||
{
|
||||
var inputs = new GraphicsProgramInputs();
|
||||
((Span<ColorComponentMap>)inputs.Mapping).Fill(ColorComponentMap.Identity);
|
||||
return inputs;
|
||||
}
|
||||
|
||||
public readonly bool Equals(GraphicsProgramInputs other) =>
|
||||
PixelActive == other.PixelActive && DepthBound == other.DepthBound &&
|
||||
((ReadOnlySpan<ColorComponentMap>)Mapping).SequenceEqual(other.Mapping);
|
||||
|
||||
public override readonly bool Equals(object? obj) => obj is GraphicsProgramInputs other && Equals(other);
|
||||
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
foreach (var map in (ReadOnlySpan<ColorComponentMap>)Mapping)
|
||||
{
|
||||
hash.Add(map.Packed);
|
||||
}
|
||||
|
||||
hash.Add(PixelActive);
|
||||
hash.Add(DepthBound);
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
// The host side of resolving programs on a thread other than the render thread. The resolving
|
||||
// thread records what it assumed of the host's GPU-ownership state; the render thread checks,
|
||||
// when it takes the result, that the assumptions still hold.
|
||||
public interface IConcurrentResolveHost
|
||||
{
|
||||
// On the resolving thread, around one resolution.
|
||||
void BeginConcurrentResolve();
|
||||
|
||||
object EndConcurrentResolve();
|
||||
|
||||
// On the render thread.
|
||||
bool IsStillCurrent(object assumptions);
|
||||
}
|
||||
|
||||
// Program resolutions computed ahead of their draws. A draw takes one only when it was resolved
|
||||
// from the same register banks with the same inputs; otherwise it resolves its programs itself.
|
||||
public interface IProgramPrefetch
|
||||
{
|
||||
bool TryTakeGraphics(RegisterBanks banks, in GraphicsProgramInputs inputs, out GraphicsPrograms programs);
|
||||
}
|
||||
|
||||
public sealed partial class RenderExecutor
|
||||
{
|
||||
public IProgramPrefetch? Prefetch { get; set; }
|
||||
|
||||
// The inputs ResolveShaderPrograms derives from the targets TryResolveDrawTargets binds, from the
|
||||
// registers alone: the target requests and the pixel and depth activity, without the images.
|
||||
public static GraphicsProgramInputs ProgramInputsOf(RegisterBanks banks, IImageFormatSupport formats, Func<string, Exception> fatal)
|
||||
{
|
||||
var inputs = GraphicsProgramInputs.Create();
|
||||
var context = banks.Context;
|
||||
Span<ColorComponentMap> mapping = inputs.Mapping;
|
||||
for (var slot = 0u; slot < ContextRegisters.ColorTargetCount; slot++)
|
||||
{
|
||||
if (slot != 0 && (context.RenderTargetMaskForSlot(slot) == 0 || context.ColorTargets[slot].BaseAddress == 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ColorTargetResolver.Resolve(context, slot, DrawLayerOffset, ignoreTargetMask: false, out var resolvedSlot) is { } resolution)
|
||||
{
|
||||
mapping[(int)resolvedSlot] = resolution.ExportMapping;
|
||||
}
|
||||
}
|
||||
|
||||
inputs.DepthBound = DepthTargetResolver.Resolve(context, formats, fatal) is not null;
|
||||
inputs.PixelActive = HasActivePixelShader(banks);
|
||||
return inputs;
|
||||
}
|
||||
|
||||
// The programs of a draw with these banks and inputs, as ResolveShaderPrograms resolves them.
|
||||
public GraphicsPrograms ResolveGraphicsPrograms(RegisterBanks banks, in GraphicsProgramInputs inputs)
|
||||
{
|
||||
var context = banks.Context;
|
||||
return _pipelines.GetGraphicsPrograms(
|
||||
banks.Shader.Vertex,
|
||||
banks.Shader.Pixel,
|
||||
context.ShaderInterface,
|
||||
context,
|
||||
inputs.Mapping,
|
||||
inputs.PixelActive,
|
||||
inputs.DepthBound);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,8 @@ public sealed partial class RenderExecutor
|
|||
throw _host.Fatal($"The vertex input has too many buffers: count={buffers.Length} max={VertexInputInfo.MaxBuffers}.");
|
||||
}
|
||||
|
||||
Span<VertexBufferRange> ranges = stackalloc VertexBufferRange[VertexInputInfo.MaxBuffers];
|
||||
// Sized to this draw's buffers: a MaxBuffers span is over a kilobyte, zeroed on every draw.
|
||||
Span<VertexBufferRange> ranges = stackalloc VertexBufferRange[buffers.Length];
|
||||
var rangeCount = 0;
|
||||
foreach (ref readonly var vertex in buffers.AsSpan())
|
||||
{
|
||||
|
|
@ -54,7 +55,7 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
ranges[..rangeCount].Sort(static (left, right) => left.BaseAddress.CompareTo(right.BaseAddress));
|
||||
Span<VertexBufferRange> merged = stackalloc VertexBufferRange[VertexInputInfo.MaxBuffers];
|
||||
Span<VertexBufferRange> merged = stackalloc VertexBufferRange[buffers.Length];
|
||||
var mergedCount = 0;
|
||||
for (var i = 0; i < rangeCount; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ public sealed partial class RenderExecutor
|
|||
private bool TryResolveDrawTargets(RegisterBanks banks, in DrawCall draw, ref DrawState state)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawTargetResolution);
|
||||
if (Diagnostics.DccWriterTrace.Enabled)
|
||||
{
|
||||
Diagnostics.DccWriterTrace.CurrentKind = "draw";
|
||||
Diagnostics.DccWriterTrace.CurrentProgram = 0;
|
||||
}
|
||||
|
||||
var context = banks.Context;
|
||||
if (TryResolveMultisampleColor(context))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -667,29 +667,22 @@ public sealed partial class RenderExecutor
|
|||
private void ResolveShaderPrograms(RegisterBanks banks, ref DrawState state)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawProgramResolution);
|
||||
var context = banks.Context;
|
||||
Span<ColorComponentMapArray> mappingStorage = stackalloc ColorComponentMapArray[1];
|
||||
Span<ColorComponentMap> targetExportMapping = mappingStorage[0];
|
||||
targetExportMapping.Fill(ColorComponentMap.Identity);
|
||||
var inputs = GraphicsProgramInputs.Create();
|
||||
Span<ColorComponentMap> targetExportMapping = inputs.Mapping;
|
||||
foreach (ref readonly var color in BoundColors(ref state))
|
||||
{
|
||||
targetExportMapping[(int)color.Slot] = color.Resolution.ExportMapping;
|
||||
}
|
||||
|
||||
state.Programs = _pipelines.GetGraphicsPrograms(
|
||||
banks.Shader.Vertex,
|
||||
banks.Shader.Pixel,
|
||||
context.ShaderInterface,
|
||||
context,
|
||||
targetExportMapping,
|
||||
state.PixelActive,
|
||||
state.Depth.HasTarget);
|
||||
}
|
||||
inputs.PixelActive = state.PixelActive;
|
||||
inputs.DepthBound = state.Depth.HasTarget;
|
||||
if (Prefetch is { } prefetch && prefetch.TryTakeGraphics(banks, in inputs, out var prefetched))
|
||||
{
|
||||
state.Programs = prefetched;
|
||||
return;
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.InlineArray(RenderingState.ColorAttachmentCapacity)]
|
||||
private struct ColorComponentMapArray
|
||||
{
|
||||
private ColorComponentMap _element0;
|
||||
state.Programs = ResolveGraphicsPrograms(banks, in inputs);
|
||||
}
|
||||
|
||||
private static void TraceDrawState(ulong submitId, RegisterBanks banks, in DrawCall draw, in DrawState state)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,29 @@ public readonly record struct RenderingAttachment(
|
|||
bool HasDepth,
|
||||
bool DepthClear,
|
||||
bool HasStencil,
|
||||
bool StencilClear);
|
||||
bool StencilClear)
|
||||
{
|
||||
// Compared by handle value: the generated comparison goes through the object comparer of ImageView
|
||||
// and boxes both views, which a draw pays for every attachment it re-checks.
|
||||
public bool Equals(RenderingAttachment other) =>
|
||||
View.Handle == other.View.Handle &&
|
||||
Layout == other.Layout &&
|
||||
Format == other.Format &&
|
||||
ClearWord0 == other.ClearWord0 &&
|
||||
ClearWord1 == other.ClearWord1 &&
|
||||
ClearWord2 == other.ClearWord2 &&
|
||||
ClearWord3 == other.ClearWord3 &&
|
||||
IsClear == other.IsClear &&
|
||||
HasDepth == other.HasDepth &&
|
||||
DepthClear == other.DepthClear &&
|
||||
HasStencil == other.HasStencil &&
|
||||
StencilClear == other.StencilClear;
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(
|
||||
View.Handle,
|
||||
HashCode.Combine(Layout, Format, ClearWord0, ClearWord1, ClearWord2, ClearWord3),
|
||||
HashCode.Combine(IsClear, HasDepth, DepthClear, HasStencil, StencilClear));
|
||||
}
|
||||
|
||||
[InlineArray(RenderingState.ColorAttachmentCapacity)]
|
||||
public struct RenderingColorAttachments
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Scheduling;
|
||||
|
||||
// Counts the stores of GPU data into guest memory: buffer and image downloads, and buffer transfers
|
||||
// the caches complete there. Work done ahead of the render thread from guest memory is still
|
||||
// current while the count has not moved.
|
||||
internal static class GpuWritebackEpoch
|
||||
{
|
||||
private static long _value;
|
||||
|
||||
public static long Value => Volatile.Read(ref _value);
|
||||
|
||||
public static void Advance() => Interlocked.Increment(ref _value);
|
||||
}
|
||||
|
|
@ -1,8 +1,23 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Scheduling;
|
||||
|
||||
public readonly record struct MemoryBarrierRequest(
|
||||
PipelineStageFlags SourceStages,
|
||||
PipelineStageFlags DestinationStages,
|
||||
AccessFlags SourceAccess,
|
||||
AccessFlags DestinationAccess)
|
||||
{
|
||||
public MemoryBarrierRequest Union(in MemoryBarrierRequest other) => new(
|
||||
SourceStages | other.SourceStages,
|
||||
DestinationStages | other.DestinationStages,
|
||||
SourceAccess | other.SourceAccess,
|
||||
DestinationAccess | other.DestinationAccess);
|
||||
}
|
||||
|
||||
public enum RecordedOperation : uint
|
||||
{
|
||||
DispatchDirect,
|
||||
|
|
@ -23,6 +38,8 @@ public sealed class RecordingBuffer
|
|||
private readonly IRenderingState _rendering;
|
||||
private SubmissionContext? _context;
|
||||
private bool _handleWasRequested;
|
||||
private MemoryBarrierRequest _pendingBarrier;
|
||||
private bool _hasPendingBarrier;
|
||||
|
||||
internal nint Buffer;
|
||||
|
||||
|
|
@ -51,7 +68,11 @@ public sealed class RecordingBuffer
|
|||
|
||||
public bool IsInvalid => Buffer == 0;
|
||||
|
||||
internal bool HasPendingCommands => _handleWasRequested || _rendering.IsRendering;
|
||||
internal bool HasPendingCommands => _handleWasRequested || _hasPendingBarrier || _rendering.IsRendering;
|
||||
|
||||
public long MergedBarriers { get; private set; }
|
||||
|
||||
public long RecordedBarriers { get; private set; }
|
||||
|
||||
// The native handle stays valid only until the next submission.
|
||||
public nint Handle
|
||||
|
|
@ -64,11 +85,68 @@ public sealed class RecordingBuffer
|
|||
}
|
||||
|
||||
// A caller can record native commands after it obtains the handle.
|
||||
FlushPendingBarrier();
|
||||
_handleWasRequested = true;
|
||||
return Buffer;
|
||||
}
|
||||
}
|
||||
|
||||
public nint StateHandle
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsInvalid)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("The command buffer is not recording.");
|
||||
}
|
||||
|
||||
_handleWasRequested = true;
|
||||
return Buffer;
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestMemoryBarrier(in MemoryBarrierRequest request)
|
||||
{
|
||||
if (IsInvalid || _rendering.IsRendering)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("A memory barrier needs a recording command buffer outside rendering.");
|
||||
}
|
||||
|
||||
if (_hasPendingBarrier)
|
||||
{
|
||||
_pendingBarrier = _pendingBarrier.Union(request);
|
||||
MergedBarriers++;
|
||||
if (Vulkan.GpuSyncProfile.Enabled)
|
||||
{
|
||||
Vulkan.GpuSyncProfile.CountMergedRequest();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingBarrier = request;
|
||||
_hasPendingBarrier = true;
|
||||
}
|
||||
|
||||
private void FlushPendingBarrier()
|
||||
{
|
||||
if (!_hasPendingBarrier)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hasPendingBarrier = false;
|
||||
if (_rendering.IsRendering)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("A pending memory barrier reached a rendering scope.");
|
||||
}
|
||||
|
||||
var recorder = Owner.MemoryBarrierRecorder ??
|
||||
throw SubmissionScheduler.Fatal("A memory barrier was requested without a recorder.");
|
||||
recorder(Buffer, _pendingBarrier);
|
||||
RecordedBarriers++;
|
||||
}
|
||||
|
||||
public SubmissionContext Context => _context ?? throw SubmissionScheduler.Fatal("The command buffer has no submission context.");
|
||||
|
||||
public void SetDebugInfo(uint op, ulong submitId, uint arg0 = 0, uint arg1 = 0, uint arg2 = 0, uint arg3 = 0, ulong arg4 = 0)
|
||||
|
|
@ -101,6 +179,7 @@ public sealed class RecordingBuffer
|
|||
|
||||
_device.BeginBuffer(Buffer);
|
||||
_handleWasRequested = false;
|
||||
_hasPendingBarrier = false;
|
||||
SetDebugInfo((uint)RecordedOperation.Unknown, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,13 +84,27 @@ public sealed class SubmissionScheduler : IGpuTickScheduler, IDisposable
|
|||
|
||||
public ulong CurrentTick => _timeline.CurrentTick;
|
||||
|
||||
public Action<nint, MemoryBarrierRequest>? MemoryBarrierRecorder { get; set; }
|
||||
|
||||
public TickTimeline Timeline => _timeline;
|
||||
|
||||
internal bool PriorityWorkerAlive => _priorityThread.IsAlive;
|
||||
|
||||
// Set on a thread that does work ahead of the render thread: there a fatal condition only
|
||||
// abandons that work, which the render thread then does itself and fails on if it is real.
|
||||
[ThreadStatic]
|
||||
internal static bool ThrowOnFatal;
|
||||
|
||||
public sealed class AbandonedWorkException(string message) : Exception(message);
|
||||
|
||||
// Report the fatal error, then return an exception to stop the caller.
|
||||
internal static Exception Fatal(string message)
|
||||
{
|
||||
if (ThrowOnFatal)
|
||||
{
|
||||
throw new AbandonedWorkException(message);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[GPU][FATAL] {message}");
|
||||
OnFatal(message);
|
||||
return new InvalidOperationException(message);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
internal enum NativeStateKind
|
||||
{
|
||||
Pipeline,
|
||||
PushConstants,
|
||||
Descriptors,
|
||||
VertexBuffers,
|
||||
IndexBuffer,
|
||||
Count,
|
||||
}
|
||||
|
||||
internal static class GpuSyncProfile
|
||||
{
|
||||
private static readonly long[] _nativeStateRecorded = new long[(int)NativeStateKind.Count];
|
||||
private static readonly long[] _nativeStateReused = new long[(int)NativeStateKind.Count];
|
||||
private static long _barriers;
|
||||
private static long _drainingBarriers;
|
||||
private static long _memoryBarriers;
|
||||
private static long _bufferBarriers;
|
||||
private static long _imageBarriers;
|
||||
private static long _mergedRequests;
|
||||
private static long _renderingScopes;
|
||||
private static long _batchFlushes;
|
||||
|
||||
public static bool Enabled => VideoOut.RenderPhaseProfile.Enabled;
|
||||
|
||||
public static void CountBarrier(PipelineStageFlags source, PipelineStageFlags destination, uint memory, uint buffer, uint image)
|
||||
{
|
||||
Interlocked.Increment(ref _barriers);
|
||||
if ((source & PipelineStageFlags.AllCommandsBit) != 0 && (destination & (PipelineStageFlags.AllCommandsBit | PipelineStageFlags.TopOfPipeBit | PipelineStageFlags.DrawIndirectBit)) != 0)
|
||||
{
|
||||
Interlocked.Increment(ref _drainingBarriers);
|
||||
}
|
||||
|
||||
Interlocked.Add(ref _memoryBarriers, memory);
|
||||
Interlocked.Add(ref _bufferBarriers, buffer);
|
||||
Interlocked.Add(ref _imageBarriers, image);
|
||||
}
|
||||
|
||||
public static void CountMergedRequest() => Interlocked.Increment(ref _mergedRequests);
|
||||
|
||||
public static void CountRenderingScope() => Interlocked.Increment(ref _renderingScopes);
|
||||
|
||||
public static void CountBatchFlush() => Interlocked.Increment(ref _batchFlushes);
|
||||
|
||||
public static void CountNativeState(NativeStateKind kind, bool reused)
|
||||
{
|
||||
if (reused)
|
||||
{
|
||||
Interlocked.Increment(ref _nativeStateReused[(int)kind]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref _nativeStateRecorded[(int)kind]);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TakeNativeStateReport()
|
||||
{
|
||||
var parts = new string[(int)NativeStateKind.Count];
|
||||
for (var index = 0; index < parts.Length; index++)
|
||||
{
|
||||
var reused = Interlocked.Exchange(ref _nativeStateReused[index], 0);
|
||||
var recorded = Interlocked.Exchange(ref _nativeStateRecorded[index], 0);
|
||||
parts[index] = FormattableString.Invariant($"{(NativeStateKind)index}={reused}/{reused + recorded}");
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
public static string TakeReport() => FormattableString.Invariant(
|
||||
$"[PERF][GPU_SYNC] barriers={Interlocked.Exchange(ref _barriers, 0)} draining={Interlocked.Exchange(ref _drainingBarriers, 0)} memory={Interlocked.Exchange(ref _memoryBarriers, 0)} buffer={Interlocked.Exchange(ref _bufferBarriers, 0)} image={Interlocked.Exchange(ref _imageBarriers, 0)} merged_requests={Interlocked.Exchange(ref _mergedRequests, 0)} rendering_scopes={Interlocked.Exchange(ref _renderingScopes, 0)} draw_count_flushes={Interlocked.Exchange(ref _batchFlushes, 0)} reused {TakeNativeStateReport()}");
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
// Advanced by every graphics pipeline bind or dynamic state change outside the guest draw path:
|
||||
// such a pipeline may fix the state the guest pipelines leave dynamic, so the dynamic state a
|
||||
// guest draw last set no longer holds in the command buffer.
|
||||
internal static class GraphicsDynamicStateEpoch
|
||||
{
|
||||
private static long _value;
|
||||
|
||||
public static long Value => Volatile.Read(ref _value);
|
||||
|
||||
public static void Advance() => Interlocked.Increment(ref _value);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
internal static class NativeBindingEpoch
|
||||
{
|
||||
private static long _value;
|
||||
|
||||
public static long Value => Volatile.Read(ref _value);
|
||||
|
||||
public static void Advance() => Interlocked.Increment(ref _value);
|
||||
}
|
||||
|
|
@ -172,10 +172,13 @@ internal sealed unsafe class VulkanAsyncReadback : IDisposable
|
|||
PSemaphores = &semaphore,
|
||||
PValues = &signalValue,
|
||||
};
|
||||
var waitStart = VideoOut.GpuWaitSiteProfile.Begin();
|
||||
using (VideoOut.RenderPhaseProfile.MeasureDetail(VideoOut.RenderPhaseProfile.Phase.GpuCompletionWait))
|
||||
{
|
||||
Require(vk.WaitSemaphores(_device.Device, &waitInfo, ulong.MaxValue), "vkWaitSemaphores(readback)");
|
||||
}
|
||||
|
||||
VideoOut.GpuWaitSiteProfile.End(waitStart);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ internal static unsafe class VulkanSynchronization
|
|||
uint imageMemoryBarrierCount,
|
||||
ImageMemoryBarrier2* imageMemoryBarriers)
|
||||
{
|
||||
if (GpuSyncProfile.Enabled)
|
||||
{
|
||||
GpuSyncProfile.CountBarrier(sourceStages, destinationStages, memoryBarrierCount, bufferMemoryBarrierCount, imageMemoryBarrierCount);
|
||||
}
|
||||
|
||||
var sourceStages2 = (PipelineStageFlags2)(ulong)sourceStages;
|
||||
var destinationStages2 = (PipelineStageFlags2)(ulong)destinationStages;
|
||||
for (var index = 0u; index < memoryBarrierCount; index++)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue