mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-18 23:41:30 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b4d93cb71 | |||
| a7dae8dd7d | |||
| dc3f719fa1 |
+1
-3
@@ -42,6 +42,4 @@ ehthumbs.db
|
|||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
clean_test/
|
|
||||||
.debug/
|
|
||||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<SharpEmuVersion>0.0.3-release.3</SharpEmuVersion>
|
<SharpEmuVersion>0.0.3-hotfix-2</SharpEmuVersion>
|
||||||
<Version>$(SharpEmuVersion)</Version>
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
+15
-19
@@ -47,28 +47,24 @@ built against `ffmpeg-core` specifically.
|
|||||||
|
|
||||||
## Supplying the FFmpeg libraries
|
## Supplying the FFmpeg libraries
|
||||||
|
|
||||||
Both `dotnet build` and `dotnet publish` fetch a prebuilt release of
|
`dotnet publish` fetches a prebuilt release of `github.com/sharpemu/ffmpeg-core`
|
||||||
`github.com/sharpemu/ffmpeg-core` (the tag is pinned in
|
(the tag is pinned in `SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to
|
||||||
`SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to the `FFmpeg.AutoGen`
|
the `FFmpeg.AutoGen` package version in `Directory.Packages.props` -- both
|
||||||
package version in `Directory.Packages.props` -- both need to agree on the
|
need to agree on the same FFmpeg ABI) and copies its dynamically linked
|
||||||
same FFmpeg ABI) and copy its dynamically linked libraries into a `plugins`
|
libraries into a `plugins` folder next to the published executable. No C
|
||||||
folder next to the resulting executable (`artifacts/bin/...` for build,
|
toolchain is required to build SharpEmu; publishing just downloads a zip.
|
||||||
`artifacts/publish/...` for publish). No C toolchain is required to build
|
`plugins` is a loose, unpacked folder rather than something embedded in the
|
||||||
SharpEmu; both just download a zip once (cached under
|
single-file bundle, so the OS loader can resolve the libraries' own
|
||||||
`$(BaseIntermediateOutputPath)ffmpeg-runtime/`, so later builds/publishes
|
inter-dependencies (`avcodec` depends on `avutil`, etc.) itself.
|
||||||
reuse it instead of re-fetching). `plugins` is a loose, unpacked folder
|
|
||||||
rather than something embedded in the single-file bundle, so the OS loader
|
|
||||||
can resolve the libraries' own inter-dependencies (`avcodec` depends on
|
|
||||||
`avutil`, etc.) itself.
|
|
||||||
|
|
||||||
A plain `dotnet build`/`dotnet publish` with no `-r` still works: it defaults
|
A plain `dotnet publish` with no `-r` still works: it defaults to the host
|
||||||
to the host machine's own RID (see `Directory.Build.props`), so it fetches
|
machine's own RID (see `Directory.Build.props`), so it fetches the matching
|
||||||
the matching `ffmpeg-core` archive and populates `plugins` without any extra
|
`ffmpeg-core` archive and populates `plugins` without any extra flags.
|
||||||
flags. Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
|
Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
|
||||||
Windows) still overrides that default normally.
|
Windows) still overrides that default normally.
|
||||||
|
|
||||||
To use a different set of FFmpeg libraries, drop them into the build or
|
To use a different set of FFmpeg libraries, drop them into the published
|
||||||
published `plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
`plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
||||||
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
||||||
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
||||||
folder and does not otherwise care where the files came from.
|
folder and does not otherwise care where the files came from.
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="FetchFfmpegRuntime"
|
<Target Name="FetchFfmpegRuntime"
|
||||||
BeforeTargets="Publish;Build"
|
BeforeTargets="Publish"
|
||||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||||
<DownloadFile
|
<DownloadFile
|
||||||
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||||
@@ -161,23 +161,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
SkipUnchangedFiles="true" />
|
SkipUnchangedFiles="true" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<!-- Mirrors PublishFfmpegRuntime for plain `dotnet build`: devs running
|
|
||||||
straight out of the build output directory (no publish step) still
|
|
||||||
need the FFmpeg plugins present, otherwise AvPlayer/Bink video probing
|
|
||||||
throws NotSupportedException the first time a guest opens a movie. -->
|
|
||||||
<Target Name="BuildFfmpegRuntime"
|
|
||||||
AfterTargets="Build"
|
|
||||||
DependsOnTargets="FetchFfmpegRuntime"
|
|
||||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
|
||||||
<ItemGroup>
|
|
||||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
|
||||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
|
||||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
|
||||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
|
||||||
DestinationFolder="$(OutDir)$(NativeLibraryFolderName)"
|
|
||||||
SkipUnchangedFiles="true" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1453,7 +1453,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
RescanTlsPatternsIfExecutable(committedBase, committedSize + 4096uL, commitProtect);
|
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1513,7 +1512,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
RescanTlsPatternsIfExecutable(committedBase, committedSize + 4096uL, commitProtect);
|
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1615,18 +1613,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-scans a just-committed window for FS:[0] TLS loads; skips non-executable commits.
|
|
||||||
private unsafe void RescanTlsPatternsIfExecutable(ulong committedBase, ulong committedSize, uint commitProtect)
|
|
||||||
{
|
|
||||||
const uint executableProtectionMask = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
|
||||||
if ((commitProtect & executableProtectionMask) == 0 || committedSize == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PatchTlsPatternsInRange(committedBase, committedBase + committedSize, announce: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldTraceLazyCommit(int traceIndex)
|
private static bool ShouldTraceLazyCommit(int traceIndex)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -3145,33 +3145,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// Large Gen5 executables can keep valid code well past the first 32 MiB.
|
// Large Gen5 executables can keep valid code well past the first 32 MiB.
|
||||||
// Astro Bot, for example, has an FS:[0] TLS load near +0x70A0000.
|
// Astro Bot, for example, has an FS:[0] TLS load near +0x70A0000.
|
||||||
const ulong MaxScanBytes = 134217728uL;
|
const ulong MaxScanBytes = 134217728uL;
|
||||||
|
ulong num = _entryPoint;
|
||||||
// _entryPoint can be a separate bootstrap allocation, not the main module —
|
ulong num2 = num + MaxScanBytes;
|
||||||
// always also scan the standard PS5/PS4 image base.
|
|
||||||
const ulong Ps5MainImageBase = 0x0000000800000000UL;
|
|
||||||
const ulong Ps4MainImageBase = 0x0000000000400000UL;
|
|
||||||
ulong scanStart = _entryPoint;
|
|
||||||
if (VirtualQuery((void*)_entryPoint, out var entryRegion, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0 &&
|
|
||||||
entryRegion.AllocationBase != 0 &&
|
|
||||||
entryRegion.AllocationBase <= _entryPoint)
|
|
||||||
{
|
|
||||||
scanStart = entryRegion.AllocationBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
PatchTlsPatternsInRange(scanStart, scanStart + MaxScanBytes, announce: true);
|
|
||||||
|
|
||||||
// Scan both windows unconditionally; overlap is safe, patched bytes just stop matching.
|
|
||||||
var mainImageBase = _entryPoint >= Ps5MainImageBase ? Ps5MainImageBase : Ps4MainImageBase;
|
|
||||||
if (mainImageBase < scanStart)
|
|
||||||
{
|
|
||||||
PatchTlsPatternsInRange(mainImageBase, mainImageBase + MaxScanBytes, announce: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void PatchTlsPatternsInRange(ulong rangeStart, ulong rangeEnd, bool announce)
|
|
||||||
{
|
|
||||||
ulong num = rangeStart;
|
|
||||||
ulong num2 = rangeEnd;
|
|
||||||
int num3 = 0;
|
int num3 = 0;
|
||||||
int num4 = 0;
|
int num4 = 0;
|
||||||
int num9 = 0;
|
int num9 = 0;
|
||||||
@@ -3220,11 +3195,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
num = num6 > num ? num6 : num + 4096uL;
|
num = num6 > num ? num6 : num + 4096uL;
|
||||||
}
|
}
|
||||||
if (announce || num3 + num4 + num9 + sse4aPatchCount > 0)
|
Console.Error.WriteLine($"[LOADER][INFO] Patched {num3} TLS loads, {num9} TLS stores, {num4} stack-canary accesses, {sse4aPatchCount} SSE4a EXTRQ blends");
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Patched {num3} TLS loads, {num9} TLS stores, {num4} stack-canary accesses, {sse4aPatchCount} SSE4a EXTRQ blends" +
|
|
||||||
(announce ? string.Empty : $" (lazy-commit rescan 0x{rangeStart:X16}-0x{rangeEnd:X16})"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
|
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
|
||||||
|
|||||||
@@ -149,7 +149,6 @@
|
|||||||
"Options.Env.Group.General": "عام",
|
"Options.Env.Group.General": "عام",
|
||||||
"Options.Env.RenderDoc.Desc": "يحمّل واجهة RenderDoc داخل التطبيق حتى يمكن التقاط الإطارات من داخل المحاكي.\nاضغط F10 أثناء تشغيل اللعبة لالتقاط إطار واحد؛ تُحفظ اللقطات في user/logs/capture_logs/<TITLE_ID>.\nيتطلب تثبيت RenderDoc. يبطئ وحدة معالجة الرسوميات ويسبب تعليق بعض الألعاب، لذا اتركه معطلاً ما لم تكن تصحح الأخطاء.",
|
"Options.Env.RenderDoc.Desc": "يحمّل واجهة RenderDoc داخل التطبيق حتى يمكن التقاط الإطارات من داخل المحاكي.\nاضغط F10 أثناء تشغيل اللعبة لالتقاط إطار واحد؛ تُحفظ اللقطات في user/logs/capture_logs/<TITLE_ID>.\nيتطلب تثبيت RenderDoc. يبطئ وحدة معالجة الرسوميات ويسبب تعليق بعض الألعاب، لذا اتركه معطلاً ما لم تكن تصحح الأخطاء.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "تسليم مقدّمات المخازن المؤقتة لأوامر GPU حتى عندما لا تلتقطها قائمة الانتظار المستهدفة أبدًا.\nاتركه مغلقًا عادة. شغّله للألعاب التي تتجمد أثناء انتظار حاجز GPU لا يُشار إليه أبدًا.",
|
|
||||||
"Common.Save": "حفظ",
|
"Common.Save": "حفظ",
|
||||||
"Common.Cancel": "إلغاء",
|
"Common.Cancel": "إلغاء",
|
||||||
"Options.About": "حول",
|
"Options.About": "حول",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"Options.Env.Group.General": "Geral",
|
"Options.Env.Group.General": "Geral",
|
||||||
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames de dentro do emulador.\nPressione F10 enquanto o jogo roda para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nExige o RenderDoc instalado. Deixa a GPU mais lenta e trava alguns títulos, então mantenha desativado a menos que esteja depurando.",
|
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames de dentro do emulador.\nPressione F10 enquanto o jogo roda para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nExige o RenderDoc instalado. Deixa a GPU mais lenta e trava alguns títulos, então mantenha desativado a menos que esteja depurando.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixe desativado normalmente. Ative para títulos cujas superfícies desenhadas pela CPU nunca chegam à tela.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixe desativado normalmente. Ative para títulos cujas superfícies desenhadas pela CPU nunca chegam à tela.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega os preâmbulos do buffer de comandos da GPU mesmo quando a fila de destino nunca os recolhe.\nDeixe desativado normalmente. Ative para títulos que travam esperando por uma fence de GPU que nunca sinaliza.",
|
|
||||||
"Options.Section.Emulation": "EMULAÇÃO",
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
"Options.Section.Logging": "LOGS",
|
"Options.Section.Logging": "LOGS",
|
||||||
"Options.Section.Launcher": "INICIALIZADOR",
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
|
|||||||
@@ -149,7 +149,6 @@
|
|||||||
"Options.Env.Group.General": "Allgemein",
|
"Options.Env.Group.General": "Allgemein",
|
||||||
"Options.Env.RenderDoc.Desc": "Lädt die RenderDoc-In-Application-API, damit Frames aus dem Emulator heraus aufgezeichnet werden können.\nDrücke F10 während das Spiel läuft, um ein Frame aufzuzeichnen; Aufzeichnungen landen in user/logs/capture_logs/<TITLE_ID>.\nErfordert eine RenderDoc-Installation. Verlangsamt die GPU und lässt manche Titel hängen, lass es also aus, wenn du nicht debuggst.",
|
"Options.Env.RenderDoc.Desc": "Lädt die RenderDoc-In-Application-API, damit Frames aus dem Emulator heraus aufgezeichnet werden können.\nDrücke F10 während das Spiel läuft, um ein Frame aufzuzeichnen; Aufzeichnungen landen in user/logs/capture_logs/<TITLE_ID>.\nErfordert eine RenderDoc-Installation. Verlangsamt die GPU und lässt manche Titel hängen, lass es also aus, wenn du nicht debuggst.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.",
|
"Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Liefert GPU-Befehlspuffer-Präambeln auch dann aus, wenn die Zielwarteschlange sie nie abholt.\nNormalerweise aus lassen. Für Titel aktivieren, die beim Warten auf einen GPU-Fence hängen bleiben, der nie signalisiert.",
|
|
||||||
"Common.Save": "Speichern",
|
"Common.Save": "Speichern",
|
||||||
"Common.Cancel": "Abbrechen",
|
"Common.Cancel": "Abbrechen",
|
||||||
"Options.About": "Über",
|
"Options.About": "Über",
|
||||||
|
|||||||
@@ -149,7 +149,6 @@
|
|||||||
"Options.Env.Group.General": "Generelt",
|
"Options.Env.Group.General": "Generelt",
|
||||||
"Options.Env.RenderDoc.Desc": "Indlæser RenderDocs in-application-API, så frames kan optages inde fra emulatoren.\nTryk på F10 mens spillet kører for at optage ét frame; optagelser havner i user/logs/capture_logs/<TITLE_ID>.\nKræver at RenderDoc er installeret. Gør GPU'en langsommere og får nogle titler til at hænge, så lad den være slået fra, medmindre du fejlfinder.",
|
"Options.Env.RenderDoc.Desc": "Indlæser RenderDocs in-application-API, så frames kan optages inde fra emulatoren.\nTryk på F10 mens spillet kører for at optage ét frame; optagelser havner i user/logs/capture_logs/<TITLE_ID>.\nKræver at RenderDoc er installeret. Gør GPU'en langsommere og får nogle titler til at hænge, så lad den være slået fra, medmindre du fejlfinder.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Leverer GPU-kommandobuffer-præambler, selv når målkøen aldrig henter dem.\nLad den være slået fra normalt. Slå til for titler, der hænger og venter på en GPU-fence, der aldrig signalerer.",
|
|
||||||
"Common.Save": "Gem",
|
"Common.Save": "Gem",
|
||||||
"Common.Cancel": "Annuller",
|
"Common.Cancel": "Annuller",
|
||||||
"Options.About": "Om",
|
"Options.About": "Om",
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
"Options.Env.Group.General": "General",
|
"Options.Env.Group.General": "General",
|
||||||
"Options.Env.RenderDoc.Desc": "Load the RenderDoc in-application API so frames can be captured from inside the emulator.\nPress F10 while the game runs to capture one frame; captures land in user/logs/capture_logs/<TITLE_ID>.\nRequires RenderDoc to be installed. Slows the GPU down and hangs some titles, so leave it off unless you are debugging.",
|
"Options.Env.RenderDoc.Desc": "Load the RenderDoc in-application API so frames can be captured from inside the emulator.\nPress F10 while the game runs to capture one frame; captures land in user/logs/capture_logs/<TITLE_ID>.\nRequires RenderDoc to be installed. Slows the GPU down and hangs some titles, so leave it off unless you are debugging.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Deliver GPU command-buffer preambles even when their target queue never picks them up.\nLeave off normally. Turn on for titles that hang waiting on a GPU fence that never signals.",
|
|
||||||
"Options.DefaultProfile.Label": "Default profile name",
|
"Options.DefaultProfile.Label": "Default profile name",
|
||||||
"Options.DefaultProfile.Desc": "Name used when a game asks for text input. Defaults to Sharp.",
|
"Options.DefaultProfile.Desc": "Name used when a game asks for text input. Defaults to Sharp.",
|
||||||
"Options.Section.Emulation": "EMULATION",
|
"Options.Section.Emulation": "EMULATION",
|
||||||
|
|||||||
@@ -159,7 +159,6 @@
|
|||||||
"Options.Env.Group.General": "General",
|
"Options.Env.Group.General": "General",
|
||||||
"Options.Env.RenderDoc.Desc": "Carga la API in-application de RenderDoc para poder capturar fotogramas desde el emulador.\nPulsa F10 mientras el juego se ejecuta para capturar un fotograma; las capturas se guardan en user/logs/capture_logs/<TITLE_ID>.\nRequiere tener RenderDoc instalado. Ralentiza la GPU y bloquea algunos títulos, así que déjalo desactivado salvo que estés depurando.",
|
"Options.Env.RenderDoc.Desc": "Carga la API in-application de RenderDoc para poder capturar fotogramas desde el emulador.\nPulsa F10 mientras el juego se ejecuta para capturar un fotograma; las capturas se guardan en user/logs/capture_logs/<TITLE_ID>.\nRequiere tener RenderDoc instalado. Ralentiza la GPU y bloquea algunos títulos, así que déjalo desactivado salvo que estés depurando.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega los preámbulos del búfer de comandos de la GPU incluso cuando la cola de destino nunca los recoge.\nDejar desactivado normalmente. Activar en títulos que se cuelgan esperando una fence de GPU que nunca se señaliza.",
|
|
||||||
"Common.Save": "Guardar",
|
"Common.Save": "Guardar",
|
||||||
"Common.Cancel": "Cancelar",
|
"Common.Cancel": "Cancelar",
|
||||||
"Updater.Auto.Label": "Buscar actualizaciones al iniciar",
|
"Updater.Auto.Label": "Buscar actualizaciones al iniciar",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"Options.Env.Group.General": "Général",
|
"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 F10 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.RenderDoc.Desc": "Charge l'API in-application de RenderDoc afin de capturer des images depuis l'émulateur.\nAppuyez sur F10 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.GuestImageCpuSync.Desc": "Recharger les surfaces invité que le code CPU du jeu réécrit lui-même.\nLaisser désactivé normalement. Activer pour les titres dont les surfaces dessinées par le CPU n'atteignent jamais l'écran.\nCoûte des performances et provoque des régressions sur certains titres, comme GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Recharger les surfaces invité que le code CPU du jeu réécrit lui-même.\nLaisser désactivé normalement. Activer pour les titres dont les surfaces dessinées par le CPU n'atteignent jamais l'écran.\nCoûte des performances et provoque des régressions sur certains titres, comme GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Soumettre les préambules de tampon de commandes GPU même quand leur file cible ne les récupère jamais.\nLaisser désactivé normalement. Activer pour les titres qui se figent en attendant une fence GPU qui ne se déclenche jamais.",
|
|
||||||
"Options.Section.Emulation": "ÉMULATION",
|
"Options.Section.Emulation": "ÉMULATION",
|
||||||
"Options.Section.Logging": "JOURNALISATION",
|
"Options.Section.Logging": "JOURNALISATION",
|
||||||
"Options.Section.Launcher": "LANCEUR",
|
"Options.Section.Launcher": "LANCEUR",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"Options.Env.Group.General": "Általános",
|
"Options.Env.Group.General": "Általános",
|
||||||
"Options.Env.RenderDoc.Desc": "Betölti a RenderDoc alkalmazáson belüli API-ját, így képkockák rögzíthetők az emulátorból.\nNyomd meg az F10-et futó játék közben egy képkocka rögzítéséhez; a felvételek a user/logs/capture_logs/<TITLE_ID> mappába kerülnek.\nTelepített RenderDocot igényel. Lassítja a GPU-t és egyes játékokat lefagyaszt, ezért hagyd kikapcsolva, hacsak nem hibakeresel.",
|
"Options.Env.RenderDoc.Desc": "Betölti a RenderDoc alkalmazáson belüli API-ját, így képkockák rögzíthetők az emulátorból.\nNyomd meg az F10-et futó játék közben egy képkocka rögzítéséhez; a felvételek a user/logs/capture_logs/<TITLE_ID> mappába kerülnek.\nTelepített RenderDocot igényel. Lassítja a GPU-t és egyes játékokat lefagyaszt, ezért hagyd kikapcsolva, hacsak nem hibakeresel.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Újratölti azokat a vendégfelületeket, amelyeket a játék saját CPU-kódja ír felül.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek CPU-val rajzolt felületei sosem jutnak ki a képernyőre.\nTeljesítménybe kerül, és egyes címeknél, például a GTA V-nél regressziót okoz.",
|
"Options.Env.GuestImageCpuSync.Desc": "Újratölti azokat a vendégfelületeket, amelyeket a játék saját CPU-kódja ír felül.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek CPU-val rajzolt felületei sosem jutnak ki a képernyőre.\nTeljesítménybe kerül, és egyes címeknél, például a GTA V-nél regressziót okoz.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "GPU parancspuffer-előtagokat is kézbesít, ha a célsor sosem veszi fel őket.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek egy sosem jelző GPU-fence-re várva lefagynak.",
|
|
||||||
"Options.Section.Emulation": "EMULÁCIÓ",
|
"Options.Section.Emulation": "EMULÁCIÓ",
|
||||||
"Options.Section.Logging": "LOGOLÁS",
|
"Options.Section.Logging": "LOGOLÁS",
|
||||||
"Options.Section.Launcher": "INDITÓ",
|
"Options.Section.Launcher": "INDITÓ",
|
||||||
|
|||||||
@@ -154,7 +154,6 @@
|
|||||||
"Options.Env.Group.General": "Generale",
|
"Options.Env.Group.General": "Generale",
|
||||||
"Options.Env.RenderDoc.Desc": "Carica l'API in-application di RenderDoc per catturare i frame dall'interno dell'emulatore.\nPremi F10 mentre il gioco è in esecuzione per catturare un frame; le catture finiscono in user/logs/capture_logs/<TITLE_ID>.\nRichiede RenderDoc installato. Rallenta la GPU e blocca alcuni titoli, quindi lascialo disattivato se non stai facendo debug.",
|
"Options.Env.RenderDoc.Desc": "Carica l'API in-application di RenderDoc per catturare i frame dall'interno dell'emulatore.\nPremi F10 mentre il gioco è in esecuzione per catturare un frame; le catture finiscono in user/logs/capture_logs/<TITLE_ID>.\nRichiede RenderDoc installato. Rallenta la GPU e blocca alcuni titoli, quindi lascialo disattivato se non stai facendo debug.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Consegna i preamboli del buffer di comandi GPU anche quando la coda di destinazione non li preleva mai.\nLasciare disattivato normalmente. Attivare per i titoli che si bloccano in attesa di una fence GPU che non segnala mai.",
|
|
||||||
"Common.Save": "Salva",
|
"Common.Save": "Salva",
|
||||||
"Common.Cancel": "Annulla",
|
"Common.Cancel": "Annulla",
|
||||||
"Options.About": "Informazioni",
|
"Options.About": "Informazioni",
|
||||||
|
|||||||
@@ -149,7 +149,6 @@
|
|||||||
"Options.Env.Group.General": "一般",
|
"Options.Env.Group.General": "一般",
|
||||||
"Options.Env.RenderDoc.Desc": "RenderDoc のアプリ内 API を読み込み、エミュレーター内からフレームをキャプチャできるようにします。\nゲーム実行中に F10 を押すと 1 フレームをキャプチャします。保存先は user/logs/capture_logs/<TITLE_ID> です。\nRenderDoc のインストールが必要です。GPU が遅くなり一部のタイトルはハングするため、デバッグ時以外はオフのままにしてください。",
|
"Options.Env.RenderDoc.Desc": "RenderDoc のアプリ内 API を読み込み、エミュレーター内からフレームをキャプチャできるようにします。\nゲーム実行中に F10 を押すと 1 フレームをキャプチャします。保存先は user/logs/capture_logs/<TITLE_ID> です。\nRenderDoc のインストールが必要です。GPU が遅くなり一部のタイトルはハングするため、デバッグ時以外はオフのままにしてください。",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。",
|
"Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "対象キューが受け取らない場合でも GPU コマンドバッファのプリアンブルを配信します。\n通常はオフのままにしてください。決してシグナルされない GPU フェンスを待ってハングするタイトルで有効にします。",
|
|
||||||
"Common.Save": "保存",
|
"Common.Save": "保存",
|
||||||
"Common.Cancel": "キャンセル",
|
"Common.Cancel": "キャンセル",
|
||||||
"Options.About": "情報",
|
"Options.About": "情報",
|
||||||
|
|||||||
@@ -149,7 +149,6 @@
|
|||||||
"Options.Env.Group.General": "일반",
|
"Options.Env.Group.General": "일반",
|
||||||
"Options.Env.RenderDoc.Desc": "RenderDoc의 인앱 API를 로드하여 에뮬레이터 내부에서 프레임을 캡처할 수 있게 합니다.\n게임 실행 중 F10을 누르면 한 프레임을 캡처하며, 캡처 파일은 user/logs/capture_logs/<TITLE_ID>에 저장됩니다.\nRenderDoc이 설치되어 있어야 합니다. GPU 속도가 느려지고 일부 타이틀은 멈추므로 디버깅할 때가 아니면 꺼 두세요.",
|
"Options.Env.RenderDoc.Desc": "RenderDoc의 인앱 API를 로드하여 에뮬레이터 내부에서 프레임을 캡처할 수 있게 합니다.\n게임 실행 중 F10을 누르면 한 프레임을 캡처하며, 캡처 파일은 user/logs/capture_logs/<TITLE_ID>에 저장됩니다.\nRenderDoc이 설치되어 있어야 합니다. GPU 속도가 느려지고 일부 타이틀은 멈추므로 디버깅할 때가 아니면 꺼 두세요.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.",
|
"Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "대상 큐가 절대 가져가지 않아도 GPU 명령 버퍼 프리앰블을 전달합니다.\n평소에는 꺼 두세요. 절대 신호를 보내지 않는 GPU 펜스를 기다리며 멈추는 타이틀에서 켜세요.",
|
|
||||||
"Common.Save": "저장",
|
"Common.Save": "저장",
|
||||||
"Common.Cancel": "취소",
|
"Common.Cancel": "취소",
|
||||||
"Options.About": "정보",
|
"Options.About": "정보",
|
||||||
|
|||||||
@@ -149,7 +149,6 @@
|
|||||||
"Options.Env.Group.General": "Algemeen",
|
"Options.Env.Group.General": "Algemeen",
|
||||||
"Options.Env.RenderDoc.Desc": "Laadt de RenderDoc in-application-API zodat frames vanuit de emulator kunnen worden vastgelegd.\nDruk op F10 terwijl het spel draait om één frame vast te leggen; opnamen komen in user/logs/capture_logs/<TITLE_ID>.\nVereist een geïnstalleerde RenderDoc. Vertraagt de GPU en laat sommige games vastlopen, dus laat dit uit tenzij je aan het debuggen bent.",
|
"Options.Env.RenderDoc.Desc": "Laadt de RenderDoc in-application-API zodat frames vanuit de emulator kunnen worden vastgelegd.\nDruk op F10 terwijl het spel draait om één frame vast te leggen; opnamen komen in user/logs/capture_logs/<TITLE_ID>.\nVereist een geïnstalleerde RenderDoc. Vertraagt de GPU en laat sommige games vastlopen, dus laat dit uit tenzij je aan het debuggen bent.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Levert GPU-commandobuffer-preambules af, zelfs wanneer de doelwachtrij ze nooit ophaalt.\nNormaal uit laten. Inschakelen voor titels die vasthangen in afwachting van een GPU-fence die nooit signaleert.",
|
|
||||||
"Common.Save": "Opslaan",
|
"Common.Save": "Opslaan",
|
||||||
"Common.Cancel": "Annuleren",
|
"Common.Cancel": "Annuleren",
|
||||||
"Options.About": "Over",
|
"Options.About": "Over",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"Options.Env.Group.General": "Geral",
|
"Options.Env.Group.General": "Geral",
|
||||||
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames a partir do emulador.\nPrime F10 enquanto o jogo corre para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nRequer o RenderDoc instalado. Torna a GPU mais lenta e bloqueia alguns títulos, por isso deixa desativado a menos que estejas a depurar.",
|
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames a partir do emulador.\nPrime F10 enquanto o jogo corre para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nRequer o RenderDoc instalado. Torna a GPU mais lenta e bloqueia alguns títulos, por isso deixa desativado a menos que estejas a depurar.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixar desativado normalmente. Ativar para títulos cujas superfícies desenhadas pela CPU nunca chegam ao ecrã.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixar desativado normalmente. Ativar para títulos cujas superfícies desenhadas pela CPU nunca chegam ao ecrã.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega os preâmbulos do buffer de comandos da GPU mesmo quando a fila de destino nunca os recolhe.\nDeixar desativado normalmente. Ativar para títulos que bloqueiam à espera de uma fence de GPU que nunca sinaliza.",
|
|
||||||
"Options.Section.Emulation": "EMULAÇÃO",
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
"Options.Section.Logging": "REGISTOS",
|
"Options.Section.Logging": "REGISTOS",
|
||||||
"Options.Section.Launcher": "LANÇADOR",
|
"Options.Section.Launcher": "LANÇADOR",
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"Options.Env.Group.General": "Общие",
|
"Options.Env.Group.General": "Общие",
|
||||||
"Options.Env.RenderDoc.Desc": "Загружает внутренний API RenderDoc, чтобы кадры можно было захватывать из эмулятора.\nНажмите F10 во время игры, чтобы захватить один кадр; захваты сохраняются в user/logs/capture_logs/<TITLE_ID>.\nТребует установленного RenderDoc. Замедляет GPU и подвешивает некоторые игры, поэтому оставьте выключенным, если не занимаетесь отладкой.",
|
"Options.Env.RenderDoc.Desc": "Загружает внутренний API RenderDoc, чтобы кадры можно было захватывать из эмулятора.\nНажмите F10 во время игры, чтобы захватить один кадр; захваты сохраняются в user/logs/capture_logs/<TITLE_ID>.\nТребует установленного RenderDoc. Замедляет GPU и подвешивает некоторые игры, поэтому оставьте выключенным, если не занимаетесь отладкой.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Повторно загружать гостевые поверхности, которые переписывает собственный код ЦП игры.\nОбычно оставляйте выключенным. Включайте для игр, чьи отрисованные ЦП поверхности не попадают на экран.\nСнижает производительность и вызывает регрессии в некоторых играх, например в GTA V.",
|
"Options.Env.GuestImageCpuSync.Desc": "Повторно загружать гостевые поверхности, которые переписывает собственный код ЦП игры.\nОбычно оставляйте выключенным. Включайте для игр, чьи отрисованные ЦП поверхности не попадают на экран.\nСнижает производительность и вызывает регрессии в некоторых играх, например в GTA V.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Доставляет преамбулы буфера команд GPU, даже если целевая очередь их так и не забирает.\nОбычно оставляйте выключенным. Включайте для игр, которые зависают в ожидании GPU-fence, который никогда не срабатывает.",
|
|
||||||
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||||
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||||
"Options.Section.Launcher": "ЛАУНЧЕР",
|
"Options.Section.Launcher": "ЛАУНЧЕР",
|
||||||
|
|||||||
@@ -183,7 +183,6 @@
|
|||||||
"Options.Env.Group.General": "Genel",
|
"Options.Env.Group.General": "Genel",
|
||||||
"Options.Env.RenderDoc.Desc": "RenderDoc uygulama içi API'sini yükler, böylece kareler emülatörün içinden yakalanabilir.\nOyun çalışırken F10'a basınca bir kare yakalanır; kayıtlar user/logs/capture_logs/<TITLE_ID> altına düşer.\nRenderDoc'un kurulu olmasını gerektirir. GPU'yu yavaşlatır ve bazı oyunları kilitler, hata ayıklamıyorsanız kapalı bırakın.",
|
"Options.Env.RenderDoc.Desc": "RenderDoc uygulama içi API'sini yükler, böylece kareler emülatörün içinden yakalanabilir.\nOyun çalışırken F10'a basınca bir kare yakalanır; kayıtlar user/logs/capture_logs/<TITLE_ID> altına düşer.\nRenderDoc'un kurulu olmasını gerektirir. GPU'yu yavaşlatır ve bazı oyunları kilitler, hata ayıklamıyorsanız kapalı bırakın.",
|
||||||
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
|
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
|
||||||
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Hedef kuyruk onları asla almasa bile GPU komut arabelleği önsözlerini teslim eder.\nNormalde kapalı bırakın. Asla sinyal vermeyen bir GPU fence'ini bekleyerek takılan oyunlarda açın.",
|
|
||||||
"Options.DefaultProfile.Label": "Varsayilan profil adi",
|
"Options.DefaultProfile.Label": "Varsayilan profil adi",
|
||||||
"Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.",
|
"Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.",
|
||||||
"Common.Save": "Kaydet",
|
"Common.Save": "Kaydet",
|
||||||
|
|||||||
@@ -483,7 +483,6 @@ public partial class MainWindow
|
|||||||
("SHARPEMU_LOG_IO", GameEnvLogIoToggle),
|
("SHARPEMU_LOG_IO", GameEnvLogIoToggle),
|
||||||
("SHARPEMU_LOG_NP", GameEnvLogNpToggle),
|
("SHARPEMU_LOG_NP", GameEnvLogNpToggle),
|
||||||
("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle),
|
("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle),
|
||||||
("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES", GameEnvForceSubmitOrphanPreamblesToggle),
|
|
||||||
("SHARPEMU_RENDERDOC", GameEnvRenderDocToggle),
|
("SHARPEMU_RENDERDOC", GameEnvRenderDocToggle),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -977,14 +977,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ToggleSwitch x:Name="GameEnvGuestImageCpuSyncToggle"
|
<ToggleSwitch x:Name="GameEnvGuestImageCpuSyncToggle"
|
||||||
Classes="optionToggle" />
|
Classes="optionToggle" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow Classes="optionRow"
|
|
||||||
Label="SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES"
|
|
||||||
Description="{Binding [Options.Env.ForceSubmitOrphanPreambles.Desc],
|
|
||||||
Source={x:Static local:Localization.Instance},
|
|
||||||
x:CompileBindings=False}">
|
|
||||||
<ToggleSwitch x:Name="GameEnvForceSubmitOrphanPreamblesToggle"
|
|
||||||
Classes="optionToggle" />
|
|
||||||
</local:SettingRow>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -1501,11 +1493,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Description="{Binding [Options.Env.GuestImageCpuSync.Desc], Source={x:Static local:Localization.Instance}}">
|
Description="{Binding [Options.Env.GuestImageCpuSync.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvGuestImageCpuSyncToggle" Classes="optionToggle" />
|
<ToggleSwitch x:Name="EnvGuestImageCpuSyncToggle" Classes="optionToggle" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvForceSubmitOrphanPreamblesRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES"
|
|
||||||
Description="{Binding [Options.Env.ForceSubmitOrphanPreambles.Desc], Source={x:Static local:Localization.Instance}}">
|
|
||||||
<ToggleSwitch x:Name="EnvForceSubmitOrphanPreamblesToggle" Classes="optionToggle" />
|
|
||||||
</local:SettingRow>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -290,10 +290,6 @@ public partial class MainWindow : Window
|
|||||||
SetEnvironmentToggle(
|
SetEnvironmentToggle(
|
||||||
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
|
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
|
||||||
EnvGuestImageCpuSyncToggle.IsChecked == true);
|
EnvGuestImageCpuSyncToggle.IsChecked == true);
|
||||||
EnvForceSubmitOrphanPreamblesToggle.IsCheckedChanged += (_, _) =>
|
|
||||||
SetEnvironmentToggle(
|
|
||||||
"SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES",
|
|
||||||
EnvForceSubmitOrphanPreamblesToggle.IsChecked == true);
|
|
||||||
DefaultProfileBox.TextChanged += (_, _) =>
|
DefaultProfileBox.TextChanged += (_, _) =>
|
||||||
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
|
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
|
||||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||||
@@ -1217,8 +1213,6 @@ public partial class MainWindow : Window
|
|||||||
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||||
EnvGuestImageCpuSyncToggle.IsChecked =
|
EnvGuestImageCpuSyncToggle.IsChecked =
|
||||||
_settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC");
|
_settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC");
|
||||||
EnvForceSubmitOrphanPreamblesToggle.IsChecked =
|
|
||||||
_settings.EnvironmentToggles.Contains("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES");
|
|
||||||
EnvRenderDocToggle.IsChecked =
|
EnvRenderDocToggle.IsChecked =
|
||||||
_settings.EnvironmentToggles.Contains("SHARPEMU_RENDERDOC");
|
_settings.EnvironmentToggles.Contains("SHARPEMU_RENDERDOC");
|
||||||
DefaultProfileBox.Text = _settings.DefaultProfile;
|
DefaultProfileBox.Text = _settings.DefaultProfile;
|
||||||
|
|||||||
@@ -99,50 +99,6 @@ public static class AcmExports
|
|||||||
return CompleteBatchStart(ctx, context, infoCount, errorAddress, batchAddress);
|
return CompleteBatchStart(ctx, context, infoCount, errorAddress, batchAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DSP batch submission and synchronization. The emulator runs no ACM DSP
|
|
||||||
// jobs (FFT/panner/reverb output stays silent), but Scream's workers trap
|
|
||||||
// with int 0x41/0x42 asserts whenever a submission call reports failure,
|
|
||||||
// so the whole batch surface must report success.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "WeZOIm8+8WI",
|
|
||||||
ExportName = "sceAcmBatchInitialize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAcm")]
|
|
||||||
public static int AcmBatchInitialize(CpuContext ctx) =>
|
|
||||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Mk1xvQXIdkk",
|
|
||||||
ExportName = "sceAcmBatchInitializeLite",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAcm")]
|
|
||||||
public static int AcmBatchInitializeLite(CpuContext ctx) =>
|
|
||||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "A5NXCXK5Gfc",
|
|
||||||
ExportName = "sceAcmBatchStart",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAcm")]
|
|
||||||
public static int AcmBatchStart(CpuContext ctx) =>
|
|
||||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "S3BPrjCfZ90",
|
|
||||||
ExportName = "sceAcmBatchStartMultiple",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAcm")]
|
|
||||||
public static int AcmBatchStartMultiple(CpuContext ctx) =>
|
|
||||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "uqDIauipRbo",
|
|
||||||
ExportName = "sceAcmBatchProcess",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAcm")]
|
|
||||||
public static int AcmBatchProcess(CpuContext ctx) =>
|
|
||||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "RLN3gRlXJLE",
|
Nid = "RLN3gRlXJLE",
|
||||||
ExportName = "sceAcmBatchWait",
|
ExportName = "sceAcmBatchWait",
|
||||||
|
|||||||
+75
-1772
File diff suppressed because it is too large
Load Diff
@@ -60,17 +60,6 @@ internal static class GpuWaitRegistry
|
|||||||
// address) so distinct guest processes never alias.
|
// address) so distinct guest processes never alias.
|
||||||
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
|
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
|
||||||
|
|
||||||
|
|
||||||
private static object? Canonicalize(object? memory)
|
|
||||||
{
|
|
||||||
while (memory is SharpEmu.HLE.ICpuMemoryWrapper wrapper)
|
|
||||||
{
|
|
||||||
memory = wrapper.Inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
return memory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int Count
|
public static int Count
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -90,7 +79,6 @@ internal static class GpuWaitRegistry
|
|||||||
|
|
||||||
public static int CountForMemory(object memory)
|
public static int CountForMemory(object memory)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
var total = 0;
|
var total = 0;
|
||||||
@@ -118,7 +106,6 @@ internal static class GpuWaitRegistry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static OutstandingSnapshot SnapshotOutstanding(object? memory = null)
|
public static OutstandingSnapshot SnapshotOutstanding(object? memory = null)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory);
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
var outstanding = 0;
|
var outstanding = 0;
|
||||||
@@ -168,7 +155,6 @@ internal static class GpuWaitRegistry
|
|||||||
public static void Register(ulong address, WaitingDcb waiter)
|
public static void Register(ulong address, WaitingDcb waiter)
|
||||||
{
|
{
|
||||||
waiter.WaitAddress = address;
|
waiter.WaitAddress = address;
|
||||||
waiter.Memory = Canonicalize(waiter.Memory);
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!_waiters.TryGetValue(address, out var list))
|
if (!_waiters.TryGetValue(address, out var list))
|
||||||
@@ -191,7 +177,6 @@ internal static class GpuWaitRegistry
|
|||||||
object memory,
|
object memory,
|
||||||
Func<ulong, bool, ulong?> readValue)
|
Func<ulong, bool, ulong?> readValue)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
List<WaitingDcb>? woken = null;
|
List<WaitingDcb>? woken = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -252,7 +237,6 @@ internal static class GpuWaitRegistry
|
|||||||
long nowTicks,
|
long nowTicks,
|
||||||
long maxAgeTicks)
|
long maxAgeTicks)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
List<WaitingDcb>? stale = null;
|
List<WaitingDcb>? stale = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -289,7 +273,6 @@ internal static class GpuWaitRegistry
|
|||||||
ulong start,
|
ulong start,
|
||||||
ulong length)
|
ulong length)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
var matches = new List<(ulong Address, int Count)>();
|
var matches = new List<(ulong Address, int Count)>();
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
@@ -345,7 +328,6 @@ internal static class GpuWaitRegistry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool LatchSatisfiedByValue(object memory, ulong address, ulong value)
|
public static bool LatchSatisfiedByValue(object memory, ulong address, ulong value)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
var latchedAny = false;
|
var latchedAny = false;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -373,56 +355,6 @@ internal static class GpuWaitRegistry
|
|||||||
return latchedAny;
|
return latchedAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Every registered waiter, for the flip-stall watchdog. Not filtered by
|
|
||||||
/// memory identity — the watchdog wants a whole-process view.
|
|
||||||
/// </summary>
|
|
||||||
public static List<WaitingDcb> SnapshotAll()
|
|
||||||
{
|
|
||||||
var snapshot = new List<WaitingDcb>();
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
foreach (var (_, list) in _waiters)
|
|
||||||
{
|
|
||||||
snapshot.AddRange(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes the waiter at <paramref name="address"/> whose State is
|
|
||||||
/// <paramref name="state"/> — used when a new submission supersedes a
|
|
||||||
/// ring-tail park that would otherwise pin the queue forever.
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryRemoveByState(object state, ulong address)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_waiters.TryGetValue(address, out var list))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(list[i].State, state))
|
|
||||||
{
|
|
||||||
list.RemoveAt(i);
|
|
||||||
if (list.Count == 0)
|
|
||||||
{
|
|
||||||
_waiters.Remove(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes and returns waiters carrying a <see cref="WaitingDcb.RetryDeadlineTicks"/>
|
/// Removes and returns waiters carrying a <see cref="WaitingDcb.RetryDeadlineTicks"/>
|
||||||
/// that has elapsed. Used for indirect-dispatch dimension retries: the caller
|
/// that has elapsed. Used for indirect-dispatch dimension retries: the caller
|
||||||
@@ -431,7 +363,6 @@ internal static class GpuWaitRegistry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static List<WaitingDcb>? CollectExpiredRetries(object memory, long nowTicks)
|
public static List<WaitingDcb>? CollectExpiredRetries(object memory, long nowTicks)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
List<WaitingDcb>? expired = null;
|
List<WaitingDcb>? expired = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -474,7 +405,6 @@ internal static class GpuWaitRegistry
|
|||||||
|
|
||||||
public static List<WaitingDcb>? CollectAllForMemory(object memory)
|
public static List<WaitingDcb>? CollectAllForMemory(object memory)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
List<WaitingDcb>? collected = null;
|
List<WaitingDcb>? collected = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -560,7 +490,6 @@ internal static class GpuWaitRegistry
|
|||||||
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
|
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
|
||||||
public static bool RecordProduced(object memory, ulong address, ulong value)
|
public static bool RecordProduced(object memory, ulong address, ulong value)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_lastProduced.Count >= 8192)
|
if (_lastProduced.Count >= 8192)
|
||||||
@@ -594,7 +523,6 @@ internal static class GpuWaitRegistry
|
|||||||
long nowTicks,
|
long nowTicks,
|
||||||
long minAgeTicks)
|
long minAgeTicks)
|
||||||
{
|
{
|
||||||
memory = Canonicalize(memory)!;
|
|
||||||
List<WaitingDcb>? broken = null;
|
List<WaitingDcb>? broken = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
@@ -636,21 +564,6 @@ internal static class GpuWaitRegistry
|
|||||||
return broken;
|
return broken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Under orphan force-submit, producers can run ahead of waiter
|
|
||||||
// registration and pass an equal-compare value before it's ever seen.
|
|
||||||
// Treat == as "reached or passed" only in that mode, so other titles
|
|
||||||
// keep exact hardware semantics. SHARPEMU_GPU_WAIT_EQ_EXACT=1 restores
|
|
||||||
// strict equality for A/B.
|
|
||||||
private static readonly bool _equalCompareExact =
|
|
||||||
string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_GPU_WAIT_EQ_EXACT"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal) ||
|
|
||||||
!string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||||
{
|
{
|
||||||
var masked = value & waiter.Mask;
|
var masked = value & waiter.Mask;
|
||||||
@@ -660,7 +573,7 @@ internal static class GpuWaitRegistry
|
|||||||
0 => true,
|
0 => true,
|
||||||
1 => masked < reference,
|
1 => masked < reference,
|
||||||
2 => masked <= reference,
|
2 => masked <= reference,
|
||||||
3 => _equalCompareExact ? masked == reference : masked >= reference,
|
3 => masked == reference,
|
||||||
4 => masked != reference,
|
4 => masked != reference,
|
||||||
5 => masked >= reference,
|
5 => masked >= reference,
|
||||||
6 => masked > reference,
|
6 => masked > reference,
|
||||||
|
|||||||
@@ -21,18 +21,7 @@ public static class AjmExports
|
|||||||
private const int OrbisAjmErrorJobCreation = unchecked((int)0x80930012);
|
private const int OrbisAjmErrorJobCreation = unchecked((int)0x80930012);
|
||||||
private const ulong MaxSilentPcmBytes = 1 << 20;
|
private const ulong MaxSilentPcmBytes = 1 << 20;
|
||||||
private const uint Atrac9CodecType = 1;
|
private const uint Atrac9CodecType = 1;
|
||||||
// instanceId packs codecType into the high bits and the instance slot
|
private const uint MaxCodecType = 25;
|
||||||
// into the low InstanceIdSlotBits bits (see AjmInstanceCreate's
|
|
||||||
// `(codecType << InstanceIdSlotBits) | instanceSlot` and the
|
|
||||||
// `& InstanceIdSlotMask` unpacks in AjmInstanceDestroy/GetError).
|
|
||||||
private const int InstanceIdSlotBits = 14;
|
|
||||||
private const uint InstanceIdSlotMask = (1u << InstanceIdSlotBits) - 1;
|
|
||||||
// Registration is pure bookkeeping (a HashSet.Add), so the only real
|
|
||||||
// constraint is that codecType must not overflow the 32-bit instanceId
|
|
||||||
// once shifted left by InstanceIdSlotBits -- not any hardcoded list of
|
|
||||||
// known Sony codec ids, which a retail title's Gen5 codec type (e.g. 24)
|
|
||||||
// can legitimately fall outside of.
|
|
||||||
private const uint MaxCodecType = 1u << (32 - InstanceIdSlotBits);
|
|
||||||
private const int MaxInstanceIndex = 0x2FFF;
|
private const int MaxInstanceIndex = 0x2FFF;
|
||||||
private const int MaxDecodeBufferBytes = 64 * 1024 * 1024;
|
private const int MaxDecodeBufferBytes = 64 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -130,12 +119,9 @@ public static class AjmExports
|
|||||||
LibraryName = "libSceAjm")]
|
LibraryName = "libSceAjm")]
|
||||||
public static int AjmFinalize(CpuContext ctx)
|
public static int AjmFinalize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
if (!Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _))
|
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
|
||||||
{
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -287,7 +273,7 @@ public static class AjmExports
|
|||||||
}
|
}
|
||||||
while (state.InstancesBySlot.ContainsKey(instanceSlot));
|
while (state.InstancesBySlot.ContainsKey(instanceSlot));
|
||||||
|
|
||||||
instanceId = (codecType << InstanceIdSlotBits) | instanceSlot;
|
instanceId = (codecType << 14) | instanceSlot;
|
||||||
Span<byte> value = stackalloc byte[sizeof(uint)];
|
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(value, instanceId);
|
BinaryPrimitives.WriteUInt32LittleEndian(value, instanceId);
|
||||||
if (!ctx.Memory.TryWrite(outputAddress, value))
|
if (!ctx.Memory.TryWrite(outputAddress, value))
|
||||||
@@ -328,7 +314,7 @@ public static class AjmExports
|
|||||||
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
|
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
var instanceSlot = instanceId & InstanceIdSlotMask;
|
var instanceSlot = instanceId & 0x3FFF;
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (instanceSlot == 0 || !state.InstancesBySlot.Remove(instanceSlot))
|
if (instanceSlot == 0 || !state.InstancesBySlot.Remove(instanceSlot))
|
||||||
@@ -348,21 +334,8 @@ public static class AjmExports
|
|||||||
LibraryName = "libSceAjm")]
|
LibraryName = "libSceAjm")]
|
||||||
public static int AjmModuleUnregister(CpuContext ctx)
|
public static int AjmModuleUnregister(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
ctx[CpuRegister.Rax] = 0;
|
||||||
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
|
return 0;
|
||||||
if (!Contexts.TryGetValue(contextId, out var state))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool removed;
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
removed = state.RegisteredCodecs.Remove(codecType);
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace($"module_unregister context={contextId} codec={codecType} was_registered={removed}");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -694,8 +667,8 @@ public static class AjmExports
|
|||||||
private static bool TryGetInstance(uint instanceId, out AjmInstanceState instance)
|
private static bool TryGetInstance(uint instanceId, out AjmInstanceState instance)
|
||||||
{
|
{
|
||||||
instance = null!;
|
instance = null!;
|
||||||
var codec = instanceId >> InstanceIdSlotBits;
|
var codec = instanceId >> 14;
|
||||||
var slot = instanceId & InstanceIdSlotMask;
|
var slot = instanceId & 0x3FFF;
|
||||||
if (slot == 0)
|
if (slot == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -163,33 +163,6 @@ public static class AudioOut2Exports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ghost of Yotei calls this with flags=0 during Scream startup and never
|
|
||||||
// checks the result before continuing into its mastering path; the actual
|
|
||||||
// mastering chain lives in the host mixer, so accepting the request is
|
|
||||||
// sufficient.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "XHl38ZNknbs",
|
|
||||||
ExportName = "sceAudioOut2MasteringInit",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut2")]
|
|
||||||
public static int AudioOut2MasteringInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3D-audio object latency hint; the host mixer has no object pipeline to
|
|
||||||
// tune, but failure here makes Yotei tear down its whole ACM context and
|
|
||||||
// abort audio arena bring-up.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "TViD1EZXkNI",
|
|
||||||
ExportName = "sceAudioOut2Set3DLatency",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut2")]
|
|
||||||
public static int AudioOut2Set3DLatency(CpuContext ctx)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "t5YrizufpQc",
|
Nid = "t5YrizufpQc",
|
||||||
ExportName = "sceAudioOut2ContextResetParam",
|
ExportName = "sceAudioOut2ContextResetParam",
|
||||||
@@ -948,7 +921,6 @@ public static class AudioOut2Exports
|
|||||||
|
|
||||||
if (mixedPorts == 0)
|
if (mixedPorts == 0)
|
||||||
{
|
{
|
||||||
TraceSubmitSkipped(context, frames, "no-ports");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -970,7 +942,6 @@ public static class AudioOut2Exports
|
|||||||
var backend = ResolveContextBackend(context, out var backendName);
|
var backend = ResolveContextBackend(context, out var backendName);
|
||||||
if (backend is null)
|
if (backend is null)
|
||||||
{
|
{
|
||||||
TraceSubmitSkipped(context, frames, "no-backend");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,14 +1212,4 @@ public static class AudioOut2Exports
|
|||||||
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceSubmitSkipped(ContextState context, int frames, string reason)
|
|
||||||
{
|
|
||||||
var n = Interlocked.Increment(ref _submitSkipTraceCount);
|
|
||||||
if (n <= 8 || n % 500 == 0)
|
|
||||||
{
|
|
||||||
TraceAudioOut2(
|
|
||||||
$"context-submit-skip#{n} handle=0x{context.Handle:X} frames={frames} reason={reason}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.Media;
|
using SharpEmu.Libs.Media;
|
||||||
using SharpEmu.Libs.VideoOut;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -24,213 +22,14 @@ public static class AvPlayerExports
|
|||||||
private const int FrameHeightAlignment = 16;
|
private const int FrameHeightAlignment = 16;
|
||||||
private const int FrameInfoSize = 40;
|
private const int FrameInfoSize = 40;
|
||||||
private const int FrameInfoExSize = 104;
|
private const int FrameInfoExSize = 104;
|
||||||
// The legacy destination is 40 bytes on Gen4 but only 32 bytes on Gen5.
|
// This structure is 32 bytes. A larger write can damage the guest stack.
|
||||||
// Writing the Gen4 layout into a Gen5 caller can overwrite its stack canary.
|
private const int StreamInfoSize = 32;
|
||||||
private const int Gen4StreamInfoSize = 40;
|
private const int StreamInfoExSize = 32;
|
||||||
private const int Gen5StreamInfoSize = 32;
|
|
||||||
private const int StreamInfoExSize = 104;
|
|
||||||
private const int MaxGuestPathLength = 4096;
|
private const int MaxGuestPathLength = 4096;
|
||||||
private const int VideoPitchAlignment = 256;
|
|
||||||
private static readonly object StateGate = new();
|
private static readonly object StateGate = new();
|
||||||
private static readonly HashSet<string> TracedOnce = new();
|
private static readonly HashSet<string> TracedOnce = new();
|
||||||
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
private static readonly Dictionary<ulong, PlayerState> Players = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, ulong> VideoBufferRanges = new();
|
|
||||||
private static readonly bool TraceVideoImages = string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_AVPLAYER_IMAGES"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
private static int _traceCount;
|
private static int _traceCount;
|
||||||
private static int _videoPayloadTraceCount;
|
|
||||||
private static long _fallbackPresentationSerial;
|
|
||||||
|
|
||||||
internal static bool TryGetFallbackPresentationFrame(
|
|
||||||
out byte[] pixels,
|
|
||||||
out uint width,
|
|
||||||
out uint height,
|
|
||||||
out long serial)
|
|
||||||
{
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
PlayerState? latest = null;
|
|
||||||
foreach (var player in Players.Values)
|
|
||||||
{
|
|
||||||
if (player.FallbackPlayback is { } playback)
|
|
||||||
{
|
|
||||||
if (playback.TryGetFrame(
|
|
||||||
advanceClock: true,
|
|
||||||
out var playbackPixels,
|
|
||||||
out var advanced))
|
|
||||||
{
|
|
||||||
var skipFirstDecodedFrame =
|
|
||||||
player.SkipFirstFallbackPlaybackFrame;
|
|
||||||
if (ShouldPublishFallbackPlaybackFrame(
|
|
||||||
advanced,
|
|
||||||
player.FallbackPresentationPixels is not null,
|
|
||||||
ref skipFirstDecodedFrame))
|
|
||||||
{
|
|
||||||
player.FallbackPresentationPixels = playbackPixels;
|
|
||||||
player.FallbackPresentationWidth = playback.Width;
|
|
||||||
player.FallbackPresentationHeight = playback.Height;
|
|
||||||
player.FallbackPresentationSerial =
|
|
||||||
Interlocked.Increment(ref _fallbackPresentationSerial);
|
|
||||||
}
|
|
||||||
player.SkipFirstFallbackPlaybackFrame =
|
|
||||||
skipFirstDecodedFrame;
|
|
||||||
}
|
|
||||||
else if (playback.IsFinished)
|
|
||||||
{
|
|
||||||
playback.Dispose();
|
|
||||||
player.FallbackPlayback = null;
|
|
||||||
player.FallbackPlaybackCompleted = true;
|
|
||||||
player.FallbackPlaybackCompletedTicks = Stopwatch.GetTimestamp();
|
|
||||||
Trace(
|
|
||||||
$"host_fallback_finished handle=0x{player.Handle:X16} " +
|
|
||||||
"holding_last_frame=true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The host decoder can finish long before a heavily throttled
|
|
||||||
// guest AvPlayer reaches EOF. Keep its final image over the
|
|
||||||
// stale guest texture until the guest has actually consumed
|
|
||||||
// the stream; otherwise frame zero becomes visible again and
|
|
||||||
// the intro appears to start a second time. The hold is
|
|
||||||
// bounded: a title that pauses its player after the poster
|
|
||||||
// frame never reaches EOF, and an unbounded hold would pin the
|
|
||||||
// final movie image over everything the game renders next.
|
|
||||||
if (ShouldReleaseCompletedFallback(
|
|
||||||
player.FallbackPlaybackCompleted,
|
|
||||||
player.EndOfStream,
|
|
||||||
player.FallbackPlaybackCompletedTicks,
|
|
||||||
Stopwatch.GetTimestamp()))
|
|
||||||
{
|
|
||||||
ClearFallbackPresentation(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player.FallbackPresentationPixels is null ||
|
|
||||||
player.FallbackPresentationSerial <= 0 ||
|
|
||||||
latest is not null &&
|
|
||||||
player.FallbackPresentationSerial <= latest.FallbackPresentationSerial)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
latest = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest?.FallbackPresentationPixels is not { } frame)
|
|
||||||
{
|
|
||||||
pixels = [];
|
|
||||||
width = 0;
|
|
||||||
height = 0;
|
|
||||||
serial = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pixels = frame;
|
|
||||||
width = latest.FallbackPresentationWidth;
|
|
||||||
height = latest.FallbackPresentationHeight;
|
|
||||||
serial = latest.FallbackPresentationSerial;
|
|
||||||
return IsValidBgraFrame(pixels, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool ShouldPublishFallbackPlaybackFrame(
|
|
||||||
bool advanced,
|
|
||||||
bool hasPresentation,
|
|
||||||
ref bool skipFirstDecodedFrame)
|
|
||||||
{
|
|
||||||
if (advanced && hasPresentation && skipFirstDecodedFrame)
|
|
||||||
{
|
|
||||||
skipFirstDecodedFrame = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return advanced || !hasPresentation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How long a finished host playback keeps its final image on screen while
|
|
||||||
/// waiting for the guest player to reach end of stream. Titles that pause
|
|
||||||
/// their AvPlayer after the first frame never do, so the hold expires.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly long FallbackHoldGraceTicks = Stopwatch.Frequency;
|
|
||||||
|
|
||||||
internal static bool ShouldReleaseCompletedFallback(
|
|
||||||
bool fallbackPlaybackCompleted,
|
|
||||||
bool guestEndOfStream,
|
|
||||||
long completedTicks,
|
|
||||||
long nowTicks) =>
|
|
||||||
fallbackPlaybackCompleted &&
|
|
||||||
(guestEndOfStream ||
|
|
||||||
completedTicks != 0 && nowTicks - completedTicks >= FallbackHoldGraceTicks);
|
|
||||||
|
|
||||||
private static void ClearFallbackPresentation(PlayerState player)
|
|
||||||
{
|
|
||||||
player.FallbackPresentationPixels = null;
|
|
||||||
player.FallbackPresentationWidth = 0;
|
|
||||||
player.FallbackPresentationHeight = 0;
|
|
||||||
player.FallbackPresentationSerial = 0;
|
|
||||||
player.FallbackPlaybackCompleted = false;
|
|
||||||
player.FallbackPlaybackCompletedTicks = 0;
|
|
||||||
player.SkipFirstFallbackPlaybackFrame = false;
|
|
||||||
Trace(
|
|
||||||
$"host_fallback_released handle=0x{player.Handle:X16} " +
|
|
||||||
$"guest_eof={player.EndOfStream}");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool ShouldTraceVideoBufferAddress(ulong address)
|
|
||||||
{
|
|
||||||
if (!TraceVideoImages || address == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (start, length) in VideoBufferRanges)
|
|
||||||
{
|
|
||||||
if (address >= start && address - start < length)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool ShouldTraceVideoBufferRange(ulong address, ulong length)
|
|
||||||
{
|
|
||||||
if (!TraceVideoImages || address == 0 || length == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (start, rangeLength) in VideoBufferRanges)
|
|
||||||
{
|
|
||||||
if (address <= start
|
|
||||||
? start - address < length
|
|
||||||
: address - start < rangeLength)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RegisterVideoBuffer(ulong address, int size, int index, string source)
|
|
||||||
{
|
|
||||||
if (address == 0 || size <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoBufferRanges[address] = checked((ulong)size);
|
|
||||||
if (TraceVideoImages)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[AVPLAYER][TRACE] video_buffer index={index} source={source} " +
|
|
||||||
$"data=0x{address:X16} size={size}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class PlayerState : IDisposable
|
private sealed class PlayerState : IDisposable
|
||||||
{
|
{
|
||||||
@@ -246,8 +45,6 @@ public static class AvPlayerExports
|
|||||||
public int Height { get; set; }
|
public int Height { get; set; }
|
||||||
public double FramesPerSecond { get; set; } = 30.0;
|
public double FramesPerSecond { get; set; } = 30.0;
|
||||||
public ulong DurationMilliseconds { get; set; }
|
public ulong DurationMilliseconds { get; set; }
|
||||||
public bool HasAudio { get; set; }
|
|
||||||
public bool IsGen5 { get; init; }
|
|
||||||
public bool Started { get; set; }
|
public bool Started { get; set; }
|
||||||
public bool Paused { get; set; }
|
public bool Paused { get; set; }
|
||||||
public bool Looping { get; set; }
|
public bool Looping { get; set; }
|
||||||
@@ -264,20 +61,10 @@ public static class AvPlayerExports
|
|||||||
public int GuestBufferStride { get; set; }
|
public int GuestBufferStride { get; set; }
|
||||||
public int NextGuestBuffer { get; set; }
|
public int NextGuestBuffer { get; set; }
|
||||||
public ulong LastGuestBuffer { get; set; }
|
public ulong LastGuestBuffer { get; set; }
|
||||||
public ulong LastVideoTimestamp { get; set; }
|
|
||||||
public long NextFrameIndex { get; set; }
|
public long NextFrameIndex { get; set; }
|
||||||
public ulong AudioBufferBase { get; set; }
|
public ulong AudioBufferBase { get; set; }
|
||||||
public int NextAudioBuffer { get; set; }
|
public int NextAudioBuffer { get; set; }
|
||||||
public long NextAudioFrameIndex { get; set; }
|
public long NextAudioFrameIndex { get; set; }
|
||||||
public byte[]? FallbackPresentationPixels { get; set; }
|
|
||||||
public uint FallbackPresentationWidth { get; set; }
|
|
||||||
public uint FallbackPresentationHeight { get; set; }
|
|
||||||
public long FallbackPresentationSerial { get; set; }
|
|
||||||
public MediaFramePlayback? FallbackPlayback { get; set; }
|
|
||||||
public bool FallbackPlaybackAttempted { get; set; }
|
|
||||||
public bool FallbackPlaybackCompleted { get; set; }
|
|
||||||
public long FallbackPlaybackCompletedTicks { get; set; }
|
|
||||||
public bool SkipFirstFallbackPlaybackFrame { get; set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -285,8 +72,6 @@ public static class AvPlayerExports
|
|||||||
DecoderOutput = null;
|
DecoderOutput = null;
|
||||||
AudioDecoderOutput?.Dispose();
|
AudioDecoderOutput?.Dispose();
|
||||||
AudioDecoderOutput = null;
|
AudioDecoderOutput = null;
|
||||||
FallbackPlayback?.Dispose();
|
|
||||||
FallbackPlayback = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetPlayback()
|
public void ResetPlayback()
|
||||||
@@ -294,19 +79,9 @@ public static class AvPlayerExports
|
|||||||
Dispose();
|
Dispose();
|
||||||
PlaybackClock.Reset();
|
PlaybackClock.Reset();
|
||||||
NextFrameIndex = 0;
|
NextFrameIndex = 0;
|
||||||
LastGuestBuffer = 0;
|
|
||||||
LastVideoTimestamp = 0;
|
|
||||||
NextAudioFrameIndex = 0;
|
NextAudioFrameIndex = 0;
|
||||||
SkippedFrameDebt = 0;
|
SkippedFrameDebt = 0;
|
||||||
EndOfStream = false;
|
EndOfStream = false;
|
||||||
FallbackPresentationPixels = null;
|
|
||||||
FallbackPresentationWidth = 0;
|
|
||||||
FallbackPresentationHeight = 0;
|
|
||||||
FallbackPresentationSerial = 0;
|
|
||||||
FallbackPlaybackAttempted = false;
|
|
||||||
FallbackPlaybackCompleted = false;
|
|
||||||
FallbackPlaybackCompletedTicks = 0;
|
|
||||||
SkipFirstFallbackPlaybackFrame = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,12 +102,10 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
var autoStartOffset = GetAutoStartOffset(ctx.TargetGeneration, extended: false);
|
|
||||||
Players.Add(handle, new PlayerState
|
Players.Add(handle, new PlayerState
|
||||||
{
|
{
|
||||||
Handle = handle,
|
Handle = handle,
|
||||||
IsGen5 = IsGen5Target(ctx.TargetGeneration),
|
AutoStart = TryReadByte(ctx, initDataAddress + 108, out var autoStart) && autoStart != 0,
|
||||||
AutoStart = TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
|
|
||||||
AllocatorObject = TryReadUInt64(ctx, initDataAddress, out var allocatorObject) ? allocatorObject : 0,
|
AllocatorObject = TryReadUInt64(ctx, initDataAddress, out var allocatorObject) ? allocatorObject : 0,
|
||||||
AllocateTextureCallback = TryReadUInt64(ctx, initDataAddress + 24, out var allocateTexture) ? allocateTexture : 0,
|
AllocateTextureCallback = TryReadUInt64(ctx, initDataAddress + 24, out var allocateTexture) ? allocateTexture : 0,
|
||||||
AllocateCallback = TryReadUInt64(ctx, initDataAddress + 8, out var allocate) ? allocate : 0,
|
AllocateCallback = TryReadUInt64(ctx, initDataAddress + 8, out var allocate) ? allocate : 0,
|
||||||
@@ -384,12 +157,10 @@ public static class AvPlayerExports
|
|||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
var autoStartOffset = GetAutoStartOffset(ctx.TargetGeneration, extended: true);
|
|
||||||
Players.Add(handle, new PlayerState
|
Players.Add(handle, new PlayerState
|
||||||
{
|
{
|
||||||
Handle = handle,
|
Handle = handle,
|
||||||
IsGen5 = IsGen5Target(ctx.TargetGeneration),
|
AutoStart = TryReadByte(ctx, initDataAddress + 164, out var autoStart) && autoStart != 0,
|
||||||
AutoStart = TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
|
|
||||||
AllocatorObject = TryReadUInt64(ctx, initDataAddress + 8, out var allocatorObject) ? allocatorObject : 0,
|
AllocatorObject = TryReadUInt64(ctx, initDataAddress + 8, out var allocatorObject) ? allocatorObject : 0,
|
||||||
AllocateTextureCallback = TryReadUInt64(ctx, initDataAddress + 32, out var allocateTexture) ? allocateTexture : 0,
|
AllocateTextureCallback = TryReadUInt64(ctx, initDataAddress + 32, out var allocateTexture) ? allocateTexture : 0,
|
||||||
AllocateCallback = TryReadUInt64(ctx, initDataAddress + 16, out var allocate) ? allocate : 0,
|
AllocateCallback = TryReadUInt64(ctx, initDataAddress + 16, out var allocate) ? allocate : 0,
|
||||||
@@ -550,24 +321,20 @@ public static class AvPlayerExports
|
|||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerResume(CpuContext ctx)
|
public static int AvPlayerResume(CpuContext ctx)
|
||||||
{
|
{
|
||||||
PlayerState player;
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var foundPlayer))
|
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var player))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, InvalidParameters);
|
return SetReturn(ctx, InvalidParameters);
|
||||||
}
|
}
|
||||||
player = foundPlayer;
|
|
||||||
|
|
||||||
player.Paused = false;
|
player.Paused = false;
|
||||||
if (player.DecoderOutput is not null)
|
if (player.DecoderOutput is not null)
|
||||||
{
|
{
|
||||||
player.PlaybackClock.Start();
|
player.PlaybackClock.Start();
|
||||||
}
|
}
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyEvent(ctx, player, 3); // StatePlay
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -618,33 +385,8 @@ public static class AvPlayerExports
|
|||||||
ExportName = "sceAvPlayerGetStreamInfoEx",
|
ExportName = "sceAvPlayerGetStreamInfoEx",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfoEx(CpuContext ctx)
|
public static int AvPlayerGetStreamInfoEx(CpuContext ctx) =>
|
||||||
{
|
GetStreamInfoCore(ctx, StreamInfoExSize);
|
||||||
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var player) ||
|
|
||||||
streamIndex > (player.HasAudio ? 1u : 0u) ||
|
|
||||||
infoAddress == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, InvalidParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[StreamInfoExSize];
|
|
||||||
info.Clear();
|
|
||||||
WriteGen5StreamInfoEx(
|
|
||||||
info,
|
|
||||||
GetStreamType(ctx.TargetGeneration, streamIndex),
|
|
||||||
streamIndex == 0 ? checked((uint)player.Width) : 0,
|
|
||||||
streamIndex == 0 ? checked((uint)player.Height) : 0,
|
|
||||||
streamIndex == 0 ? player.FramesPerSecond : 0,
|
|
||||||
player.DurationMilliseconds);
|
|
||||||
return SetReturn(
|
|
||||||
ctx,
|
|
||||||
ctx.Memory.TryWrite(infoAddress, info) ? 0 : InvalidParameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XC9wM+xULz8",
|
Nid = "XC9wM+xULz8",
|
||||||
@@ -718,15 +460,14 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
var found = Players.TryGetValue(ctx[CpuRegister.Rdi], out var player);
|
var found = Players.TryGetValue(ctx[CpuRegister.Rdi], out var player);
|
||||||
if (!found || infoAddress == 0 || !player!.Started || player.Paused ||
|
if (!found || infoAddress == 0 || !player!.Started || player.Paused ||
|
||||||
player.EndOfStream || player.SourcePath is null ||
|
player.EndOfStream || player.SourcePath is null || !EnsureAudioDecoder(player))
|
||||||
!player.HasAudio || !EnsureAudioDecoder(player))
|
|
||||||
{
|
{
|
||||||
TraceOnce(
|
TraceOnce(
|
||||||
"audio_data_refused",
|
"audio_data_refused",
|
||||||
$"audio_data refused found={found} info=0x{infoAddress:X16} " +
|
$"audio_data refused found={found} info=0x{infoAddress:X16} " +
|
||||||
$"started={(found && player!.Started)} paused={(found && player!.Paused)} " +
|
$"started={(found && player!.Started)} paused={(found && player!.Paused)} " +
|
||||||
$"eos={(found && player!.EndOfStream)} " +
|
$"eos={(found && player!.EndOfStream)} " +
|
||||||
$"has_audio={(found && player!.HasAudio)}");
|
$"decoder={(found && player!.SourcePath is not null && EnsureAudioDecoder(player))}");
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,11 +550,9 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return SetReturn(
|
var known = Players.ContainsKey(ctx[CpuRegister.Rdi]);
|
||||||
ctx,
|
TraceOnce("stream_count", $"stream_count known={known} returned={(known ? 2 : -1)}");
|
||||||
Players.TryGetValue(ctx[CpuRegister.Rdi], out var player)
|
return SetReturn(ctx, known ? 2 : InvalidParameters);
|
||||||
? player.HasAudio ? 2 : 1
|
|
||||||
: InvalidParameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,12 +560,7 @@ public static class AvPlayerExports
|
|||||||
ulong handle,
|
ulong handle,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
ulong durationMilliseconds,
|
ulong durationMilliseconds)
|
||||||
ulong allocateTextureCallback = 0,
|
|
||||||
ulong allocateCallback = 0,
|
|
||||||
bool hasAudio = false,
|
|
||||||
double framesPerSecond = 30.0,
|
|
||||||
bool isGen5 = true)
|
|
||||||
{
|
{
|
||||||
PlayerState? previous;
|
PlayerState? previous;
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -835,40 +569,15 @@ public static class AvPlayerExports
|
|||||||
Players[handle] = new PlayerState
|
Players[handle] = new PlayerState
|
||||||
{
|
{
|
||||||
Handle = handle,
|
Handle = handle,
|
||||||
IsGen5 = isGen5,
|
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
DurationMilliseconds = durationMilliseconds,
|
DurationMilliseconds = durationMilliseconds,
|
||||||
HasAudio = hasAudio,
|
|
||||||
FramesPerSecond = framesPerSecond,
|
|
||||||
AllocateTextureCallback = allocateTextureCallback,
|
|
||||||
AllocateCallback = allocateCallback,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
previous?.Dispose();
|
previous?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool AllocateGuestVideoBuffersForTest(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong handle,
|
|
||||||
out ulong firstBuffer)
|
|
||||||
{
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
if (!Players.TryGetValue(handle, out var player))
|
|
||||||
{
|
|
||||||
firstBuffer = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bufferSize = GetVideoBufferSize(player);
|
|
||||||
var allocated = AllocateGuestVideoBuffers(ctx, player, bufferSize);
|
|
||||||
firstBuffer = player.GuestBuffers[0];
|
|
||||||
return allocated && firstBuffer != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void RemovePlayerForTest(ulong handle)
|
internal static void RemovePlayerForTest(ulong handle)
|
||||||
{
|
{
|
||||||
PlayerState? player;
|
PlayerState? player;
|
||||||
@@ -886,27 +595,23 @@ public static class AvPlayerExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAvPlayer")]
|
LibraryName = "libSceAvPlayer")]
|
||||||
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
|
public static int AvPlayerGetStreamInfo(CpuContext ctx) =>
|
||||||
GetStreamInfoCore(ctx);
|
GetStreamInfoCore(ctx, StreamInfoSize);
|
||||||
|
|
||||||
private static int GetStreamInfoCore(CpuContext ctx)
|
private static int GetStreamInfoCore(CpuContext ctx, int infoSize)
|
||||||
{
|
{
|
||||||
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
var infoAddress = ctx[CpuRegister.Rdx];
|
var infoAddress = ctx[CpuRegister.Rdx];
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var player) ||
|
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var player) ||
|
||||||
streamIndex > (player.HasAudio ? 1u : 0u) ||
|
streamIndex > 1 || infoAddress == 0 || player.Width <= 0 || player.Height <= 0)
|
||||||
infoAddress == 0 || player.Width <= 0 || player.Height <= 0)
|
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, InvalidParameters);
|
return SetReturn(ctx, InvalidParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
var infoSize = GetLegacyStreamInfoSize(ctx.TargetGeneration);
|
|
||||||
Span<byte> info = stackalloc byte[infoSize];
|
Span<byte> info = stackalloc byte[infoSize];
|
||||||
info.Clear();
|
info.Clear();
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio
|
||||||
info[0..],
|
|
||||||
GetStreamType(ctx.TargetGeneration, streamIndex));
|
|
||||||
if (streamIndex == 0)
|
if (streamIndex == 0)
|
||||||
{
|
{
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[8..], checked((uint)player.Width));
|
BinaryPrimitives.WriteUInt32LittleEndian(info[8..], checked((uint)player.Width));
|
||||||
@@ -945,14 +650,7 @@ public static class AvPlayerExports
|
|||||||
player = foundPlayer;
|
player = foundPlayer;
|
||||||
|
|
||||||
var hostPath = ResolveGuestPath(guestPath);
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
if (hostPath is null ||
|
if (hostPath is null || !ProbeVideo(hostPath, out var width, out var height, out var fps, out var duration))
|
||||||
!ProbeVideo(
|
|
||||||
hostPath,
|
|
||||||
out var width,
|
|
||||||
out var height,
|
|
||||||
out var fps,
|
|
||||||
out var duration,
|
|
||||||
out var hasAudio))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[AVPLAYER][ERROR] Could not open guest video '{guestPath}' (resolved '{hostPath ?? "<none>"}').");
|
Console.Error.WriteLine($"[AVPLAYER][ERROR] Could not open guest video '{guestPath}' (resolved '{hostPath ?? "<none>"}').");
|
||||||
return SetReturn(ctx, OperationFailed);
|
return SetReturn(ctx, OperationFailed);
|
||||||
@@ -964,13 +662,14 @@ public static class AvPlayerExports
|
|||||||
player.Height = height;
|
player.Height = height;
|
||||||
player.FramesPerSecond = fps;
|
player.FramesPerSecond = fps;
|
||||||
player.DurationMilliseconds = duration;
|
player.DurationMilliseconds = duration;
|
||||||
player.HasAudio = hasAudio;
|
|
||||||
player.Started = player.AutoStart;
|
player.Started = player.AutoStart;
|
||||||
autoStart = player.AutoStart;
|
autoStart = player.AutoStart;
|
||||||
Trace(
|
Trace($"source guest='{guestPath}' host='{hostPath}' {width}x{height} fps={fps:F3} duration_ms={duration} auto_start={player.AutoStart}");
|
||||||
$"source guest='{guestPath}' host='{hostPath}' {width}x{height} " +
|
|
||||||
$"fps={fps:F3} duration_ms={duration} audio={hasAudio} auto_start={player.AutoStart}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
EnsureGuestVideoBuffers(ctx, player);
|
||||||
|
|
||||||
NotifyEvent(ctx, player, 2); // StateReady
|
NotifyEvent(ctx, player, 2); // StateReady
|
||||||
if (autoStart)
|
if (autoStart)
|
||||||
{
|
{
|
||||||
@@ -985,23 +684,12 @@ public static class AvPlayerExports
|
|||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var player) ||
|
if (!Players.TryGetValue(ctx[CpuRegister.Rdi], out var player) ||
|
||||||
infoAddress == 0 || !player.Started || player.EndOfStream ||
|
infoAddress == 0 || !player.Started || player.Paused || player.EndOfStream ||
|
||||||
player.SourcePath is null)
|
player.SourcePath is null)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.Paused)
|
|
||||||
{
|
|
||||||
return SetReturn(
|
|
||||||
ctx,
|
|
||||||
player.IsGen5 &&
|
|
||||||
player.LastGuestBuffer != 0 &&
|
|
||||||
WriteHeldVideoFrameInfo(ctx, player, infoAddress, extended)
|
|
||||||
? 1
|
|
||||||
: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EnsureDecoder(player))
|
if (!EnsureDecoder(player))
|
||||||
{
|
{
|
||||||
player.EndOfStream = true;
|
player.EndOfStream = true;
|
||||||
@@ -1043,7 +731,6 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
player.LastVideoTimestamp = timestamp;
|
|
||||||
|
|
||||||
Trace($"video_frame handle=0x{player.Handle:X16} ex={extended} ts={timestamp} data=0x{player.LastGuestBuffer:X16}");
|
Trace($"video_frame handle=0x{player.Handle:X16} ex={extended} ts={timestamp} data=0x{player.LastGuestBuffer:X16}");
|
||||||
return SetReturn(ctx, 1);
|
return SetReturn(ctx, 1);
|
||||||
@@ -1169,10 +856,9 @@ public static class AvPlayerExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var alignedWidth = AlignUp(player.Width, 16);
|
var alignedWidth = AlignUp(player.Width, FramePitchAlignment);
|
||||||
var alignedHeight = AlignUp(player.Height, 16);
|
var alignedHeight = AlignUp(player.Height, FrameHeightAlignment);
|
||||||
var (pitch, bufferHeight) = GetFrameGeometry(player, extended);
|
var bufferStride = GetVideoBufferSize(player);
|
||||||
var bufferStride = CalculateNv12BufferSize(pitch, bufferHeight);
|
|
||||||
if (player.GuestBuffers[0] == 0)
|
if (player.GuestBuffers[0] == 0)
|
||||||
{
|
{
|
||||||
if (!AllocateGuestVideoBuffers(ctx, player, bufferStride))
|
if (!AllocateGuestVideoBuffers(ctx, player, bufferStride))
|
||||||
@@ -1180,34 +866,12 @@ public static class AvPlayerExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
player.GuestBufferStride = bufferStride;
|
player.GuestBufferStride = bufferStride;
|
||||||
Trace(
|
|
||||||
$"video_layout ex={extended} width={player.Width} height={player.Height} " +
|
|
||||||
$"pitch={pitch} uv_offset={checked(pitch * bufferHeight)} size={bufferStride}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var frameData = player.RawFrame;
|
var frameData = player.RawFrame;
|
||||||
if (extended)
|
if (!extended && (alignedWidth != player.Width || alignedHeight != player.Height))
|
||||||
{
|
{
|
||||||
if (player.PaddedFrame is null || player.PaddedFrame.Length != bufferStride)
|
player.PaddedFrame ??= new byte[bufferStride];
|
||||||
{
|
|
||||||
player.PaddedFrame = new byte[bufferStride];
|
|
||||||
}
|
|
||||||
CopyNv12ToGuestBuffer(
|
|
||||||
player.RawFrame,
|
|
||||||
player.PaddedFrame,
|
|
||||||
player.Width,
|
|
||||||
player.Height,
|
|
||||||
player.Width,
|
|
||||||
player.Width,
|
|
||||||
pitch);
|
|
||||||
frameData = player.PaddedFrame;
|
|
||||||
}
|
|
||||||
else if (alignedWidth != player.Width || alignedHeight != player.Height)
|
|
||||||
{
|
|
||||||
if (player.PaddedFrame is null || player.PaddedFrame.Length != bufferStride)
|
|
||||||
{
|
|
||||||
player.PaddedFrame = new byte[bufferStride];
|
|
||||||
}
|
|
||||||
player.PaddedFrame.AsSpan().Clear();
|
player.PaddedFrame.AsSpan().Clear();
|
||||||
for (var row = 0; row < player.Height; row++)
|
for (var row = 0; row < player.Height; row++)
|
||||||
{
|
{
|
||||||
@@ -1231,218 +895,44 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (player.TextureAllocatorFailed)
|
|
||||||
{
|
|
||||||
EnsureFallbackPlayback(player);
|
|
||||||
if (player.FallbackPresentationPixels is null)
|
|
||||||
{
|
|
||||||
// Keep one immediate poster frame while the background decoder
|
|
||||||
// starts. Subsequent frames come from the bounded, scaled host
|
|
||||||
// playback; converting every 4K NV12 guest frame here would
|
|
||||||
// duplicate decoding work and dominate the emulation thread.
|
|
||||||
var bgra = GC.AllocateUninitializedArray<byte>(
|
|
||||||
checked(player.Width * player.Height * 4));
|
|
||||||
ConvertNv12ToBgra(
|
|
||||||
frameData,
|
|
||||||
pitch,
|
|
||||||
bufferHeight,
|
|
||||||
player.Width,
|
|
||||||
player.Height,
|
|
||||||
bgra);
|
|
||||||
player.FallbackPresentationPixels = bgra;
|
|
||||||
player.FallbackPresentationWidth = checked((uint)player.Width);
|
|
||||||
player.FallbackPresentationHeight = checked((uint)player.Height);
|
|
||||||
player.FallbackPresentationSerial =
|
|
||||||
Interlocked.Increment(ref _fallbackPresentationSerial);
|
|
||||||
player.SkipFirstFallbackPlaybackFrame =
|
|
||||||
player.FallbackPlayback is not null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (TraceVideoImages)
|
|
||||||
{
|
|
||||||
var traceIndex = Interlocked.Increment(ref _videoPayloadTraceCount);
|
|
||||||
if (traceIndex <= 16)
|
|
||||||
{
|
|
||||||
var summary = GuestImageUploadPayloadDiagnostics.Summarize(frameData);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[AVPLAYER][TRACE] video_payload index={traceIndex - 1} " +
|
|
||||||
$"data=0x{bufferAddress:X16} bytes={frameData.Length} " +
|
|
||||||
$"pitch={pitch} uv_offset={checked(pitch * bufferHeight)} " +
|
|
||||||
$"nonzero_bytes={summary.NonzeroBytes}/{frameData.Length} " +
|
|
||||||
$"hash=0x{summary.Hash:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> info = extended
|
Span<byte> info = extended
|
||||||
? stackalloc byte[FrameInfoExSize]
|
? stackalloc byte[FrameInfoExSize]
|
||||||
: stackalloc byte[FrameInfoSize];
|
: stackalloc byte[FrameInfoSize];
|
||||||
info.Clear();
|
info.Clear();
|
||||||
WriteVideoFrameInfo(
|
BinaryPrimitives.WriteUInt64LittleEndian(info[0..], bufferAddress);
|
||||||
info,
|
BinaryPrimitives.WriteUInt64LittleEndian(info[16..], timestamp);
|
||||||
ctx.TargetGeneration,
|
BinaryPrimitives.WriteUInt32LittleEndian(info[24..], checked((uint)(extended ? player.Width : alignedWidth)));
|
||||||
extended,
|
BinaryPrimitives.WriteUInt32LittleEndian(info[28..], checked((uint)(extended ? player.Height : alignedHeight)));
|
||||||
bufferAddress,
|
BinaryPrimitives.WriteSingleLittleEndian(info[32..], 1.0f);
|
||||||
timestamp,
|
if (extended)
|
||||||
checked((uint)pitch),
|
{
|
||||||
checked((uint)player.Width),
|
BinaryPrimitives.WriteUInt32LittleEndian(info[60..], checked((uint)player.Width));
|
||||||
checked((uint)(extended ? player.Height : bufferHeight)),
|
info[64] = 8;
|
||||||
checked((uint)pitch),
|
info[65] = 8;
|
||||||
player.FramesPerSecond);
|
}
|
||||||
return ctx.Memory.TryWrite(infoAddress, info);
|
return ctx.Memory.TryWrite(infoAddress, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (int Pitch, int Height) GetFrameGeometry(
|
|
||||||
PlayerState player,
|
|
||||||
bool extended)
|
|
||||||
{
|
|
||||||
var gen5Extended = extended && player.IsGen5;
|
|
||||||
return (
|
|
||||||
gen5Extended ? CalculateNv12Pitch(player.Width) : AlignUp(player.Width, 16),
|
|
||||||
gen5Extended ? player.Height : AlignUp(player.Height, 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool WriteHeldVideoFrameInfo(
|
|
||||||
CpuContext ctx,
|
|
||||||
PlayerState player,
|
|
||||||
ulong infoAddress,
|
|
||||||
bool extended)
|
|
||||||
{
|
|
||||||
var (pitch, bufferHeight) = GetFrameGeometry(player, extended);
|
|
||||||
Span<byte> info = extended
|
|
||||||
? stackalloc byte[FrameInfoExSize]
|
|
||||||
: stackalloc byte[FrameInfoSize];
|
|
||||||
info.Clear();
|
|
||||||
WriteVideoFrameInfo(
|
|
||||||
info,
|
|
||||||
ctx.TargetGeneration,
|
|
||||||
extended,
|
|
||||||
player.LastGuestBuffer,
|
|
||||||
player.LastVideoTimestamp,
|
|
||||||
checked((uint)pitch),
|
|
||||||
checked((uint)player.Width),
|
|
||||||
checked((uint)(extended ? player.Height : bufferHeight)),
|
|
||||||
checked((uint)pitch),
|
|
||||||
player.FramesPerSecond);
|
|
||||||
return ctx.Memory.TryWrite(infoAddress, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The title-provided allocators can reject large decoded surfaces. In
|
|
||||||
/// that case the guest has no texture it can sample, and some titles pause
|
|
||||||
/// their AvPlayer after acquiring a poster frame. Keep that compatibility
|
|
||||||
/// path useful by running a separate, bounded host playback to completion.
|
|
||||||
/// MediaFramePlayback performs decode work off the Vulkan thread, advances
|
|
||||||
/// on the movie clock, drops frames when rendering is slow, and relinquishes
|
|
||||||
/// presentation automatically at EOF so normal guest rendering resumes.
|
|
||||||
/// </summary>
|
|
||||||
private static void EnsureFallbackPlayback(PlayerState player)
|
|
||||||
{
|
|
||||||
if (player.FallbackPlaybackAttempted || player.SourcePath is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.FallbackPlaybackAttempted = true;
|
|
||||||
var videoOptions = HostVideoHost.CurrentOptions;
|
|
||||||
var maximumWidth = checked((uint)videoOptions.Width);
|
|
||||||
var maximumHeight = checked((uint)videoOptions.Height);
|
|
||||||
if (!FfmpegVideoDecoder.TryOpen(
|
|
||||||
player.SourcePath,
|
|
||||||
maximumWidth,
|
|
||||||
maximumHeight,
|
|
||||||
out var decoder) ||
|
|
||||||
decoder is null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[AVPLAYER][WARN] Could not start host fallback playback for '{player.SourcePath}'.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.FallbackPlayback = new MediaFramePlayback(decoder);
|
|
||||||
Trace(
|
|
||||||
$"host_fallback_started handle=0x{player.Handle:X16} " +
|
|
||||||
$"source={player.Width}x{player.Height} output={decoder.Width}x{decoder.Height} " +
|
|
||||||
$"host_limit={maximumWidth}x{maximumHeight} " +
|
|
||||||
$"fps={decoder.FramesPerSecondNumerator}/{decoder.FramesPerSecondDenominator}");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static int CalculateNv12Pitch(int width) =>
|
|
||||||
AlignUp(width, VideoPitchAlignment);
|
|
||||||
|
|
||||||
internal static int CalculateNv12BufferSize(int pitch, int height) =>
|
|
||||||
checked(pitch * height * 3 / 2);
|
|
||||||
|
|
||||||
internal static void ConvertNv12ToBgra(
|
|
||||||
ReadOnlySpan<byte> nv12,
|
|
||||||
int pitch,
|
|
||||||
int bufferHeight,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
Span<byte> bgra)
|
|
||||||
{
|
|
||||||
var requiredNv12 = CalculateNv12BufferSize(pitch, bufferHeight);
|
|
||||||
var requiredBgra = checked(width * height * 4);
|
|
||||||
if (pitch < width || bufferHeight < height ||
|
|
||||||
nv12.Length < requiredNv12 || bgra.Length < requiredBgra)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("NV12 frame dimensions do not match the supplied buffers.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var chromaOffset = checked(pitch * bufferHeight);
|
|
||||||
for (var y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
var lumaRow = y * pitch;
|
|
||||||
var chromaRow = chromaOffset + ((y >> 1) * pitch);
|
|
||||||
var outputRow = y * width * 4;
|
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
|
||||||
var luma = nv12[lumaRow + x];
|
|
||||||
var chromaColumn = x & ~1;
|
|
||||||
var u = nv12[chromaRow + chromaColumn];
|
|
||||||
var v = nv12[chromaRow + chromaColumn + 1];
|
|
||||||
var c = Math.Max(0, luma - 16);
|
|
||||||
var d = u - 128;
|
|
||||||
var e = v - 128;
|
|
||||||
var output = outputRow + (x * 4);
|
|
||||||
bgra[output] = ClampToByte((298 * c + 516 * d + 128) >> 8);
|
|
||||||
bgra[output + 1] = ClampToByte((298 * c - 100 * d - 208 * e + 128) >> 8);
|
|
||||||
bgra[output + 2] = ClampToByte((298 * c + 409 * e + 128) >> 8);
|
|
||||||
bgra[output + 3] = byte.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte ClampToByte(int value) =>
|
|
||||||
checked((byte)Math.Clamp(value, byte.MinValue, byte.MaxValue));
|
|
||||||
|
|
||||||
private static int GetVideoBufferSize(PlayerState player) =>
|
private static int GetVideoBufferSize(PlayerState player) =>
|
||||||
checked(
|
checked(
|
||||||
AlignUp(player.Width, FramePitchAlignment) *
|
AlignUp(player.Width, FramePitchAlignment) *
|
||||||
AlignUp(player.Height, FrameHeightAlignment) * 3 / 2);
|
AlignUp(player.Height, FrameHeightAlignment) * 3 / 2);
|
||||||
|
|
||||||
internal static void CopyNv12ToGuestBuffer(
|
private static void EnsureGuestVideoBuffers(CpuContext ctx, PlayerState player)
|
||||||
ReadOnlySpan<byte> source,
|
|
||||||
Span<byte> destination,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
int sourceLumaStride,
|
|
||||||
int sourceChromaStride,
|
|
||||||
int destinationPitch)
|
|
||||||
{
|
{
|
||||||
var sourceChromaOffset = checked(sourceLumaStride * height);
|
lock (StateGate)
|
||||||
var destinationChromaOffset = checked(destinationPitch * height);
|
{
|
||||||
var destinationSize = CalculateNv12BufferSize(destinationPitch, height);
|
if (player.GuestBuffers[0] != 0 || player.Width <= 0 || player.Height <= 0)
|
||||||
destination[..destinationSize].Clear();
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (var row = 0; row < height; row++)
|
var bufferSize = GetVideoBufferSize(player);
|
||||||
{
|
if (AllocateGuestVideoBuffers(ctx, player, bufferSize))
|
||||||
source.Slice(row * sourceLumaStride, width)
|
{
|
||||||
.CopyTo(destination.Slice(row * destinationPitch, width));
|
player.GuestBufferStride = bufferSize;
|
||||||
}
|
}
|
||||||
for (var row = 0; row < height / 2; row++)
|
|
||||||
{
|
|
||||||
source.Slice(sourceChromaOffset + (row * sourceChromaStride), width)
|
|
||||||
.CopyTo(destination.Slice(destinationChromaOffset + (row * destinationPitch), width));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1486,7 +976,6 @@ public static class AvPlayerExports
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
player.GuestBuffers[index] = buffer;
|
player.GuestBuffers[index] = buffer;
|
||||||
RegisterVideoBuffer(buffer, bufferSize, index, "guest-callback");
|
|
||||||
Trace($"{kind}_buffer index={index} data=0x{buffer:X16} size={bufferSize}");
|
Trace($"{kind}_buffer index={index} data=0x{buffer:X16} size={bufferSize}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1495,6 +984,7 @@ public static class AvPlayerExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
player.TextureAllocatorFailed = true;
|
player.TextureAllocatorFailed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1509,7 +999,6 @@ public static class AvPlayerExports
|
|||||||
for (var index = 0; index < player.GuestBuffers.Length; index++)
|
for (var index = 0; index < player.GuestBuffers.Length; index++)
|
||||||
{
|
{
|
||||||
player.GuestBuffers[index] = bufferBase + checked((ulong)(index * bufferSize));
|
player.GuestBuffers[index] = bufferBase + checked((ulong)(index * bufferSize));
|
||||||
RegisterVideoBuffer(player.GuestBuffers[index], bufferSize, index, "hle-fallback");
|
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine("[AVPLAYER][WARN] Guest texture allocator unavailable; using generic HLE memory.");
|
Console.Error.WriteLine("[AVPLAYER][WARN] Guest texture allocator unavailable; using generic HLE memory.");
|
||||||
return true;
|
return true;
|
||||||
@@ -1520,14 +1009,12 @@ public static class AvPlayerExports
|
|||||||
out int width,
|
out int width,
|
||||||
out int height,
|
out int height,
|
||||||
out double framesPerSecond,
|
out double framesPerSecond,
|
||||||
out ulong durationMilliseconds,
|
out ulong durationMilliseconds)
|
||||||
out bool hasAudio)
|
|
||||||
{
|
{
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
framesPerSecond = 30.0;
|
framesPerSecond = 30.0;
|
||||||
durationMilliseconds = 0;
|
durationMilliseconds = 0;
|
||||||
hasAudio = false;
|
|
||||||
|
|
||||||
if (!FfmpegMediaStream.TryProbe(path, out width, out height, out var rate, out var duration))
|
if (!FfmpegMediaStream.TryProbe(path, out width, out height, out var rate, out var duration))
|
||||||
{
|
{
|
||||||
@@ -1544,10 +1031,6 @@ public static class AvPlayerExports
|
|||||||
durationMilliseconds = checked((ulong)Math.Max(0, Math.Round(duration * 1000.0)));
|
durationMilliseconds = checked((ulong)Math.Max(0, Math.Round(duration * 1000.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAudio = FfmpegMediaStream.TryOpenAudio(path, out var audioStream) &&
|
|
||||||
audioStream is not null;
|
|
||||||
audioStream?.Dispose();
|
|
||||||
|
|
||||||
return width > 0 && height > 0 && framesPerSecond > 0;
|
return width > 0 && height > 0 && framesPerSecond > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1837,104 +1320,6 @@ public static class AvPlayerExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsValidBgraFrame(
|
|
||||||
ReadOnlySpan<byte> pixels,
|
|
||||||
uint width,
|
|
||||||
uint height)
|
|
||||||
{
|
|
||||||
if (width == 0 || height == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requiredBytes = (ulong)width * height * 4;
|
|
||||||
return requiredBytes <= int.MaxValue &&
|
|
||||||
pixels.Length >= checked((int)requiredBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsGen5Target(Generation generation) =>
|
|
||||||
(generation & Generation.Gen5) != 0;
|
|
||||||
|
|
||||||
internal static ulong GetAutoStartOffset(Generation generation, bool extended) =>
|
|
||||||
IsGen5Target(generation)
|
|
||||||
? extended ? 168UL : 112UL
|
|
||||||
: extended ? 164UL : 108UL;
|
|
||||||
|
|
||||||
internal static int GetLegacyStreamInfoSize(Generation generation) =>
|
|
||||||
IsGen5Target(generation)
|
|
||||||
? Gen5StreamInfoSize
|
|
||||||
: Gen4StreamInfoSize;
|
|
||||||
|
|
||||||
internal static uint GetStreamType(Generation generation, uint streamIndex) =>
|
|
||||||
IsGen5Target(generation)
|
|
||||||
? streamIndex + 1
|
|
||||||
: streamIndex;
|
|
||||||
|
|
||||||
internal static void WriteGen5StreamInfoEx(
|
|
||||||
Span<byte> info,
|
|
||||||
uint streamType,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
double framesPerSecond,
|
|
||||||
ulong durationMilliseconds)
|
|
||||||
{
|
|
||||||
if (info.Length < StreamInfoExSize)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Stream-info buffer must contain at least {StreamInfoExSize} bytes.",
|
|
||||||
nameof(info));
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0..], StreamInfoExSize);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[8..], streamType);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[16..], width);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[20..], height);
|
|
||||||
BinaryPrimitives.WriteDoubleLittleEndian(info[0x40..], framesPerSecond);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x60..], durationMilliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void WriteVideoFrameInfo(
|
|
||||||
Span<byte> info,
|
|
||||||
Generation generation,
|
|
||||||
bool extended,
|
|
||||||
ulong bufferAddress,
|
|
||||||
ulong timestamp,
|
|
||||||
uint width,
|
|
||||||
uint visibleWidth,
|
|
||||||
uint height,
|
|
||||||
uint pitch,
|
|
||||||
double framesPerSecond)
|
|
||||||
{
|
|
||||||
var requiredSize = extended ? FrameInfoExSize : FrameInfoSize;
|
|
||||||
if (info.Length < requiredSize)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Frame-info buffer must contain at least {requiredSize} bytes.",
|
|
||||||
nameof(info));
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0..], bufferAddress);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(info[16..], timestamp);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[24..], width);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[28..], height);
|
|
||||||
BinaryPrimitives.WriteSingleLittleEndian(info[32..], 1.0f);
|
|
||||||
if (!extended)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
info[48..],
|
|
||||||
width > visibleWidth ? width - visibleWidth : 0);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[60..], pitch);
|
|
||||||
info[64] = 8;
|
|
||||||
info[65] = 8;
|
|
||||||
if (IsGen5Target(generation))
|
|
||||||
{
|
|
||||||
BinaryPrimitives.WriteDoubleLittleEndian(info[0x48..], framesPerSecond);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
{
|
{
|
||||||
value = string.Empty;
|
value = string.Empty;
|
||||||
|
|||||||
@@ -1,567 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using FFmpeg.AutoGen;
|
|
||||||
using SharpEmu.Libs.VideoOut;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Codec;
|
|
||||||
|
|
||||||
/// <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.
|
|
||||||
///
|
|
||||||
/// Three-stage pipeline, none of it on the guest thread:
|
|
||||||
/// Decode() -> AU queue -> decode worker -> frame queue -> scheduler -> Submit
|
|
||||||
///
|
|
||||||
/// The scheduler paces presentation to the stream's own framerate (no PTS
|
|
||||||
/// is available) instead of draining as fast as it decodes. Neither worker
|
|
||||||
/// thread may write to guest memory directly (the guest's stack slot may
|
|
||||||
/// already be reused by the time they finish), so readiness is reported via
|
|
||||||
/// TryConsumeProtocolReadySignal (metadata only) while pixels go straight
|
|
||||||
/// to VulkanVideoPresenter.Submit from the scheduler thread.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed unsafe class Videodec2Decoder : IDisposable
|
|
||||||
{
|
|
||||||
// BGRA matches VulkanVideoPresenter.Submit; decode bypasses guest memory entirely.
|
|
||||||
private const AVPixelFormat OutputPixelFormat = AVPixelFormat.AV_PIX_FMT_BGRA;
|
|
||||||
|
|
||||||
// Enough lookahead to absorb decode jitter without adding visible latency.
|
|
||||||
private const int FrameQueueCapacity = 4;
|
|
||||||
|
|
||||||
// Fallback when the stream doesn't declare a usable framerate.
|
|
||||||
private const double FallbackFps = 30.0;
|
|
||||||
|
|
||||||
private static bool _rootPathInitialized;
|
|
||||||
private static readonly object InitGate = new();
|
|
||||||
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private AVCodecContext* _codecContext;
|
|
||||||
private AVFrame* _frame;
|
|
||||||
private AVPacket* _packet;
|
|
||||||
private SwsContext* _swsContext;
|
|
||||||
private int _swsSourceWidth;
|
|
||||||
private int _swsSourceHeight;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
SingleReader = true,
|
|
||||||
SingleWriter = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
|
||||||
private readonly Channel<(byte[] Bgra, uint Width, uint Height)> _frameQueue =
|
|
||||||
Channel.CreateBounded<(byte[], uint, uint)>(new BoundedChannelOptions(FrameQueueCapacity)
|
|
||||||
{
|
|
||||||
FullMode = BoundedChannelFullMode.Wait,
|
|
||||||
SingleReader = true,
|
|
||||||
SingleWriter = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
private readonly Thread _worker;
|
|
||||||
private readonly Thread _scheduler;
|
|
||||||
|
|
||||||
// Cancelled (not just completed) on Dispose so both loops stop promptly instead of draining a backlog.
|
|
||||||
private readonly CancellationTokenSource _workerCts = new();
|
|
||||||
|
|
||||||
private readonly object _protocolGate = new();
|
|
||||||
private long _producedCount;
|
|
||||||
private long _reportedCount;
|
|
||||||
private uint _lastWidth;
|
|
||||||
private uint _lastHeight;
|
|
||||||
|
|
||||||
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
|
|
||||||
{
|
|
||||||
_codecContext = codecContext;
|
|
||||||
_frame = frame;
|
|
||||||
_packet = packet;
|
|
||||||
_worker = new Thread(WorkerLoop)
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "SharpEmu Videodec2 Worker",
|
|
||||||
};
|
|
||||||
_scheduler = new Thread(SchedulerLoop)
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "SharpEmu Videodec2 Scheduler",
|
|
||||||
};
|
|
||||||
_worker.Start();
|
|
||||||
_scheduler.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Opens a new H.264 session, or null if FFmpeg is unavailable or the decoder couldn't open.</summary>
|
|
||||||
public static Videodec2Decoder? TryCreate()
|
|
||||||
{
|
|
||||||
EnsureRootPathInitialized();
|
|
||||||
|
|
||||||
AVCodecContext* codecContext = null;
|
|
||||||
AVFrame* frame = null;
|
|
||||||
AVPacket* packet = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
|
|
||||||
if (codec == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
codecContext = ffmpeg.avcodec_alloc_context3(codec);
|
|
||||||
if (codecContext == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
|
|
||||||
{
|
|
||||||
ffmpeg.avcodec_free_context(&codecContext);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame = ffmpeg.av_frame_alloc();
|
|
||||||
packet = ffmpeg.av_packet_alloc();
|
|
||||||
if (frame == null || packet == null)
|
|
||||||
{
|
|
||||||
if (frame != null)
|
|
||||||
{
|
|
||||||
ffmpeg.av_frame_free(&frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packet != null)
|
|
||||||
{
|
|
||||||
ffmpeg.av_packet_free(&packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
ffmpeg.avcodec_free_context(&codecContext);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Videodec2Decoder(codecContext, frame, packet);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
|
|
||||||
{
|
|
||||||
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
|
|
||||||
if (codecContext != null)
|
|
||||||
{
|
|
||||||
ffmpeg.avcodec_free_context(&codecContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureRootPathInitialized()
|
|
||||||
{
|
|
||||||
if (_rootPathInitialized)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (InitGate)
|
|
||||||
{
|
|
||||||
if (_rootPathInitialized)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_rootPathInitialized = true;
|
|
||||||
// Must be set before any ffmpeg.* call, or bindings resolve against the empty default RootPath.
|
|
||||||
ffmpeg.RootPath = Path.Combine(AppContext.BaseDirectory, "plugins");
|
|
||||||
DynamicallyLoadedBindings.Initialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
|
||||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
|
||||||
{
|
|
||||||
_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()
|
|
||||||
{
|
|
||||||
_workChannel.Writer.TryWrite(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
|
|
||||||
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
|
|
||||||
{
|
|
||||||
lock (_protocolGate)
|
|
||||||
{
|
|
||||||
if (_reportedCount >= _producedCount)
|
|
||||||
{
|
|
||||||
width = 0;
|
|
||||||
height = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_reportedCount++;
|
|
||||||
width = _lastWidth;
|
|
||||||
height = _lastHeight;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WorkerLoop()
|
|
||||||
{
|
|
||||||
var reader = _workChannel.Reader;
|
|
||||||
var token = _workerCts.Token;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
byte[]? item;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!reader.WaitToReadAsync(token).AsTask().GetAwaiter().GetResult())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reader.TryRead(out item))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ChannelClosedException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!decodedOk || !hasPicture || bgraFrame is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
|
|
||||||
_frameQueue.Writer.WriteAsync((bgraFrame, width, height), token).AsTask().GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (ChannelClosedException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_protocolGate)
|
|
||||||
{
|
|
||||||
_producedCount++;
|
|
||||||
_lastWidth = width;
|
|
||||||
_lastHeight = height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SchedulerLoop()
|
|
||||||
{
|
|
||||||
var reader = _frameQueue.Reader;
|
|
||||||
var token = _workerCts.Token;
|
|
||||||
var haveDeadline = false;
|
|
||||||
var nextDeadline = DateTime.MinValue;
|
|
||||||
var frameInterval = TimeSpan.FromSeconds(1.0 / FallbackFps);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
(byte[] Bgra, uint Width, uint Height) item;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!reader.WaitToReadAsync(token).AsTask().GetAwaiter().GetResult())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reader.TryRead(out item))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ChannelClosedException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveDeadline)
|
|
||||||
{
|
|
||||||
// Framerate isn't known until FFmpeg parses the first frame's SPS/VUI.
|
|
||||||
var rate = _codecContext->framerate;
|
|
||||||
var fps = rate.den > 0 && rate.num > 0
|
|
||||||
? (double)rate.num / rate.den
|
|
||||||
: FallbackFps;
|
|
||||||
frameInterval = TimeSpan.FromSeconds(1.0 / fps);
|
|
||||||
nextDeadline = DateTime.UtcNow;
|
|
||||||
haveDeadline = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
if (nextDeadline > now)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Task.Delay(nextDeadline - now, token).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VulkanVideoPresenter.Submit(item.Bgra, item.Width, item.Height);
|
|
||||||
nextDeadline += frameInterval;
|
|
||||||
|
|
||||||
// Resync to "now" if we fell behind, instead of burning through a deadline backlog unpaced.
|
|
||||||
if (nextDeadline < DateTime.UtcNow)
|
|
||||||
{
|
|
||||||
nextDeadline = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Feeds one access unit and converts the resulting picture to BGRA, if any. Decode-worker thread only.</summary>
|
|
||||||
private bool DecodeCoreLocked(
|
|
||||||
byte[] accessUnit,
|
|
||||||
out byte[]? bgraFrame,
|
|
||||||
out bool hasPicture,
|
|
||||||
out uint width,
|
|
||||||
out uint height)
|
|
||||||
{
|
|
||||||
bgraFrame = null;
|
|
||||||
hasPicture = false;
|
|
||||||
width = 0;
|
|
||||||
height = 0;
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ffmpeg.av_packet_unref(_packet);
|
|
||||||
var buffer = ffmpeg.av_malloc((nuint)accessUnit.Length + (nuint)ffmpeg.AV_INPUT_BUFFER_PADDING_SIZE);
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed (byte* source = accessUnit)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(source, buffer, accessUnit.Length, accessUnit.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
new Span<byte>((byte*)buffer + accessUnit.Length, ffmpeg.AV_INPUT_BUFFER_PADDING_SIZE).Clear();
|
|
||||||
|
|
||||||
_packet->data = (byte*)buffer;
|
|
||||||
_packet->size = accessUnit.Length;
|
|
||||||
|
|
||||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
|
||||||
ffmpeg.av_freep(&buffer);
|
|
||||||
_packet->data = null;
|
|
||||||
_packet->size = 0;
|
|
||||||
if (sendResult < 0 && sendResult != ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
|
||||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || receiveResult == ffmpeg.AVERROR_EOF)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiveResult < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bgraFrame = ConvertFrameToBgraLocked(out width, out height);
|
|
||||||
if (bgraFrame == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPicture = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ffmpeg.av_frame_unref(_frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Signals end-of-stream and pulls one remaining buffered frame, if any. Decode-worker thread only.</summary>
|
|
||||||
private bool DrainCoreLocked(out byte[]? bgraFrame, out bool hasPicture, out uint width, out uint height)
|
|
||||||
{
|
|
||||||
bgraFrame = null;
|
|
||||||
hasPicture = false;
|
|
||||||
width = 0;
|
|
||||||
height = 0;
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, null);
|
|
||||||
if (sendResult < 0 && sendResult != ffmpeg.AVERROR_EOF)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
|
||||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || receiveResult == ffmpeg.AVERROR_EOF)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiveResult < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bgraFrame = ConvertFrameToBgraLocked(out width, out height);
|
|
||||||
if (bgraFrame == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPicture = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ffmpeg.av_frame_unref(_frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Converts <see cref="_frame"/> to a tightly packed width*height*4 BGRA buffer, or null on failure.</summary>
|
|
||||||
private byte[]? ConvertFrameToBgraLocked(out uint width, out uint height)
|
|
||||||
{
|
|
||||||
width = (uint)_frame->width;
|
|
||||||
height = (uint)_frame->height;
|
|
||||||
var sourceFormat = (AVPixelFormat)_frame->format;
|
|
||||||
|
|
||||||
if (_swsContext == null ||
|
|
||||||
_swsSourceWidth != _frame->width ||
|
|
||||||
_swsSourceHeight != _frame->height ||
|
|
||||||
_swsSourceFormat != sourceFormat)
|
|
||||||
{
|
|
||||||
if (_swsContext != null)
|
|
||||||
{
|
|
||||||
ffmpeg.sws_freeContext(_swsContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
_swsContext = ffmpeg.sws_getContext(
|
|
||||||
_frame->width, _frame->height, sourceFormat,
|
|
||||||
_frame->width, _frame->height, OutputPixelFormat,
|
|
||||||
ffmpeg.SWS_BILINEAR, null, null, null);
|
|
||||||
if (_swsContext == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_swsSourceWidth = _frame->width;
|
|
||||||
_swsSourceHeight = _frame->height;
|
|
||||||
_swsSourceFormat = sourceFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bgraFrame = new byte[checked((int)(width * height * 4))];
|
|
||||||
fixed (byte* destinationPtr = bgraFrame)
|
|
||||||
{
|
|
||||||
var dstData = new byte_ptrArray4();
|
|
||||||
var dstLinesize = new int_array4();
|
|
||||||
ffmpeg.av_image_fill_arrays(
|
|
||||||
ref dstData, ref dstLinesize, destinationPtr,
|
|
||||||
OutputPixelFormat, _frame->width, _frame->height, 1);
|
|
||||||
|
|
||||||
var srcData = new byte_ptrArray8();
|
|
||||||
var srcLinesize = new int_array8();
|
|
||||||
for (var i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
srcData[(uint)i] = _frame->data[(uint)i];
|
|
||||||
srcLinesize[(uint)i] = _frame->linesize[(uint)i];
|
|
||||||
}
|
|
||||||
|
|
||||||
ffmpeg.sws_scale(
|
|
||||||
_swsContext, srcData, srcLinesize, 0, _frame->height,
|
|
||||||
dstData, dstLinesize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bgraFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outside _gate: the worker needs it to finish whatever item it's mid-call on.
|
|
||||||
_workerCts.Cancel();
|
|
||||||
_workChannel.Writer.TryComplete();
|
|
||||||
_frameQueue.Writer.TryComplete();
|
|
||||||
_worker.Join(TimeSpan.FromSeconds(2));
|
|
||||||
_scheduler.Join(TimeSpan.FromSeconds(2));
|
|
||||||
_workerCts.Dispose();
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_swsContext != null)
|
|
||||||
{
|
|
||||||
ffmpeg.sws_freeContext(_swsContext);
|
|
||||||
_swsContext = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_packet != null)
|
|
||||||
{
|
|
||||||
var packet = _packet;
|
|
||||||
ffmpeg.av_packet_free(&packet);
|
|
||||||
_packet = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_frame != null)
|
|
||||||
{
|
|
||||||
var frame = _frame;
|
|
||||||
ffmpeg.av_frame_free(&frame);
|
|
||||||
_frame = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_codecContext != null)
|
|
||||||
{
|
|
||||||
var codecContext = _codecContext;
|
|
||||||
ffmpeg.avcodec_free_context(&codecContext);
|
|
||||||
_codecContext = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Codec;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// libSceVideodec2 (hardware compute-based decoder). sceVideodec2Decode
|
|
||||||
/// feeds a real FFmpeg H.264 session (Videodec2Decoder) when one can be
|
|
||||||
/// opened, falling back to the original "no picture" stub otherwise.
|
|
||||||
/// </summary>
|
|
||||||
public static class Videodec2Exports
|
|
||||||
{
|
|
||||||
private const int Ok = 0;
|
|
||||||
|
|
||||||
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
|
|
||||||
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
|
|
||||||
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "RnDibcGCPKw",
|
|
||||||
ExportName = "sceVideodec2QueryComputeMemoryInfo",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (paramAddress == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success needs no memory writes; the game initializes from its own fields.
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var queueAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "qqMCwlULR+E",
|
|
||||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
private const ulong DecoderToken = 0x56D2_C0DE_0002UL;
|
|
||||||
|
|
||||||
// Handle is opaque to the game; a monotonic counter seeded at the old fixed token.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "CNNRoRYd8XI",
|
|
||||||
ExportName = "sceVideodec2CreateDecoder",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2CreateDecoder(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var decoderAddress = ctx[CpuRegister.Rdx];
|
|
||||||
if (decoderAddress == 0)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
|
||||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
|
||||||
{
|
|
||||||
Decoders.TryRemove(handle, out var created);
|
|
||||||
created?.Dispose();
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "l1hXwscLuCY",
|
|
||||||
ExportName = "sceVideodec2Flush",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2Flush(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = ctx[CpuRegister.Rdi];
|
|
||||||
var outputInfoAddress = ctx[CpuRegister.Rdx];
|
|
||||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No state to reset.
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "wJXikG6QFN8",
|
|
||||||
ExportName = "sceVideodec2Reset",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2Reset(CpuContext ctx)
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "jwImxXRGSKA",
|
|
||||||
ExportName = "sceVideodec2DeleteDecoder",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2DeleteDecoder(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = ctx[CpuRegister.Rdi];
|
|
||||||
if (Decoders.TryRemove(handle, out var decoder))
|
|
||||||
{
|
|
||||||
decoder?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceVideodec2")]
|
|
||||||
public static int Videodec2Decode(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = ctx[CpuRegister.Rdi];
|
|
||||||
var inputAuStruct = ctx[CpuRegister.Rsi];
|
|
||||||
var outputSlotObj = ctx[CpuRegister.Rdx];
|
|
||||||
var outputInfoAddress = ctx[CpuRegister.Rcx];
|
|
||||||
|
|
||||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
|
|
||||||
{
|
|
||||||
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputAuStruct == 0 ||
|
|
||||||
!ctx.TryReadUInt64(inputAuStruct + 0x08, out var auDataPtr) ||
|
|
||||||
!ctx.TryReadUInt64(inputAuStruct + 0x10, out var auDataSize) ||
|
|
||||||
auDataPtr == 0 || auDataSize == 0 || auDataSize > MaxPlausibleAuBytes ||
|
|
||||||
outputSlotObj == 0 ||
|
|
||||||
!ctx.TryReadUInt64(outputSlotObj + 0x08, out var slotPtr) ||
|
|
||||||
!ctx.TryReadUInt64(outputSlotObj + 0x10, out var slotSize) ||
|
|
||||||
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
|
|
||||||
{
|
|
||||||
// Nothing sane to feed/fill this call; not an error.
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
var auBuffer = new byte[auDataSize];
|
|
||||||
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
|
||||||
decoder.EnqueueAccessUnit(auBuffer);
|
|
||||||
|
|
||||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
|
||||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
|
||||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetReturn(ctx, Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly byte[] NoPicture = [0];
|
|
||||||
private static readonly byte[] PictureReady = [1];
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,10 +23,6 @@ internal static class GuestDataPool
|
|||||||
|
|
||||||
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
|
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
|
||||||
|
|
||||||
/// <summary>Outstanding lease count and idle cached bytes, for leak diagnostics.</summary>
|
|
||||||
public static (int LeaseCount, ulong CachedBytes) DiagnosticStats() =>
|
|
||||||
((BoundedByteArrayPool)Shared).Stats();
|
|
||||||
|
|
||||||
private sealed class BoundedByteArrayPool : ArrayPool<byte>
|
private sealed class BoundedByteArrayPool : ArrayPool<byte>
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
@@ -123,14 +119,6 @@ internal static class GuestDataPool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public (int LeaseCount, ulong CachedBytes) Stats()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
return (_leases.Count, _cachedBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetAllocationLength(int minimumLength)
|
private int GetAllocationLength(int minimumLength)
|
||||||
{
|
{
|
||||||
if (minimumLength <= 16)
|
if (minimumLength <= 16)
|
||||||
|
|||||||
@@ -1070,29 +1070,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GPU interrupt events must not coalesce: the AGC driver's
|
QueueOrUpdateEvent(
|
||||||
// interrupt thread accounts exactly one completion per
|
queue,
|
||||||
// delivered kevent (it never reads the kevent payload), so
|
new KernelQueuedEvent(
|
||||||
// merging N triggers into one pending entry silently drops
|
registration.Ident,
|
||||||
// N-1 completions and wedges its dependency counters. Queue
|
registration.Filter,
|
||||||
// a distinct entry per trigger, with a defensive cap so an
|
registration.Flags,
|
||||||
// undrained queue cannot grow without bound.
|
1,
|
||||||
var queuedEvent = new KernelQueuedEvent(
|
data,
|
||||||
registration.Ident,
|
registration.UserData));
|
||||||
registration.Filter,
|
|
||||||
registration.Flags,
|
|
||||||
1,
|
|
||||||
data,
|
|
||||||
registration.UserData);
|
|
||||||
if (CountPendingEvents(queue, registration.Ident, registration.Filter) < 256)
|
|
||||||
{
|
|
||||||
queue.AddLast(queuedEvent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
QueueOrUpdateEvent(queue, queuedEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
(wakeQueues ??= []).Add(state);
|
(wakeQueues ??= []).Add(state);
|
||||||
triggeredCount++;
|
triggeredCount++;
|
||||||
|
|
||||||
@@ -1318,24 +1304,6 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountPendingEvents(
|
|
||||||
KernelEventDeque queue,
|
|
||||||
ulong ident,
|
|
||||||
short filter)
|
|
||||||
{
|
|
||||||
var count = 0;
|
|
||||||
for (var i = 0; i < queue.Count; i++)
|
|
||||||
{
|
|
||||||
var pending = queue[i];
|
|
||||||
if (pending.Ident == ident && pending.Filter == filter)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void QueueOrUpdateEvent(
|
private static void QueueOrUpdateEvent(
|
||||||
KernelEventDeque queue,
|
KernelEventDeque queue,
|
||||||
KernelQueuedEvent queuedEvent)
|
KernelQueuedEvent queuedEvent)
|
||||||
|
|||||||
@@ -466,45 +466,4 @@ public static class KernelExports
|
|||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "tU5e3f9gSiU",
|
|
||||||
ExportName = "sceKernelIsTrinityMode",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelIsTrinityMode(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "DLORcroUqbc",
|
|
||||||
ExportName = "sceKernelGetOpenPsId",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int KernelGetOpenPsId(CpuContext ctx)
|
|
||||||
{
|
|
||||||
ulong bufferPtr = ctx[CpuRegister.Rdi];
|
|
||||||
|
|
||||||
if (bufferPtr == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> openPsId = stackalloc byte[16];
|
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(bufferPtr, openPsId))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,15 +99,6 @@ public static class NpTrophy2Exports
|
|||||||
public static int NpTrophy2GetTrophyInfo(CpuContext ctx) =>
|
public static int NpTrophy2GetTrophyInfo(CpuContext ctx) =>
|
||||||
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "y3zHpdZO6ME",
|
|
||||||
ExportName = "sceNpTrophy2GetTrophyInfoArray",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpTrophy2")]
|
|
||||||
public static int NpTrophy2GetTrophyInfoArray(CpuContext ctx) =>
|
|
||||||
SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
|
||||||
|
|
||||||
|
|
||||||
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||||
{
|
{
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||||
<!-- SharpEmu.Core's stall watchdog reads GpuWaitRegistry for diagnostics. -->
|
|
||||||
<InternalsVisibleTo Include="SharpEmu.Core" />
|
<InternalsVisibleTo Include="SharpEmu.Core" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.VideoOut;
|
|
||||||
|
|
||||||
internal static class GuestImageUploadPayloadDiagnostics
|
|
||||||
{
|
|
||||||
internal static (long NonzeroBytes, ulong Hash) Summarize(ReadOnlySpan<byte> pixels)
|
|
||||||
{
|
|
||||||
const ulong offsetBasis = 14695981039346656037UL;
|
|
||||||
const ulong prime = 1099511628211UL;
|
|
||||||
|
|
||||||
var nonzeroBytes = 0L;
|
|
||||||
var hash = offsetBasis;
|
|
||||||
foreach (var value in pixels)
|
|
||||||
{
|
|
||||||
nonzeroBytes += value == 0 ? 0 : 1;
|
|
||||||
hash = (hash ^ value) * prime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (nonzeroBytes, hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,14 +59,9 @@ public sealed record HostVideoOptions
|
|||||||
|
|
||||||
public static class HostVideoHost
|
public static class HostVideoHost
|
||||||
{
|
{
|
||||||
private static HostVideoOptions _currentOptions = HostVideoOptions.Default;
|
|
||||||
|
|
||||||
public static HostVideoOptions CurrentOptions => Volatile.Read(ref _currentOptions);
|
|
||||||
|
|
||||||
public static bool TryConfigureVideo(HostVideoOptions options)
|
public static bool TryConfigureVideo(HostVideoOptions options)
|
||||||
{
|
{
|
||||||
var normalized = options.Normalize();
|
var normalized = options.Normalize();
|
||||||
Volatile.Write(ref _currentOptions, normalized);
|
|
||||||
return VulkanVideoPresenter.TryConfigureVideo(normalized) &
|
return VulkanVideoPresenter.TryConfigureVideo(normalized) &
|
||||||
MetalVideoPresenter.TryConfigureVideo(normalized);
|
MetalVideoPresenter.TryConfigureVideo(normalized);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -729,14 +729,6 @@ public static class VideoOutExports
|
|||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, 0);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, 0);
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||||
// Ghost of Yotei polls a flag past the classic 0x28-byte struct and
|
|
||||||
// spins on sceKernelUsleep(1) while it's nonzero; the caller never
|
|
||||||
// pre-zeroes that stack buffer, so an untouched field reads back as
|
|
||||||
// garbage. Flips complete synchronously in this emulator (see
|
|
||||||
// SubmitFlip/sceVideoOutIsFlipPending, always not-pending), so the
|
|
||||||
// extended region must read zero here too.
|
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x28, 0);
|
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x30, 0);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,14 +1114,7 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
if (category > 1 || option != 0)
|
if (category > 1 || option != 0)
|
||||||
{
|
{
|
||||||
// Ghost of Yotei registers its display buffers with a nonzero
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
// category/option pair; rejecting the registration guarantees the
|
|
||||||
// title can never flip. Treat unknown categories as the standard
|
|
||||||
// uncompressed layout instead of failing the whole registration.
|
|
||||||
TraceVideoOut(
|
|
||||||
$"register_buffers2 nonstandard category=0x{categoryRaw:X} " +
|
|
||||||
$"option=0x{option:X} handle={handle} set={setIndex} " +
|
|
||||||
$"start={bufferIndexStart} count={bufferNum}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
||||||
@@ -1315,12 +1300,10 @@ public static class VideoOutExports
|
|||||||
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
||||||
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
||||||
var (draws, drawMs, pipelines, spirvCompiles) = GuestGpu.Current.ReadAndResetPerfCounters();
|
var (draws, drawMs, pipelines, spirvCompiles) = GuestGpu.Current.ReadAndResetPerfCounters();
|
||||||
var (poolLeases, poolCachedBytes) = SharpEmu.Libs.Gpu.GuestDataPool.DiagnosticStats();
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
||||||
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
|
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
|
||||||
$"draws={draws} draw_ms={drawMs:F0} pipelines={pipelines} spirv={spirvCompiles} " +
|
$"draws={draws} draw_ms={drawMs:F0} pipelines={pipelines} spirv={spirvCompiles}");
|
||||||
$"pool_leases={poolLeases} pool_cached_mb={poolCachedBytes / 1024.0 / 1024.0:F1}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly bool _flipPacingDisabled = string.Equals(
|
private static readonly bool _flipPacingDisabled = string.Equals(
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Silk.NET.Core.Native;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Agc;
|
using SharpEmu.Libs.Agc;
|
||||||
using SharpEmu.Libs.AvPlayer;
|
|
||||||
using SharpEmu.Libs.Media;
|
using SharpEmu.Libs.Media;
|
||||||
using SharpEmu.Libs.Gpu;
|
using SharpEmu.Libs.Gpu;
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
@@ -526,9 +525,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// render thread reaches the previous image, which otherwise starves
|
// render thread reaches the previous image, which otherwise starves
|
||||||
// presentation indefinitely.
|
// presentation indefinitely.
|
||||||
private static readonly Queue<Presentation> _pendingGuestImagePresentations = new();
|
private static readonly Queue<Presentation> _pendingGuestImagePresentations = new();
|
||||||
// Same fix as _pendingGuestImagePresentations above, for Submit()'s decoded video
|
|
||||||
// frames: a single "latest wins" slot dropped frames the render loop didn't poll in time.
|
|
||||||
private static readonly Queue<Presentation> _pendingVideoPresentations = new();
|
|
||||||
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
||||||
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
||||||
// Write-tracker generation last uploaded for a CPU-backed guest image.
|
// Write-tracker generation last uploaded for a CPU-backed guest image.
|
||||||
@@ -808,7 +804,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_pendingSyncGuestWorkCount = 0;
|
_pendingSyncGuestWorkCount = 0;
|
||||||
_pendingGuestWorkBytes = 0;
|
_pendingGuestWorkBytes = 0;
|
||||||
_pendingGuestImagePresentations.Clear();
|
_pendingGuestImagePresentations.Clear();
|
||||||
_pendingVideoPresentations.Clear();
|
|
||||||
_guestImageWorkSequences.Clear();
|
_guestImageWorkSequences.Clear();
|
||||||
_availableGuestImages.Clear();
|
_availableGuestImages.Clear();
|
||||||
_cpuBackedUploadGenerations.Clear();
|
_cpuBackedUploadGenerations.Clear();
|
||||||
@@ -864,7 +859,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
|
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
|
||||||
var presentation = new Presentation(
|
_latestPresentation = new Presentation(
|
||||||
bgraFrame,
|
bgraFrame,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -873,15 +868,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: 0,
|
RequiredGuestWorkSequence: 0,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
|
||||||
// Also dual-written to _latestPresentation as a fallback once the queue drains.
|
|
||||||
_pendingVideoPresentations.Enqueue(presentation);
|
|
||||||
while (_pendingVideoPresentations.Count > MaxPendingGuestFlipVersions)
|
|
||||||
{
|
|
||||||
_pendingVideoPresentations.Dequeue();
|
|
||||||
}
|
|
||||||
|
|
||||||
_latestPresentation = presentation;
|
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -2440,7 +2426,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (IsGuestWorkCompletedLocked(pending.RequiredGuestWorkSequence))
|
if (IsGuestWorkCompletedLocked(pending.RequiredGuestWorkSequence))
|
||||||
{
|
{
|
||||||
presentation = _pendingGuestImagePresentations.Dequeue();
|
presentation = _pendingGuestImagePresentations.Dequeue();
|
||||||
TryReplaceWithHostMovieFrame(ref presentation);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2448,19 +2433,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video's RequiredGuestWorkSequence is always 0, so this never blocks like the guest-image queue can.
|
|
||||||
while (_pendingVideoPresentations.Count > 0 &&
|
|
||||||
_pendingVideoPresentations.Peek().Sequence <= presentedSequence)
|
|
||||||
{
|
|
||||||
_pendingVideoPresentations.Dequeue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pendingVideoPresentations.Count > 0)
|
|
||||||
{
|
|
||||||
presentation = _pendingVideoPresentations.Dequeue();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_latestPresentation is not { } latest ||
|
if (_latestPresentation is not { } latest ||
|
||||||
latest.Sequence == presentedSequence ||
|
latest.Sequence == presentedSequence ||
|
||||||
!IsGuestWorkCompletedLocked(latest.RequiredGuestWorkSequence))
|
!IsGuestWorkCompletedLocked(latest.RequiredGuestWorkSequence))
|
||||||
@@ -2486,97 +2458,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
presentation = latest;
|
presentation = latest;
|
||||||
TryReplaceWithHostMovieFrame(ref presentation);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AvPlayer titles whose guest texture allocators reject the decoded movie
|
|
||||||
/// surface have no sampled image to draw, so the movie would never become
|
|
||||||
/// visible. In that case the AvPlayer HLE keeps a host-decoded BGRA frame
|
|
||||||
/// available; substitute it for the guest image the title is flipping.
|
|
||||||
/// </summary>
|
|
||||||
private static void TryReplaceWithHostMovieFrame(ref Presentation presentation)
|
|
||||||
{
|
|
||||||
if (!TryTakeHostMovieFrame(out var pixels, out var width, out var height))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
presentation = new Presentation(
|
|
||||||
pixels,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
presentation.Sequence,
|
|
||||||
GuestDrawKind.None,
|
|
||||||
TranslatedDraw: null,
|
|
||||||
presentation.RequiredGuestWorkSequence,
|
|
||||||
IsSplash: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The movie is decoded on the host clock, so it must not be limited to the
|
|
||||||
/// title's flip rate: emulated flips are far slower than 59.94 Hz, which
|
|
||||||
/// would turn the intro into a slideshow. The render loop uses this on the
|
|
||||||
/// ticks where the guest produced no new flip, keeping the same presented
|
|
||||||
/// sequence so guest presentation bookkeeping is untouched.
|
|
||||||
/// </summary>
|
|
||||||
private static bool TryTakeHostMovieOnlyPresentation(
|
|
||||||
long presentedSequence,
|
|
||||||
out Presentation presentation)
|
|
||||||
{
|
|
||||||
if (!TryTakeHostMovieFrame(out var pixels, out var width, out var height))
|
|
||||||
{
|
|
||||||
presentation = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
presentation = new Presentation(
|
|
||||||
pixels,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
presentedSequence,
|
|
||||||
GuestDrawKind.None,
|
|
||||||
TranslatedDraw: null,
|
|
||||||
RequiredGuestWorkSequence: 0,
|
|
||||||
IsSplash: false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryTakeHostMovieFrame(
|
|
||||||
out byte[] pixels,
|
|
||||||
out uint width,
|
|
||||||
out uint height)
|
|
||||||
{
|
|
||||||
if (!AvPlayerExports.TryGetFallbackPresentationFrame(
|
|
||||||
out pixels,
|
|
||||||
out width,
|
|
||||||
out height,
|
|
||||||
out var serial))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Interlocked.Exchange(
|
|
||||||
ref _tracedAvPlayerFallbackPresentationSerial,
|
|
||||||
serial) != serial)
|
|
||||||
{
|
|
||||||
var frameCount = Interlocked.Increment(
|
|
||||||
ref _avPlayerFallbackPresentationCount);
|
|
||||||
if (frameCount <= 4 || frameCount % 30 == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[VIDEOOUT][INFO] AvPlayer host fallback frame presented: " +
|
|
||||||
$"frame={frameCount} serial={serial} size={width}x{height}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long _tracedAvPlayerFallbackPresentationSerial;
|
|
||||||
private static long _avPlayerFallbackPresentationCount;
|
|
||||||
private static readonly HashSet<long> _tracedGuestImagePresentRejections = new();
|
private static readonly HashSet<long> _tracedGuestImagePresentRejections = new();
|
||||||
|
|
||||||
private static bool HasPendingGuestPresentation(long presentedSequence)
|
private static bool HasPendingGuestPresentation(long presentedSequence)
|
||||||
@@ -10294,12 +10179,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
|
||||||
if (guestBuffer.BaseAddress > ulong.MaxValue - size)
|
var endAddress = checked(guestBuffer.BaseAddress + size);
|
||||||
{
|
|
||||||
return CreateTransientGlobalBufferResource(guestBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
var endAddress = guestBuffer.BaseAddress + size;
|
|
||||||
GuestBufferAllocation? allocation = null;
|
GuestBufferAllocation? allocation = null;
|
||||||
foreach (var candidate in _guestBufferAllocations)
|
foreach (var candidate in _guestBufferAllocations)
|
||||||
{
|
{
|
||||||
@@ -10534,14 +10414,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var size = (ulong)Math.Max(buffer.Length, sizeof(uint));
|
var size = (ulong)Math.Max(buffer.Length, sizeof(uint));
|
||||||
if (buffer.BaseAddress > ulong.MaxValue - size - 3)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var alignedStart = buffer.BaseAddress &
|
var alignedStart = buffer.BaseAddress &
|
||||||
~(GuestStorageBufferOffsetAlignment - 1);
|
~(GuestStorageBufferOffsetAlignment - 1);
|
||||||
var paddedEnd = (buffer.BaseAddress + size + 3) & ~3UL;
|
var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL;
|
||||||
ranges.Add((
|
ranges.Add((
|
||||||
alignedStart,
|
alignedStart,
|
||||||
paddedEnd));
|
paddedEnd));
|
||||||
@@ -15682,12 +15557,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
tookPresentation = TryTakePresentation(_presentedSequence, out presentation);
|
tookPresentation = TryTakePresentation(_presentedSequence, out presentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tookPresentation &&
|
|
||||||
TryTakeHostMovieOnlyPresentation(_presentedSequence, out presentation))
|
|
||||||
{
|
|
||||||
tookPresentation = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tookPresentation)
|
if (!tookPresentation)
|
||||||
{
|
{
|
||||||
// A render-loop tick with no newer flip is normal. Warn only when
|
// A render-loop tick with no newer flip is normal. Warn only when
|
||||||
|
|||||||
@@ -101,12 +101,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instruction.Opcode is "VMovrelsB32" or "VMovreldB32" or
|
|
||||||
"VMovrelsdB32" or "VMovrelsd2B32")
|
|
||||||
{
|
|
||||||
return TryEmitMoveRelative(instruction, destination, out error);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint result;
|
uint result;
|
||||||
switch (instruction.Opcode)
|
switch (instruction.Opcode)
|
||||||
{
|
{
|
||||||
@@ -1019,76 +1013,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// V_MOVREL*_B32: register-relative moves. M0 is added at run time to the
|
|
||||||
// source and/or destination register number encoded in the instruction,
|
|
||||||
// which is how shader compilers implement a dynamically indexed array
|
|
||||||
// that stayed in registers instead of being spilled to memory. Astro Bot
|
|
||||||
// ships pixel shaders that index a small register-resident table this
|
|
||||||
// way; without this the whole shader fails to translate.
|
|
||||||
//
|
|
||||||
// V_MOVRELS_B32 vdst = vgpr[src0 + M0]
|
|
||||||
// V_MOVRELD_B32 vgpr[vdst + M0] = src0
|
|
||||||
// V_MOVRELSD_B32 vgpr[vdst + M0] = vgpr[src0 + M0]
|
|
||||||
// V_MOVRELSD_2_B32 vgpr[vdst + M0[25:16]] = vgpr[src0 + M0[9:0]]
|
|
||||||
//
|
|
||||||
// The relative forms address the VGPR file relative to the wave's own
|
|
||||||
// allocation base, which is exactly what the private register array
|
|
||||||
// models, so the encoded number and M0 simply add.
|
|
||||||
private bool TryEmitMoveRelative(
|
|
||||||
Gen5ShaderInstruction instruction,
|
|
||||||
uint destination,
|
|
||||||
out string error)
|
|
||||||
{
|
|
||||||
error = string.Empty;
|
|
||||||
if (instruction.Sources.Count == 0)
|
|
||||||
{
|
|
||||||
error = $"missing source for {instruction.Opcode}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var m0 = LoadS(M0ScalarRegister);
|
|
||||||
uint sourceOffset;
|
|
||||||
uint destinationOffset;
|
|
||||||
if (instruction.Opcode == "VMovrelsd2B32")
|
|
||||||
{
|
|
||||||
sourceOffset = BitwiseAnd(m0, UInt(0x3FF));
|
|
||||||
destinationOffset = BitwiseAnd(ShiftRightLogical(m0, UInt(16)), UInt(0x3FF));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sourceOffset = m0;
|
|
||||||
destinationOffset = m0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint value;
|
|
||||||
if (instruction.Opcode == "VMovreldB32")
|
|
||||||
{
|
|
||||||
// Only the destination is relative here; src0 is an ordinary
|
|
||||||
// operand and may be an SGPR or an inline/literal constant.
|
|
||||||
value = GetRawSource(instruction, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var source = instruction.Sources[0];
|
|
||||||
if (source.Kind != Gen5OperandKind.VectorRegister)
|
|
||||||
{
|
|
||||||
error = $"{instruction.Opcode} source must be a vector register";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = LoadVDynamic(IAdd(UInt(source.Value), sourceOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.Opcode == "VMovrelsB32")
|
|
||||||
{
|
|
||||||
StoreV(destination, value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
StoreVDynamic(IAdd(UInt(destination), destinationOffset), value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packed f16 (VOP3P) arithmetic. Each source register holds two f16 values,
|
// Packed f16 (VOP3P) arithmetic. Each source register holds two f16 values,
|
||||||
// one per result lane. Every f16<->f32 conversion is done with the explicit
|
// one per result lane. Every f16<->f32 conversion is done with the explicit
|
||||||
// integer sequences below (EmitHalfToFloat / EmitFloatToHalf) instead of
|
// integer sequences below (EmitHalfToFloat / EmitFloatToHalf) instead of
|
||||||
|
|||||||
@@ -176,11 +176,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
private const uint ImageDescriptorDwords = 8;
|
private const uint ImageDescriptorDwords = 8;
|
||||||
private const uint SamplerDescriptorDwords = 4;
|
private const uint SamplerDescriptorDwords = 4;
|
||||||
private const int ScalarRegisterCount = 128;
|
private const int ScalarRegisterCount = 128;
|
||||||
|
|
||||||
// M0. Used as the runtime index added to the register numbers encoded in
|
|
||||||
// the V_MOVREL* instructions, and as the LDS/GDS base elsewhere.
|
|
||||||
private const uint M0ScalarRegister = 124;
|
|
||||||
|
|
||||||
private const long InitialScalarDefinition = -1;
|
private const long InitialScalarDefinition = -1;
|
||||||
private const long ConflictingScalarDefinition = -2;
|
private const long ConflictingScalarDefinition = -2;
|
||||||
private const long UnreachableScalarDefinition = -3;
|
private const long UnreachableScalarDefinition = -3;
|
||||||
@@ -5095,33 +5090,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
_vectorRegisters,
|
_vectorRegisters,
|
||||||
UInt(register));
|
UInt(register));
|
||||||
|
|
||||||
// The V_MOVREL* opcodes address the VGPR file with a register number that
|
|
||||||
// is only known at run time (encoded number + M0), so the access chain
|
|
||||||
// takes a computed index instead of a constant. The index is masked to
|
|
||||||
// the array bounds: SPIR-V leaves an out-of-range Private access chain
|
|
||||||
// undefined, and a mask costs nothing next to the surrounding load.
|
|
||||||
private uint DynamicVectorPointer(uint registerIndex) =>
|
|
||||||
_module.AddInstruction(
|
|
||||||
SpirvOp.AccessChain,
|
|
||||||
_privateUintPointer,
|
|
||||||
_vectorRegisters,
|
|
||||||
BitwiseAnd(registerIndex, UInt(VectorRegisterCount - 1)));
|
|
||||||
|
|
||||||
private uint LoadVDynamic(uint registerIndex) =>
|
|
||||||
Load(_uintType, DynamicVectorPointer(registerIndex));
|
|
||||||
|
|
||||||
private void StoreVDynamic(uint registerIndex, uint value)
|
|
||||||
{
|
|
||||||
var pointer = DynamicVectorPointer(registerIndex);
|
|
||||||
value = _module.AddInstruction(
|
|
||||||
SpirvOp.Select,
|
|
||||||
_uintType,
|
|
||||||
Load(_boolType, _exec),
|
|
||||||
value,
|
|
||||||
Load(_uintType, pointer));
|
|
||||||
Store(pointer, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private uint PackedHalfPointer(uint register) =>
|
private uint PackedHalfPointer(uint register) =>
|
||||||
_module.AddInstruction(
|
_module.AddInstruction(
|
||||||
SpirvOp.AccessChain,
|
SpirvOp.AccessChain,
|
||||||
|
|||||||
@@ -947,7 +947,6 @@ public static class Gen5ShaderTranslator
|
|||||||
0x42 => "VMovreldB32",
|
0x42 => "VMovreldB32",
|
||||||
0x43 => "VMovrelsB32",
|
0x43 => "VMovrelsB32",
|
||||||
0x44 => "VMovrelsdB32",
|
0x44 => "VMovrelsdB32",
|
||||||
0x48 => "VMovrelsd2B32",
|
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.ShaderCompiler;
|
|
||||||
using SharpEmu.ShaderCompiler.Vulkan;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Agc;
|
|
||||||
|
|
||||||
// Regression tests for the VOP1 register-relative moves V_MOVRELD_B32 /
|
|
||||||
// V_MOVRELS_B32 / V_MOVRELSD_B32 / V_MOVRELSD_2_B32 (opcodes 0x42/0x43/0x44/
|
|
||||||
// 0x48). These add M0 at run time to the source and/or destination register
|
|
||||||
// number encoded in the instruction, which is how a shader compiler implements
|
|
||||||
// a dynamically indexed array that stayed in registers. The decoder named them
|
|
||||||
// but nothing lowered them, so they hit the vector-ALU switch default and failed
|
|
||||||
// emission ("unsupported vector opcode"), dropping the whole shader — Astro Bot
|
|
||||||
// ships pixel shaders that use V_MOVRELS_B32.
|
|
||||||
//
|
|
||||||
// The register file is a private uint array, so the lowering is an OpAccessChain
|
|
||||||
// with a computed (non-constant) index. Each test therefore asserts both that
|
|
||||||
// the shader survives translation and that the relative operand really became a
|
|
||||||
// dynamic index rather than a constant one.
|
|
||||||
public sealed class Gen5MoveRelativeSpirvTests
|
|
||||||
{
|
|
||||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
|
||||||
|
|
||||||
// VOP1: [31:25]=0b0111111, [24:17]=vdst, [16:9]=op, [8:0]=src0
|
|
||||||
// (src0 >= 256 selects a VGPR).
|
|
||||||
private const uint Vop1 = 0x7E000000;
|
|
||||||
|
|
||||||
// SOP1 s_mov_b32 m0, <inline 2>: [31:23]=0b101111101, [22:16]=sdst,
|
|
||||||
// [15:8]=op(0x03), [7:0]=ssrc0. m0 is SGPR 124, inline constant 2 is 130.
|
|
||||||
private const uint SMovM0 = 0xBE800000u | (124u << 16) | (0x03u << 8) | 130u;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MovrelsB32_ReadsTheSourceRegisterThroughADynamicIndex()
|
|
||||||
{
|
|
||||||
// s_mov_b32 m0, 2 ; v_movrels_b32 v5, v3 -> v5 = vgpr[3 + m0]
|
|
||||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x43u << 9) | (256u + 3u)]);
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
HasDynamicVectorRegisterAccess(spirv),
|
|
||||||
"V_MOVRELS_B32 must index the VGPR array with a computed index");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MovreldB32_WritesTheDestinationRegisterThroughADynamicIndex()
|
|
||||||
{
|
|
||||||
// s_mov_b32 m0, 2 ; v_movreld_b32 v5, v3 -> vgpr[5 + m0] = v3
|
|
||||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x42u << 9) | (256u + 3u)]);
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
HasDynamicVectorRegisterAccess(spirv),
|
|
||||||
"V_MOVRELD_B32 must index the VGPR array with a computed index");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MovrelsdB32_TranslatesWithoutDroppingShader()
|
|
||||||
{
|
|
||||||
// s_mov_b32 m0, 2 ; v_movrelsd_b32 v5, v3 -> vgpr[5 + m0] = vgpr[3 + m0]
|
|
||||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x44u << 9) | (256u + 3u)]);
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
HasDynamicVectorRegisterAccess(spirv),
|
|
||||||
"V_MOVRELSD_B32 must index the VGPR array with a computed index");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Movrelsd2B32_TranslatesWithoutDroppingShader()
|
|
||||||
{
|
|
||||||
// s_mov_b32 m0, 2 ; v_movrelsd_2_b32 v5, v3, which splits m0 into two
|
|
||||||
// 10-bit halves (source index in [9:0], destination index in [25:16]).
|
|
||||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x48u << 9) | (256u + 3u)]);
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
HasDynamicVectorRegisterAccess(spirv),
|
|
||||||
"V_MOVRELSD_2_B32 must index the VGPR array with a computed index");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MovrelsB32_RejectsANonVectorSource()
|
|
||||||
{
|
|
||||||
// v_movrels_b32 v5, s3. The relative source is architecturally a VGPR;
|
|
||||||
// an SGPR encoding is malformed and must fail translation rather than
|
|
||||||
// silently read the wrong register file.
|
|
||||||
Assert.False(
|
|
||||||
TryCompile(
|
|
||||||
[SMovM0, Vop1 | (5u << 17) | (0x43u << 9) | 3u],
|
|
||||||
out _,
|
|
||||||
out var error));
|
|
||||||
Assert.Contains("vector register", error, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when some OpAccessChain into the "vgpr" array uses an index that is
|
|
||||||
// not an OpConstant — i.e. a register number computed from M0.
|
|
||||||
private static bool HasDynamicVectorRegisterAccess(byte[] spirv)
|
|
||||||
{
|
|
||||||
var vectorRegisters = FindNamedId(spirv, "vgpr");
|
|
||||||
Assert.True(vectorRegisters != 0, "the module must name its VGPR array");
|
|
||||||
|
|
||||||
var constants = new HashSet<uint>();
|
|
||||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
|
||||||
{
|
|
||||||
// OpConstant = 43, OpConstantNull = 46: (opcode, resultType, resultId, ...).
|
|
||||||
if (op is 43 or 46 && wordCount >= 3)
|
|
||||||
{
|
|
||||||
constants.Add(ReadWord(spirv, offset + 8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
|
||||||
{
|
|
||||||
// OpAccessChain = 65: (opcode, resultType, resultId, base, index...).
|
|
||||||
if (op != 65 || wordCount < 5 || ReadWord(spirv, offset + 12) != vectorRegisters)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!constants.Contains(ReadWord(spirv, offset + 16)))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result id of the OpName whose literal string matches, or 0.
|
|
||||||
private static uint FindNamedId(byte[] spirv, string name)
|
|
||||||
{
|
|
||||||
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
|
||||||
{
|
|
||||||
// OpName = 5: (opcode, target, literal string...).
|
|
||||||
if (op != 5 || wordCount < 3)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = spirv.AsSpan(offset + 8, (wordCount - 2) * sizeof(uint));
|
|
||||||
var terminator = bytes.IndexOf((byte)0);
|
|
||||||
var text = System.Text.Encoding.UTF8.GetString(
|
|
||||||
terminator < 0 ? bytes : bytes[..terminator]);
|
|
||||||
if (text == name)
|
|
||||||
{
|
|
||||||
return ReadWord(spirv, offset + 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
|
|
||||||
byte[] spirv)
|
|
||||||
{
|
|
||||||
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
|
||||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
|
||||||
{
|
|
||||||
var word = ReadWord(spirv, offset);
|
|
||||||
var wordCount = (int)(word >> 16);
|
|
||||||
if (wordCount <= 0)
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return ((ushort)word, wordCount, offset);
|
|
||||||
offset += wordCount * sizeof(uint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint ReadWord(byte[] spirv, int offset) =>
|
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
|
||||||
|
|
||||||
private static byte[] Compile(uint[] programWords)
|
|
||||||
{
|
|
||||||
Assert.True(TryCompile(programWords, out var spirv, out var error), error);
|
|
||||||
return spirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryCompile(uint[] programWords, out byte[] spirv, out string error)
|
|
||||||
{
|
|
||||||
spirv = [];
|
|
||||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
|
||||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
||||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
|
||||||
var shaderRegisters = new Dictionary<uint, uint>
|
|
||||||
{
|
|
||||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!Gen5ShaderTranslator.TryCreateState(
|
|
||||||
ctx,
|
|
||||||
ShaderAddress,
|
|
||||||
0,
|
|
||||||
shaderRegisters,
|
|
||||||
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
|
|
||||||
out var state,
|
|
||||||
out error) ||
|
|
||||||
!Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error) ||
|
|
||||||
!Gen5SpirvTranslator.TryCompileComputeShader(
|
|
||||||
state, evaluation, 1, 1, 1, out var shader, out error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
spirv = shader.Spirv;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -83,30 +83,6 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
Assert.Equal(InvalidContext, RegisterCodec(contextId + 1, 1));
|
Assert.Equal(InvalidContext, RegisterCodec(contextId + 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ModuleUnregister_RemovesRegisteredCodecAndRejectsUnknownContext()
|
|
||||||
{
|
|
||||||
var contextId = Initialize();
|
|
||||||
|
|
||||||
Assert.Equal(0, RegisterCodec(contextId, 1));
|
|
||||||
Assert.Equal(0, UnregisterCodec(contextId, 1));
|
|
||||||
// The codec is actually gone, not just a no-op stub: it's unusable
|
|
||||||
// for a new instance, and re-registering no longer hits
|
|
||||||
// CodecAlreadyRegistered.
|
|
||||||
Assert.Equal(CodecNotRegistered, CreateInstance(contextId, 1, 0x401, InstanceAddress));
|
|
||||||
Assert.Equal(0, RegisterCodec(contextId, 1));
|
|
||||||
|
|
||||||
Assert.Equal(InvalidContext, UnregisterCodec(contextId + 1, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ModuleUnregister_UnknownCodecIsToleratedAsANoOp()
|
|
||||||
{
|
|
||||||
var contextId = Initialize();
|
|
||||||
|
|
||||||
Assert.Equal(0, UnregisterCodec(contextId, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MemoryRegistration_TracksValidContextAndToleratesRepeatedUnregister()
|
public void MemoryRegistration_TracksValidContextAndToleratesRepeatedUnregister()
|
||||||
{
|
{
|
||||||
@@ -377,13 +353,6 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
return AjmExports.AjmModuleRegister(_ctx);
|
return AjmExports.AjmModuleRegister(_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int UnregisterCodec(uint contextId, uint codecType)
|
|
||||||
{
|
|
||||||
_ctx[CpuRegister.Rdi] = contextId;
|
|
||||||
_ctx[CpuRegister.Rsi] = codecType;
|
|
||||||
return AjmExports.AjmModuleUnregister(_ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int RegisterMemory(uint contextId, ulong address, ulong pages)
|
private int RegisterMemory(uint contextId, ulong address, ulong pages)
|
||||||
{
|
{
|
||||||
_ctx[CpuRegister.Rdi] = contextId;
|
_ctx[CpuRegister.Rdi] = contextId;
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.AvPlayer;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.AvPlayer;
|
|
||||||
|
|
||||||
public sealed class AvPlayerAbiTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Generation.Gen4, false, 108UL)]
|
|
||||||
[InlineData(Generation.Gen5, false, 112UL)]
|
|
||||||
[InlineData(Generation.Gen4, true, 164UL)]
|
|
||||||
[InlineData(Generation.Gen5, true, 168UL)]
|
|
||||||
public void InitAutoStartOffsetMatchesGeneration(
|
|
||||||
Generation generation,
|
|
||||||
bool extended,
|
|
||||||
ulong expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, AvPlayerExports.GetAutoStartOffset(generation, extended));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Generation.Gen4, 40)]
|
|
||||||
[InlineData(Generation.Gen5, 32)]
|
|
||||||
public void LegacyStreamInfoSizeMatchesGeneration(
|
|
||||||
Generation generation,
|
|
||||||
int expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, AvPlayerExports.GetLegacyStreamInfoSize(generation));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Generation.Gen4, 0u, 0u)]
|
|
||||||
[InlineData(Generation.Gen4, 1u, 1u)]
|
|
||||||
[InlineData(Generation.Gen5, 0u, 1u)]
|
|
||||||
[InlineData(Generation.Gen5, 1u, 2u)]
|
|
||||||
public void StreamTypeMatchesGeneration(
|
|
||||||
Generation generation,
|
|
||||||
uint streamIndex,
|
|
||||||
uint expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, AvPlayerExports.GetStreamType(generation, streamIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Gen5FrameInfoExCarriesPitchCropAndFrameRate()
|
|
||||||
{
|
|
||||||
var info = new byte[104];
|
|
||||||
|
|
||||||
AvPlayerExports.WriteVideoFrameInfo(
|
|
||||||
info,
|
|
||||||
Generation.Gen5,
|
|
||||||
extended: true,
|
|
||||||
bufferAddress: 0x1234_5000,
|
|
||||||
timestamp: 2_903,
|
|
||||||
width: 512,
|
|
||||||
visibleWidth: 378,
|
|
||||||
height: 150,
|
|
||||||
pitch: 512,
|
|
||||||
framesPerSecond: 29.97);
|
|
||||||
|
|
||||||
Assert.Equal(0x1234_5000UL, BinaryPrimitives.ReadUInt64LittleEndian(info));
|
|
||||||
Assert.Equal(2_903UL, BinaryPrimitives.ReadUInt64LittleEndian(info.AsSpan(16)));
|
|
||||||
Assert.Equal(512u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(24)));
|
|
||||||
Assert.Equal(150u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(28)));
|
|
||||||
Assert.Equal(134u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(48)));
|
|
||||||
Assert.Equal(512u, BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(60)));
|
|
||||||
Assert.Equal(8, info[64]);
|
|
||||||
Assert.Equal(8, info[65]);
|
|
||||||
Assert.Equal(29.97, BinaryPrimitives.ReadDoubleLittleEndian(info.AsSpan(0x48)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FallbackFrameValidationUsesTheDecodedDimensions()
|
|
||||||
{
|
|
||||||
var fullHdFrame = new byte[1920 * 1080 * 4];
|
|
||||||
|
|
||||||
Assert.True(AvPlayerExports.IsValidBgraFrame(fullHdFrame, 1920, 1080));
|
|
||||||
Assert.False(AvPlayerExports.IsValidBgraFrame(fullHdFrame, 3840, 2160));
|
|
||||||
Assert.False(AvPlayerExports.IsValidBgraFrame(fullHdFrame, 0, 1080));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PosterSuppressesTheDuplicateFirstHostDecodedFrame()
|
|
||||||
{
|
|
||||||
var skipFirstDecodedFrame = true;
|
|
||||||
|
|
||||||
Assert.False(AvPlayerExports.ShouldPublishFallbackPlaybackFrame(
|
|
||||||
advanced: true,
|
|
||||||
hasPresentation: true,
|
|
||||||
ref skipFirstDecodedFrame));
|
|
||||||
Assert.False(skipFirstDecodedFrame);
|
|
||||||
|
|
||||||
Assert.True(AvPlayerExports.ShouldPublishFallbackPlaybackFrame(
|
|
||||||
advanced: true,
|
|
||||||
hasPresentation: true,
|
|
||||||
ref skipFirstDecodedFrame));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(false, false, false)]
|
|
||||||
[InlineData(false, true, false)]
|
|
||||||
[InlineData(true, false, false)]
|
|
||||||
[InlineData(true, true, true)]
|
|
||||||
public void CompletedFallbackIsReleasedAtGuestEndOfStream(
|
|
||||||
bool fallbackCompleted,
|
|
||||||
bool guestEndOfStream,
|
|
||||||
bool expected)
|
|
||||||
{
|
|
||||||
var completedTicks = Stopwatch.GetTimestamp();
|
|
||||||
Assert.Equal(
|
|
||||||
expected,
|
|
||||||
AvPlayerExports.ShouldReleaseCompletedFallback(
|
|
||||||
fallbackCompleted,
|
|
||||||
guestEndOfStream,
|
|
||||||
completedTicks,
|
|
||||||
completedTicks));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A title that pauses its player after the poster frame never reaches end
|
|
||||||
/// of stream, so the hold must expire on its own; otherwise the last movie
|
|
||||||
/// image stays pinned over everything the game renders next.
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void CompletedFallbackHoldExpiresWithoutGuestEndOfStream()
|
|
||||||
{
|
|
||||||
var completedTicks = Stopwatch.GetTimestamp();
|
|
||||||
Assert.False(
|
|
||||||
AvPlayerExports.ShouldReleaseCompletedFallback(
|
|
||||||
fallbackPlaybackCompleted: true,
|
|
||||||
guestEndOfStream: false,
|
|
||||||
completedTicks,
|
|
||||||
completedTicks + (Stopwatch.Frequency / 10)));
|
|
||||||
Assert.True(
|
|
||||||
AvPlayerExports.ShouldReleaseCompletedFallback(
|
|
||||||
fallbackPlaybackCompleted: true,
|
|
||||||
guestEndOfStream: false,
|
|
||||||
completedTicks,
|
|
||||||
completedTicks + (Stopwatch.Frequency * 2)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.Core.Memory;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.AvPlayer;
|
|
||||||
using SharpEmu.Libs.Tests.Kernel;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.AvPlayer;
|
|
||||||
|
|
||||||
[Collection(KernelMemoryCompatStateCollection.Name)]
|
|
||||||
public sealed class AvPlayerAllocationTests : IDisposable
|
|
||||||
{
|
|
||||||
private const ulong Handle = 0xA0_0000_1000;
|
|
||||||
private readonly IGuestThreadScheduler? _previousScheduler = GuestThreadExecution.Scheduler;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FailedGuestAllocatorsFallBackToHleMemoryInTheSameAttempt()
|
|
||||||
{
|
|
||||||
using var memory = new PhysicalVirtualMemory();
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
var scheduler = new FailingAllocatorScheduler();
|
|
||||||
GuestThreadExecution.Scheduler = scheduler;
|
|
||||||
AvPlayerExports.RegisterPlayerForTest(
|
|
||||||
Handle,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
durationMilliseconds: 1,
|
|
||||||
allocateTextureCallback: 0x1000,
|
|
||||||
allocateCallback: 0x2000);
|
|
||||||
|
|
||||||
Assert.True(AvPlayerExports.AllocateGuestVideoBuffersForTest(
|
|
||||||
context,
|
|
||||||
Handle,
|
|
||||||
out var firstBuffer));
|
|
||||||
Assert.NotEqual(0UL, firstBuffer);
|
|
||||||
Assert.Equal(2, scheduler.CallCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
|
||||||
GuestThreadExecution.Scheduler = _previousScheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FailingAllocatorScheduler : IGuestThreadScheduler
|
|
||||||
{
|
|
||||||
public int CallCount { get; private set; }
|
|
||||||
|
|
||||||
public bool SupportsGuestContextTransfer => false;
|
|
||||||
|
|
||||||
public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryStartThread(
|
|
||||||
CpuContext creatorContext,
|
|
||||||
GuestThreadStartRequest request,
|
|
||||||
out string? error)
|
|
||||||
{
|
|
||||||
error = "not supported";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryJoinThread(
|
|
||||||
CpuContext callerContext,
|
|
||||||
ulong threadHandle,
|
|
||||||
out ulong returnValue,
|
|
||||||
out string? error)
|
|
||||||
{
|
|
||||||
returnValue = 0;
|
|
||||||
error = "not supported";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Pump(CpuContext callerContext, string reason)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue) => 0;
|
|
||||||
|
|
||||||
public bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority) => false;
|
|
||||||
|
|
||||||
public bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask) => false;
|
|
||||||
|
|
||||||
public IReadOnlyList<GuestThreadSnapshot> SnapshotThreads() => [];
|
|
||||||
|
|
||||||
public bool TryCallGuestFunction(
|
|
||||||
CpuContext callerContext,
|
|
||||||
ulong entryPoint,
|
|
||||||
ulong arg0,
|
|
||||||
ulong arg1,
|
|
||||||
ulong stackAddress,
|
|
||||||
ulong stackSize,
|
|
||||||
string reason,
|
|
||||||
out string? error)
|
|
||||||
{
|
|
||||||
error = "not supported";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCallGuestFunction(
|
|
||||||
CpuContext callerContext,
|
|
||||||
ulong entryPoint,
|
|
||||||
ulong arg0,
|
|
||||||
ulong arg1,
|
|
||||||
ulong arg2,
|
|
||||||
ulong stackAddress,
|
|
||||||
ulong stackSize,
|
|
||||||
string reason,
|
|
||||||
out ulong returnValue,
|
|
||||||
out string? error)
|
|
||||||
{
|
|
||||||
CallCount++;
|
|
||||||
returnValue = 0;
|
|
||||||
error = "allocator rejected the request";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCallGuestContinuation(
|
|
||||||
CpuContext callerContext,
|
|
||||||
GuestCpuContinuation continuation,
|
|
||||||
string reason,
|
|
||||||
out string? error)
|
|
||||||
{
|
|
||||||
error = "not supported";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryRaiseGuestException(
|
|
||||||
CpuContext callerContext,
|
|
||||||
ulong threadHandle,
|
|
||||||
ulong handler,
|
|
||||||
int exceptionType,
|
|
||||||
out string? error)
|
|
||||||
{
|
|
||||||
error = "not supported";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.Libs.AvPlayer;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.AvPlayer;
|
|
||||||
|
|
||||||
public sealed class AvPlayerNv12LayoutTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData(1920, 2048)]
|
|
||||||
[InlineData(3840, 3840)]
|
|
||||||
[InlineData(4097, 4352)]
|
|
||||||
public void CalculateNv12Pitch_AlignsTo256Bytes(int width, int expectedPitch)
|
|
||||||
{
|
|
||||||
Assert.Equal(expectedPitch, AvPlayerExports.CalculateNv12Pitch(width));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CalculateNv12BufferSize_IncludesBothPlanesAtTheAlignedPitch()
|
|
||||||
{
|
|
||||||
Assert.Equal(3_317_760, AvPlayerExports.CalculateNv12BufferSize(2048, 1080));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConvertNv12ToBgra_UsesTheInterleavedChromaPlaneAndOpaqueAlpha()
|
|
||||||
{
|
|
||||||
byte[] nv12 =
|
|
||||||
[
|
|
||||||
16, 235,
|
|
||||||
81, 145,
|
|
||||||
128, 128,
|
|
||||||
];
|
|
||||||
var bgra = new byte[2 * 2 * 4];
|
|
||||||
|
|
||||||
AvPlayerExports.ConvertNv12ToBgra(
|
|
||||||
nv12,
|
|
||||||
pitch: 2,
|
|
||||||
bufferHeight: 2,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
bgra);
|
|
||||||
|
|
||||||
Assert.Equal(
|
|
||||||
new byte[]
|
|
||||||
{
|
|
||||||
0, 0, 0, 255,
|
|
||||||
255, 255, 255, 255,
|
|
||||||
76, 76, 76, 255,
|
|
||||||
150, 150, 150, 255,
|
|
||||||
},
|
|
||||||
bgra);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CopyNv12ToGuestBuffer_UsesSourceStridesAndPitchedUvOffset()
|
|
||||||
{
|
|
||||||
const int width = 4;
|
|
||||||
const int height = 4;
|
|
||||||
const int sourceLumaStride = 6;
|
|
||||||
const int sourceChromaStride = 8;
|
|
||||||
const int destinationPitch = 8;
|
|
||||||
var source = Enumerable.Repeat((byte)0xEE, 40).ToArray();
|
|
||||||
for (var row = 0; row < height; row++)
|
|
||||||
{
|
|
||||||
for (var column = 0; column < width; column++)
|
|
||||||
{
|
|
||||||
source[(row * sourceLumaStride) + column] = checked((byte)(1 + (row * 10) + column));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sourceChromaOffset = sourceLumaStride * height;
|
|
||||||
for (var row = 0; row < height / 2; row++)
|
|
||||||
{
|
|
||||||
for (var column = 0; column < width; column++)
|
|
||||||
{
|
|
||||||
source[sourceChromaOffset + (row * sourceChromaStride) + column] =
|
|
||||||
checked((byte)(101 + (row * 10) + column));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var destination = Enumerable.Repeat((byte)0xCC, 48).ToArray();
|
|
||||||
AvPlayerExports.CopyNv12ToGuestBuffer(
|
|
||||||
source,
|
|
||||||
destination,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
sourceLumaStride,
|
|
||||||
sourceChromaStride,
|
|
||||||
destinationPitch);
|
|
||||||
|
|
||||||
var expected = new byte[48];
|
|
||||||
for (var row = 0; row < height; row++)
|
|
||||||
{
|
|
||||||
source.AsSpan(row * sourceLumaStride, width)
|
|
||||||
.CopyTo(expected.AsSpan(row * destinationPitch, width));
|
|
||||||
}
|
|
||||||
var destinationChromaOffset = destinationPitch * height;
|
|
||||||
for (var row = 0; row < height / 2; row++)
|
|
||||||
{
|
|
||||||
source.AsSpan(sourceChromaOffset + (row * sourceChromaStride), width)
|
|
||||||
.CopyTo(expected.AsSpan(destinationChromaOffset + (row * destinationPitch), width));
|
|
||||||
}
|
|
||||||
Assert.Equal(expected, destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,39 +19,36 @@ public sealed class AvPlayerStreamInfoTests
|
|||||||
private const byte Sentinel = 0xAB;
|
private const byte Sentinel = 0xAB;
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(Generation.Gen5, 0u, 32, 1u)]
|
[InlineData(false, 0u)]
|
||||||
[InlineData(Generation.Gen5, 1u, 32, 2u)]
|
[InlineData(true, 0u)]
|
||||||
[InlineData(Generation.Gen4, 0u, 40, 0u)]
|
[InlineData(false, 1u)]
|
||||||
[InlineData(Generation.Gen4, 1u, 40, 1u)]
|
[InlineData(true, 1u)]
|
||||||
public void GetStreamInfoUsesTheGenerationSpecificLayout(
|
public void GetStreamInfoFunctionsDoNotWritePastThe32ByteStructure(
|
||||||
Generation generation,
|
bool useExtendedFunction,
|
||||||
uint streamIndex,
|
uint streamIndex)
|
||||||
int structureSize,
|
|
||||||
uint expectedStreamType)
|
|
||||||
{
|
{
|
||||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||||
var context = new CpuContext(memory, generation);
|
var context = new CpuContext(memory, Generation.Gen5);
|
||||||
AvPlayerExports.RegisterPlayerForTest(
|
|
||||||
Handle,
|
|
||||||
1280,
|
|
||||||
720,
|
|
||||||
DurationMilliseconds,
|
|
||||||
hasAudio: true);
|
|
||||||
|
|
||||||
|
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Span<byte> window = stackalloc byte[48];
|
Span<byte> window = stackalloc byte[40];
|
||||||
window.Fill(Sentinel);
|
window.Fill(Sentinel);
|
||||||
Assert.True(memory.TryWrite(InfoAddress, window));
|
Assert.True(memory.TryWrite(InfoAddress, window));
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = Handle;
|
context[CpuRegister.Rdi] = Handle;
|
||||||
context[CpuRegister.Rsi] = streamIndex;
|
context[CpuRegister.Rsi] = streamIndex;
|
||||||
context[CpuRegister.Rdx] = InfoAddress;
|
context[CpuRegister.Rdx] = InfoAddress;
|
||||||
Assert.Equal(0, AvPlayerExports.AvPlayerGetStreamInfo(context));
|
|
||||||
|
|
||||||
Span<byte> result = stackalloc byte[48];
|
var resultCode = useExtendedFunction
|
||||||
|
? AvPlayerExports.AvPlayerGetStreamInfoEx(context)
|
||||||
|
: AvPlayerExports.AvPlayerGetStreamInfo(context);
|
||||||
|
Assert.Equal(0, resultCode);
|
||||||
|
|
||||||
|
Span<byte> result = stackalloc byte[40];
|
||||||
Assert.True(memory.TryRead(InfoAddress, result));
|
Assert.True(memory.TryRead(InfoAddress, result));
|
||||||
Assert.Equal(expectedStreamType, BinaryPrimitives.ReadUInt32LittleEndian(result));
|
Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result));
|
||||||
if (streamIndex == 0)
|
if (streamIndex == 0)
|
||||||
{
|
{
|
||||||
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
|
Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
|
||||||
@@ -64,71 +61,7 @@ public sealed class AvPlayerStreamInfoTests
|
|||||||
}
|
}
|
||||||
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
|
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..]));
|
||||||
|
|
||||||
for (var index = structureSize; index < result.Length; index++)
|
for (var index = 32; index < result.Length; index++)
|
||||||
{
|
|
||||||
Assert.Equal(Sentinel, result[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void StreamInfoRejectsAudioIndexForVideoOnlyMedia()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context[CpuRegister.Rdi] = Handle;
|
|
||||||
context[CpuRegister.Rsi] = 1;
|
|
||||||
context[CpuRegister.Rdx] = InfoAddress;
|
|
||||||
Assert.NotEqual(0, AvPlayerExports.AvPlayerGetStreamInfo(context));
|
|
||||||
Assert.NotEqual(0, AvPlayerExports.AvPlayerGetStreamInfoEx(context));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
AvPlayerExports.RemovePlayerForTest(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStreamInfoExWritesThe104ByteGen5Descriptor()
|
|
||||||
{
|
|
||||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
|
||||||
AvPlayerExports.RegisterPlayerForTest(
|
|
||||||
Handle,
|
|
||||||
378,
|
|
||||||
150,
|
|
||||||
DurationMilliseconds,
|
|
||||||
framesPerSecond: 29.97);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Span<byte> window = stackalloc byte[120];
|
|
||||||
window.Fill(Sentinel);
|
|
||||||
Assert.True(memory.TryWrite(InfoAddress, window));
|
|
||||||
|
|
||||||
context[CpuRegister.Rdi] = Handle;
|
|
||||||
context[CpuRegister.Rsi] = 0;
|
|
||||||
context[CpuRegister.Rdx] = InfoAddress;
|
|
||||||
Assert.Equal(0, AvPlayerExports.AvPlayerGetStreamInfoEx(context));
|
|
||||||
|
|
||||||
Span<byte> result = stackalloc byte[120];
|
|
||||||
Assert.True(memory.TryRead(InfoAddress, result));
|
|
||||||
Assert.Equal(104UL, BinaryPrimitives.ReadUInt64LittleEndian(result));
|
|
||||||
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..]));
|
|
||||||
Assert.Equal(378u, BinaryPrimitives.ReadUInt32LittleEndian(result[16..]));
|
|
||||||
Assert.Equal(150u, BinaryPrimitives.ReadUInt32LittleEndian(result[20..]));
|
|
||||||
Assert.Equal(29.97, BinaryPrimitives.ReadDoubleLittleEndian(result[0x40..]));
|
|
||||||
Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[0x60..]));
|
|
||||||
|
|
||||||
for (var index = 104; index < result.Length; index++)
|
|
||||||
{
|
{
|
||||||
Assert.Equal(Sentinel, result[index]);
|
Assert.Equal(Sentinel, result[index]);
|
||||||
}
|
}
|
||||||
@@ -146,12 +79,7 @@ public sealed class AvPlayerStreamInfoTests
|
|||||||
{
|
{
|
||||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||||
var context = new CpuContext(memory, Generation.Gen5);
|
var context = new CpuContext(memory, Generation.Gen5);
|
||||||
AvPlayerExports.RegisterPlayerForTest(
|
AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds);
|
||||||
Handle,
|
|
||||||
1280,
|
|
||||||
720,
|
|
||||||
DurationMilliseconds,
|
|
||||||
hasAudio: true);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using SharpEmu.Core.Cpu;
|
|
||||||
using SharpEmu.Core.Cpu.Native;
|
|
||||||
using SharpEmu.Core.Loader;
|
|
||||||
using SharpEmu.Core.Memory;
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Cpu;
|
|
||||||
|
|
||||||
public sealed class Gen5NativeReturnSmokeTests
|
|
||||||
{
|
|
||||||
private const string WorkerEnvironmentVariable = "SHARPEMU_NATIVE_RETURN_SMOKE_WORKER";
|
|
||||||
private static readonly TimeSpan WorkerTimeout = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SyntheticGen5Entry_ReturnsToHost()
|
|
||||||
{
|
|
||||||
if (!IsSupportedHost)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable(WorkerEnvironmentVariable),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
ExecuteSyntheticGuest();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await RunIsolatedWorker();
|
|
||||||
|
|
||||||
Assert.True(
|
|
||||||
result.Completed,
|
|
||||||
$"native return worker did not exit within {WorkerTimeout.TotalSeconds:F0} seconds\n{result.Output}");
|
|
||||||
Assert.True(
|
|
||||||
result.ExitCode == 0,
|
|
||||||
$"native return worker exited with code {result.ExitCode}\n{result.Output}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSupportedHost =>
|
|
||||||
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
|
|
||||||
(OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS());
|
|
||||||
|
|
||||||
private static void ExecuteSyntheticGuest()
|
|
||||||
{
|
|
||||||
using var memory = new PhysicalVirtualMemory();
|
|
||||||
var image = new SelfLoader().Load(BuildSyntheticElf(), memory);
|
|
||||||
Assert.Equal((byte)2, image.ElfHeader.AbiVersion);
|
|
||||||
Assert.Equal(0x0000_0008_0000_1000UL, image.EntryPoint);
|
|
||||||
|
|
||||||
var moduleManager = new ModuleManager();
|
|
||||||
moduleManager.Freeze();
|
|
||||||
|
|
||||||
var backend = new DirectExecutionBackend(moduleManager);
|
|
||||||
using var dispatcher = new CpuDispatcher(memory, moduleManager, backend);
|
|
||||||
var result = dispatcher.DispatchEntry(
|
|
||||||
image.EntryPoint,
|
|
||||||
Generation.Gen5,
|
|
||||||
image.ImportStubs,
|
|
||||||
image.RuntimeSymbols,
|
|
||||||
"synthetic-native-return",
|
|
||||||
new CpuExecutionOptions
|
|
||||||
{
|
|
||||||
CpuEngine = CpuExecutionEngine.NativeOnly,
|
|
||||||
EnableDisasmDiagnostics = false,
|
|
||||||
StrictDynlibResolution = true,
|
|
||||||
ImportTraceLimit = 0,
|
|
||||||
DebugHook = null
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
|
||||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, dispatcher.LastSessionSummary.Result);
|
|
||||||
Assert.Equal(CpuExitReason.ReturnedToHost, dispatcher.LastSessionSummary.Reason);
|
|
||||||
Assert.Equal(0, dispatcher.LastSessionSummary.ImportsHit);
|
|
||||||
Assert.Equal(0, dispatcher.LastSessionSummary.UniqueNidsHit);
|
|
||||||
Assert.Null(dispatcher.LastTrapInfo);
|
|
||||||
Assert.Null(dispatcher.LastMemoryFaultInfo);
|
|
||||||
Assert.Null(dispatcher.LastNotImplementedInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<WorkerResult> RunIsolatedWorker()
|
|
||||||
{
|
|
||||||
var startInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = ResolveDotnetHost(),
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true
|
|
||||||
};
|
|
||||||
startInfo.ArgumentList.Add("test");
|
|
||||||
startInfo.ArgumentList.Add(typeof(Gen5NativeReturnSmokeTests).Assembly.Location);
|
|
||||||
startInfo.ArgumentList.Add("--filter");
|
|
||||||
startInfo.ArgumentList.Add(
|
|
||||||
$"FullyQualifiedName={typeof(Gen5NativeReturnSmokeTests).FullName}.{nameof(SyntheticGen5Entry_ReturnsToHost)}");
|
|
||||||
startInfo.Environment[WorkerEnvironmentVariable] = "1";
|
|
||||||
startInfo.Environment["SHARPEMU_SENTINEL_PROBE"] = null;
|
|
||||||
|
|
||||||
using var process = Process.Start(startInfo) ??
|
|
||||||
throw new InvalidOperationException("Could not start the isolated native return worker.");
|
|
||||||
var stdout = process.StandardOutput.ReadToEndAsync();
|
|
||||||
var stderr = process.StandardError.ReadToEndAsync();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await process.WaitForExitAsync().WaitAsync(WorkerTimeout);
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
process.Kill(entireProcessTree: true);
|
|
||||||
await process.WaitForExitAsync();
|
|
||||||
return new WorkerResult(false, process.ExitCode, await ReadOutput(stdout, stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WorkerResult(true, process.ExitCode, await ReadOutput(stdout, stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolveDotnetHost()
|
|
||||||
{
|
|
||||||
var configuredHost = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
|
|
||||||
if (!string.IsNullOrWhiteSpace(configuredHost))
|
|
||||||
{
|
|
||||||
return configuredHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
var processPath = Environment.ProcessPath;
|
|
||||||
if (processPath is not null &&
|
|
||||||
string.Equals(
|
|
||||||
Path.GetFileNameWithoutExtension(processPath),
|
|
||||||
"dotnet",
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return processPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "dotnet";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> ReadOutput(Task<string> stdout, Task<string> stderr) =>
|
|
||||||
await stdout + await stderr;
|
|
||||||
|
|
||||||
private static byte[] BuildSyntheticElf()
|
|
||||||
{
|
|
||||||
const int elfHeaderSize = 0x40;
|
|
||||||
const int programHeaderSize = 0x38;
|
|
||||||
const int fileOffset = 0x1000;
|
|
||||||
const ulong entryPoint = 0x1000;
|
|
||||||
ReadOnlySpan<byte> payload = [0x31, 0xC0, 0xC3]; // xor eax, eax; ret
|
|
||||||
var image = new byte[fileOffset + payload.Length];
|
|
||||||
|
|
||||||
image[0] = 0x7F;
|
|
||||||
image[1] = (byte)'E';
|
|
||||||
image[2] = (byte)'L';
|
|
||||||
image[3] = (byte)'F';
|
|
||||||
image[4] = 2;
|
|
||||||
image[5] = 1;
|
|
||||||
image[6] = 1;
|
|
||||||
image[7] = 9;
|
|
||||||
image[8] = 2;
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x10), 3);
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x12), 0x3E);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x14), 1);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(image.AsSpan(0x18), entryPoint);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(image.AsSpan(0x20), elfHeaderSize);
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x34), elfHeaderSize);
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x36), programHeaderSize);
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x38), 1);
|
|
||||||
|
|
||||||
var programHeader = image.AsSpan(elfHeaderSize, programHeaderSize);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(programHeader, 1);
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(programHeader[0x04..], 5);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x08..], fileOffset);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x10..], entryPoint);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x18..], entryPoint);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x20..], (ulong)payload.Length);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x28..], (ulong)payload.Length);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x30..], 0x1000);
|
|
||||||
payload.CopyTo(image.AsSpan(fileOffset));
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record WorkerResult(bool Completed, int ExitCode, string Output);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user