Compare commits

..

15 Commits

Author SHA1 Message Date
frangametv b226dca3df feat(hle): prefer loaded guest exports for selected registrations (#844)
Co-authored-by: Acelogic <miguelc4600@gmail.com>
2026-08-25 22:32:57 +02:00
Foued Attar 51e5480049 fix(cpu): preserve TLS instruction boundaries after rel8 branches (#846) 2026-08-24 22:52:30 +02:00
frangametv 600fcde637 fix(memory): expand the HLE guest-allocation arena (#843)
Co-authored-by: Foued Attar <foued.attar@lyceeastier.com>
2026-08-24 19:23:43 +03:00
frangametv 807aad18b3 tools(shader): inspect raw Gen5 shader dumps (#842)
Co-authored-by: Acelogic <miguelc4600@gmail.com>
2026-08-24 19:23:18 +03:00
frangametv f4f36b558f fix(agc): refine vertex layouts without rebasing captured inputs (#841) 2026-08-24 14:49:36 +02:00
frangametv 2b8ef7d8fa shader: add compact f16 arithmetic and compare lowering (#840)
Co-authored-by: Foued Attar <attar.foued@gmail.com>
2026-08-24 13:59:51 +02:00
angleyanalbedo f8a826ec1b fix(agc,video): CMASK/DCC metadata state machine , remove heavy heavy motion trails in DeadCell (#836)
* fix(agc): add support for CMASK fast clear in rendering process

* fix(agc): prevent stale cross-frame label writes from bypassing WAIT_REG_MEM

Add frame-ID tracking to GpuWaitRegistry so that WAIT_REG_MEM in frame N+1
is not satisfied by a label written in frame N. Previously, a label that
persisted in guest memory from the previous frame could satisfy a new
WAIT_REG_MEM immediately, causing the waiting DCB to bypass its fence and
execute out of order.

Changes:
- GpuWaitRegistry: add _labelFrameIds, _currentFrameId, AdvanceFrame(),
  IsLabelFresh(); RecordProduced now stamps the frame ID on each write
- AgcExports: call AdvanceFrame() at RFlip (frame boundary);
  HandleSubmittedWaitRegMem checks IsLabelFresh before bypassing

This is one piece of the Dead Cells character visibility fix (issue #833).
The cross-frame label reuse could cause composite passes to execute before
character-layer passes, resulting in invisible characters.

* fix(video): simulate CMASK 'all clear' at frame boundary to prevent trails

On real PS5, CMASK is reset to 'all clear' at each frame boundary,
so render targets are effectively cleared. SharpEmu did not implement
this behavior, causing GBuffer targets to LOAD stale data from the
previous frame, resulting in visible trails/ghosting.

Reset Initialized=false for all offscreen guest images at the present
boundary (when _currentFrameSlot changes). This ensures every render
target starts each frame with LoadOp.Clear, matching the hardware's
CMASK behavior.

This is the trail fix for Dead Cells (issue #833), complementing the
cross-frame label staleness fix in GpuWaitRegistry.

* fix(video): only reset render targets (not textures) for CMASK simulation

Previous commit 89b4610 reset Initialized for ALL guest images, including
textures and storage images, causing a completely black screen. Only images
with a RenderPass (actual color render targets) should be reset, as CMASK
'all clear' only applies to color render targets, not sampled textures.

* Revert "fix(video): simulate CMASK 'all clear' at frame boundary to prevent trails"

This reverts commit 89b4610a015619848e4000372822dca1c08051ff.

* fix(agc): implement CMASK state machine for render target clearing

Implement CMASK (Color Mask) hardware behavior based on shadPS4's approach:

1. Track CMASK addresses from CB_COLORn_CMASK registers
2. Detect compute shaders that write to CMASK addresses (IsComputeMetaClear
   heuristic: no bitwise XOR in shader = clear shader)
3. Mark CMASK as 'all clear' when compute shader writes to it
4. EliminateFastClear mode now checks CMASK state before clearing

This addresses the trail/ghosting issue in Dead Cells (issue #833) where
642x362 GBuffer targets were never cleared after first use, causing stale
data from previous frames to persist.

Reference: shadPS4's IsComputeMetaClear + EliminateFastClear implementation.

* fix(agc): implement CMASK state machine for render target clearing

Based on shadPS4's PM4 implementation, add CMASK (Color Mask) hardware
behavior tracking:

1. TrackCmaskAddresses: Read CB_COLORn_CMASK registers to get CMASK addresses
2. CheckCmaskWrite: When DMA fill targets CMASK address with value 0,
   mark CMASK as 'all clear' (shadPS4's FillBuffer logic)
3. EliminateFastClear: Check CMASK state before clearing - only clear
   if CMASK is 'all clear', then mark as 'dirty'
4. DMA fill: Call TrackCmaskAddresses before processing to ensure
   CMASK addresses are registered when DMA fills happen

This addresses the trail/ghosting issue in Dead Cells (issue #833) where
642x362 GBuffer targets were never cleared after first use.

Reference: shadPS4's IsComputeMetaClear + EliminateFastClear + FillBuffer.

* refactor(agc): remove diagnostic CMASK traces from hot path

Remove 4 diagnostic TraceAgcShader calls that were added during CMASK
investigation:
- agc.cb_regs=[...] - allocated List<string> + string.Join on every draw
- agc.cmask_track - per-slot interpolated string on every draw
- agc.cmask_write - per-DMA-fill interpolated string
- agc.eliminate_fast_clear - per-draw interpolated string

These were debugging probes and are no longer needed.

* fix(agc): sync constant-fill zero writes to Vulkan render targets

Dead Cells GBuffer trails: the constant-fill compute kernel
(TrySubmitConstantFillKernel) wrote zeros to guest memory but never
invalided the host Vulkan GuestImageResource that backs the render
target, so the GBuffer pass kept LoadOp.Load stale previous-frame pixels.

After the guest write, request a guest color clear尷 ... clear for the
destination address (RequestAgentColorClear projection (clear)) so the next render pass
uses LoadOp.Clear. The pending clearandray mechanism already exists; the
fill kernel was simply bypassing it.

* Remove AGC lifecycle debug probes

Remove transient render-target lifecycle and indirect draw debug instrumentation from AgcExports. The change strips the noisy console probing while keeping the active render-target tracking logic intact for draw sequencing and validation.

* fix(video): clear frame-stale MRT groups at guest flip boundary

On hardware a colour surface whose fast-clear metadata is in reset
state reads back as the CB clear value instead of stale memory, so
games can leave per-frame MRT groups uncleaned and rely on that
implicit initialization. With no metadata layer, such a target keeps
whatever touched it last; when the group shares addresses with the
compositor's output, the entire finished previous frame bleeds
through every region the new frame does not repaint (Dead Cells
dungeon trails / ghosted characters).

Arm a reset at the guest's own flip command - the authoritative
frame boundary inside the submission stream - and let the first
multi-attachment colour group of the fresh frame consume it,
starting from LoadOp.Clear. One arm per flip yields exactly one
reset per guest frame regardless of CPU/GPU pipelining; later
groups keep load semantics so intra-frame pass chaining is
untouched. CPU-backed images are never touched.

Mirrors shadPS4's meta-state-driven attachment.is_clear at
render-pass begin (vk_rasterizer.cpp BeginRendering).

* fix(agc): correct CbColor0Cmask register offset and add EXT support

CbColor0Cmask was 0x320 (CMASK_SLICE, tile_max:14) instead of 0x31F
(CMASK_BASE_ADDRESS). TrackCmaskAddresses read the slice count instead
of the base address, so CMASK registration always produced garbage or
zero — the entire meta-state chain starved for every game.

Fix: 0x320 → 0x31F. Add CbColor0CmaskBaseExt (0x398) and decode the
full 48-bit address: ((ext & 0xFF) << 40) | ((low & 0x1FFFFFFF) << 8).

Phase 0 probe confirmed Dead Cells programs neither CMASK nor DCC
registers (all zeros) — it relies on unified memory semantics where
unwritten surfaces read as zero. The register offset fix is still
needed for games that do use CMASK metadata.

* feat(agc,video): CMASK/DCC meta-state machine with CLEAR WORD support

Replace the flat _cmaskClearedState dictionary with a proper meta-state
ledger that separates registration, clearing, and dirtying:

- MetaSurfaceInfo keyed by colour-buffer address (not meta address)
- Reverse map _cmaskToColorBuffer for fill/compute write detection
- TrackCmaskAddresses reads both CMASK (0x31F) and DCC (0x325) with
  EXT high-bit decoding; prefers CMASK, falls back to DCC
- CLEAR_WORD captured at registration time, passed through to
  BeginTranslatedRenderPass as VkClearValue
- EFC checks specific surface (not 'any cleared → clear slot0'),
  dirties only that surface (not entire table)
- CheckCmaskWrite uses reverse map for O(1) lookup
- Presenter bind loop queries IsMetaClearedForSurface → LoadOp.Clear
- _metaStateOverridesFlipArm flag gates flip-arm for gradual retirement

Phase 0 probe confirmed Dead Cells programs neither CMASK nor DCC
(all registers zero) — flip-arm remains the correct fix for that game.
The meta-state machine serves games that do use CMASK/DCC metadata.

* fix(agc,video): implement surface clearing at guest flip boundary

* refactor(video): remove flip-arm heuristic, meta-state machine fully replaces it

The flip-arm (_frameColorResetArmed) was a heuristic that cleared the
first multi-attachment colour group at guest flip boundary.  The
CMASK/DCC meta-state machine now handles all clearing: at flip time
MarkAllSurfacesCleared() marks every registered surface as cleared;
at bind time IsMetaClearedForSurface() triggers LoadOp.Clear and
consumes the state.  This replaces the flip-arm with a per-surface
state machine that correctly handles both explicit clears (DMA fill,
compute, EFC) and implicit frame-boundary resets.

Also fixed a bug where TrackCmaskAddresses overwrote IsCleared to
false on every call, defeating the frame-boundary reset.  Now
preserves existing IsCleared state when re-registering.

* Thread-safe meta-surface state & meta-clear decode

Add synchronization for metadata state: introduce _metaSurfaceGate and guard accesses to _metaSurfaces and _cmaskToColorBuffer in AgcExports to avoid concurrent-dictionary corruption and ensure small critical sections. Preserve cleared state only when metadata binding matches during re-registration. Make Gen5 texture format constants internal. Update logic to set/consume IsCleared under lock and check CMASK nearby windows safely.

Decode meta clear values in VulkanVideoPresenter: skip CPU-backed targets for meta clears, add UnpackMetaClearValue and HalfToFloat to convert CLEAR_WORD0/1 into proper ClearColorValue for R8G8B8A8_UNORM and R16G16B16A16_FLOAT formats (with an 8_8_8_8 fallback). This ensures correct clear colours and thread-safe metadata handling.
2026-08-24 13:56:44 +03:00
Foued Attar 4a7a45d1b3 Fix TLS-load patcher corrupting short jumps immediately before FS:[0] reads (#838)
The linear scanner consumed leading 0x66 bytes without checking that the
candidate starts on an instruction boundary. A short jump whose disp8 is
0x66 (EB 66) directly followed by a 66-prefixed FS:[0] load made the
patcher treat the displacement as a prefix and write its call opcode over
it, rewriting 'jmp forward past the TLS access' into 'jmp backward' -- an
infinite loop. GTA V's AGC resource destructors hit exactly this shape and
leaked the entire heap inside a container drain before any frame was
presented.

Reject candidates whose preceding byte is 0xEB: a bare EB can never be the
last byte of a valid instruction, so such a position is provably
mid-instruction. The outer byte scan then retries at the next offset,
which is the true boundary, and the patch lands correctly.
2026-08-23 14:52:44 +02:00
angleyanalbedo 3a744c991e fix(agc): record write_data produced labels so WAIT_REG_MEM can resume (#834)
ApplySubmittedWriteData wrote label values to guest memory but never called
GpuWaitRegistry.RecordProduced, unlike ApplySubmittedReleaseMem. A suspended
WAIT_REG_MEM on a write_data label (e.g. Dead Cells frame fence 0x10243CFD8)
then could never be latched or deadlock-broken: guest memory is reset to 0 for
frame reuse before the next re-check, and _lastProduced had no value to replay.
Record each written dword and, for increment count2, the combined 64-bit value
so 32/64-bit waits latch like ReleaseMem dataSel=2.
2026-08-21 20:18:30 +03:00
urmoit 35a28f0143 [GUI] Estonian language (#835)
* [GUI] Estonian language

Implements Estonian language support for the SharpEmu's GUI.

* [GUI] Update and refine Estonian translations

Correct grammatical errors, refine technical UI terminology (e.g., view layout descriptions), and improve overall phrasing for better consistency across the launcher UI.
2026-08-21 20:18:07 +03:00
Marcin Mitura e79a1cc70a [GUI] Polish language (#832) 2026-08-20 19:34:52 +02:00
Mathias a2241d0e83 Fix/agc zero dim and storage (#828)
* fix(videoout): promote guest images to storage usage

* fix(agc): treat zero-dimension indirect compute dispatches as valid no-ops
2026-08-18 21:20:54 +02:00
Foued Attar fe6521f617 Fix unaligned BufferLoad/GlobalLoad dword access
BufferLoadDword/x2/x3/x4, BufferStoreDword/x2/x3/x4, and their GLOBAL
counterparts were routed through LoadUnalignedBufferWord /
StoreBufferBytes, which reconstruct every dword one byte at a time
(4 bounds-checked buffer accesses per dword, each with its own
OpArrayLength + OpSelect + OpAccessChain + OpLoad/OpStore, plus
shift/mask/or reassembly on top).

The GCN ISA guarantees these opcodes are always dword-aligned - only
the byte/short/D16 variants legitimately need unaligned access, and
those already have their own dedicated path
(LoadSubdwordBufferValue / StoreBufferBytes with an explicit byte
count). The generic dword-count loop reached by every other
BufferLoad*/BufferStore*/GlobalLoad*/GlobalStore* opcode was paying
the same per-byte cost for no reason.

Route the dword-granularity path straight through the existing
LoadBufferWord / StoreBufferWord helpers (one bounds check and one
load/store per dword) instead. Measured on a compute shader with 6
BufferLoadDwordx4 instructions in its hottest basic block, this drops
GPU dispatch time for that shader from ~430-448ms to ~86-92ms (~5x)
with no change in output correctness - it is a pure translation
inefficiency fix, independent of any specific title.
2026-08-18 12:20:24 +02:00
Mathias 034ddcc092 fix(vmem): use ConcurrentDictionary for _pageProtections to prevent race corruption (#823) 2026-08-18 12:13:32 +02:00
Berk d9b599a1fd chore: bump version to 0.0.3-release.3 (#826) 2026-08-18 00:19:31 +03:00
28 changed files with 2420 additions and 303 deletions
@@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Iced.Intel;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Core.Loader;
@@ -1638,18 +1639,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (_moduleManager.TryGetExport(nid, out ExportedFunction export))
{
if (IsKernelLibrary(export.LibraryName))
var preferLleForLibc = IsLibcLibrary(export.LibraryName) && PreferLleForLibcExport(export.Name);
if (!ShouldResolveRegisteredExportViaLle(export, preferLleForLibc))
{
if (_logAllImports)
if (_logAllImports && IsKernelLibrary(export.LibraryName))
{
Console.Error.WriteLine($"[LOADER][DEBUG] TryResolveDirectImportTarget: {nid} ({export.LibraryName}:{export.Name}) -> HLE (kernel library)");
}
return false;
}
if (!IsLibcLibrary(export.LibraryName) || !PreferLleForLibcExport(export.Name))
{
return false;
}
if (TryResolveRuntimeSymbolAddress(nid, out var value2) && IsDirectImportTargetUsable(value2))
{
targetAddress = value2;
@@ -1703,6 +1701,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
internal static bool ShouldResolveRegisteredExportViaLle(
ExportedFunction export,
bool preferLleForLibc)
{
ArgumentNullException.ThrowIfNull(export);
return !IsKernelLibrary(export.LibraryName) && (export.PreferLle || preferLleForLibc);
}
private static bool IsHlePreferredNid(string nid)
{
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
@@ -3200,7 +3206,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
nint address = (nint)(ptr + i);
int remainingBytes = scanBytes - i;
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes))
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes, i))
{
num3++;
}
@@ -3343,13 +3349,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true;
}
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength)
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength, int regionOffset)
{
if (availableLength < MinTlsPatchInstructionBytes)
{
return false;
}
var region = new ReadOnlySpan<byte>(source - regionOffset, regionOffset + availableLength);
if (IsTlsLoadCandidateInsideShortJump(region, regionOffset))
{
return false;
}
var offset = 0;
while (offset < availableLength && source[offset] == 0x66)
{
@@ -3402,6 +3414,76 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return PatchTlsLoadInstruction(address, instructionLength, destinationRegister);
}
internal static bool IsTlsLoadCandidateInsideShortJump(ReadOnlySpan<byte> region, int candidateOffset)
{
if ((uint)candidateOffset >= (uint)region.Length ||
candidateOffset < 1 ||
region[candidateOffset - 1] != 0xEB)
{
return false;
}
// Accept EB when it is an aligned rel8 operand.
if (IsRel8ControlFlowInstructionEndingAtCandidate(region, candidateOffset))
{
return false;
}
return true;
}
private static bool IsRel8ControlFlowInstructionEndingAtCandidate(
ReadOnlySpan<byte> region,
int candidateOffset)
{
if (candidateOffset < 2)
{
return false;
}
var branchOffset = candidateOffset - 2;
var opcode = region[branchOffset];
if (!((opcode >= 0x70 && opcode <= 0x7F) ||
opcode is >= 0xE0 and <= 0xE3 ||
opcode == 0xEB))
{
return false;
}
var branchTarget = candidateOffset + (sbyte)region[candidateOffset - 1];
if (branchTarget < 0 || branchTarget >= branchOffset)
{
return false;
}
// Require an aligned instruction stream.
var decoder = Decoder.Create(
64,
new ByteArrayCodeReader(region[branchTarget..candidateOffset].ToArray()));
decoder.IP = (ulong)branchTarget;
while (decoder.IP < (ulong)candidateOffset)
{
var instructionOffset = (int)decoder.IP;
decoder.Decode(out var instruction);
if (instruction.Code == Code.INVALID || instruction.Length <= 0)
{
return false;
}
if (instructionOffset == branchOffset)
{
return instruction.Length == 2 && decoder.IP == (ulong)candidateOffset;
}
if (decoder.IP > (ulong)branchOffset)
{
return false;
}
}
return false;
}
private unsafe bool PatchTlsLoadInstruction(nint address, int instructionLength, int destinationRegister)
{
uint flNewProtect = default(uint);
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
@@ -18,7 +19,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private readonly object _allocationSearchHintGate = new();
private readonly List<MemoryRegion> _regions = new();
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private readonly ConcurrentDictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private bool _disposed;
[ThreadStatic]
@@ -28,7 +29,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private const ulong PageSize = 0x1000;
private const ulong HostAllocationGranularity = 0x10000;
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
private const ulong GuestAllocationArenaSize = 0x0100_0000;
// Full C++ runtimes can route large numbers of HLE-backed heap allocations
// through this arena. The original 16 MiB capacity could be exhausted
// during asset setup, turning a valid allocation request into a null
// pointer that failed later in unrelated guest code. Keep enough capacity
// for those workloads. Adapted from foufouadi's allocator-exhaustion
// investigation.
private const ulong GuestAllocationArenaSize = 0x2000_0000;
private const ulong GuestAllocationArenaStartOffset = PageSize;
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
private const ulong FullCommitRegionLimit = 4UL << 30;
+213
View File
@@ -0,0 +1,213 @@
{
"_languageName": "Eesti",
"Page.Library": "Teek",
"Page.Options": "Seaded",
"Page.GameCount.One": "1 mäng",
"Page.GameCount.Other": "{0} mängu",
"Library.SearchWatermark": "Otsi teegist…",
"Library.AddFolder": "Lisa kaust",
"Library.OpenFile": "Ava fail…",
"Library.View.Grid": "Ruudustikvaade",
"Library.View.Carousel": "Karussellvaade",
"Library.Context.Launch": "Käivita",
"Library.Context.OpenFolder": "Ava mängukaust",
"Library.Context.CopyPath": "Kopeeri tee",
"Library.Context.CopyTitleId": "Kopeeri Title ID",
"Library.Context.GameSettings": "Mängu seaded…",
"Library.Context.Remove": "Eemalda teegist",
"Library.Empty.Title": "Sinu teek on tühi",
"Library.Empty.Hint": "Alustamiseks lisa kaust, kus sinu mängud asuvad.",
"Library.Empty.SearchTitle": "Otsingule ei vasta ükski mäng",
"Library.Empty.SearchHint": "Ükski teegis olev mäng ei vasta otsingule “{0}”.",
"Library.Empty.AddFolder": " Lisa mängukaust",
"Library.Loading": "Teeki laaditakse…",
"Library.Stat.Version": "Versioon",
"Library.Stat.Installed": "Paigaldatud",
"Library.Stat.TitleId": "Title ID",
"Common.Back": "Tagasi",
"Options.General": "Üldine",
"Options.Logging": "Logimine",
"Options.Env.Tab": "Keskkond",
"Options.Section.Environment": "KESKKONNAMUUTUJAD",
"Options.Env.Desc": "Lülitid, mis edastatakse emulaatorile käivitamisel keskkonnamuutujatena.",
"Options.Env.Bthid.Desc": "Märgi Bluetooth HID kättesaamatuks mängude puhul, mille rooli/FFB-vahetarkvara küsib andmeid lõputult.\nTavaliselt jäta välja lülitatuks. Mõni mäng hangub, kui initsialiseerimine ebaõnnestub.",
"Options.Env.LoopGuard.Desc": "Ära sulge sundkorras mänge, mis kordavad sama kutset liiga kaua.\nProovi seda, kui mäng laadimise ajal iseenesest sulgub.",
"Options.Env.WritableApp0.Desc": "Luba mängudel luua ja kirjutada faile oma paigalduskausta.\nVajalik pakendamata koopiatele, mis salvestavad oma andmed või seadistused kausta /app0.",
"Options.Env.VkValidation.Desc": "Luba Vulkani valideerimiskihid GPU silumiseks.\nAeglane. Nõuab paigaldatud Vulkan SDK-d.",
"Options.Env.DumpSpirv.Desc": "Salvesta AGC-shaderid ja nende SPIR-V tõlked kausta shader-dumps.\nKasuta shaderi- või kuvamisvea teatamisel.",
"Options.Env.LogDirectMemory.Desc": "Logi konsooli otsese mälu eraldamised ja tõrked.\nKasuta siis, kui mäng katkeb või sulgub käivitamise ajal.",
"Options.Env.LogIo.Desc": "Logi konsooli failide avamise, lugemise ja tee lahendamise tegevused.\nKasuta siis, kui mäng ei leia käivitamisel oma andmefaile.",
"Options.Env.LogNp.Desc": "Logi konsooli NP (PlayStation Network) teegi kutsed.",
"Options.Env.Group.Debug": "Silumine",
"Options.Env.Group.General": "Üldine",
"Options.Env.RenderDoc.Desc": "Laadib RenderDoc'i rakendusesisese API, et kaadreid saaks hõivata otse emulaatorist.\nVajuta mängu käigus F10 ühe kaadri hõivamiseks; hõivatised salvestatakse kausta user/logs/capture_logs/<TITLE_ID>.\nNõuab paigaldatud RenderDoc'i. Aeglustab GPU-d ja põhjustab mõningate mängude hangumist, seega hoia välja lülitatuna, kui sa ei silu.",
"Options.Env.GuestImageCpuSync.Desc": "Laadi uuesti üles mängupinnad, mida selle enda CPU-kood ümber kirjutab.\nTavaliselt jäta välja lülitatuks. Lülita sisse mängude puhul, mille CPU-ga joonistatud pinnad ei jõua kunagi ekraanile.\nVähendab jõudlust ja halvendab mõne mängu töökindlust (nt GTA V).",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Edasta GPU käsupuhvrite preambulid isegi siis, kui nende sihtjärjekord neid kunagi ei võta.\nTavaliselt jäta välja lülitatuks. Lülita sisse mängude puhul, mis hanguvad oodates GPU-fence'i, mis kunagi ei rakendu.",
"Options.DefaultProfile.Label": "Vaikimisi profiili nimi",
"Options.DefaultProfile.Desc": "Nimi, mida kasutatakse, kui mäng küsib tekstisisestust. Vaikimisi Sharp.",
"Options.Section.Emulation": "EMULATSIOON",
"Options.Section.Logging": "LOGIMINE",
"Options.Section.Launcher": "KÄIVITI",
"Options.Section.Rendering": "RENDERDAMINE",
"Options.Section.Display": "KUVA",
"Options.Graphics": "Graafika",
"Options.RenderResolution.Label": "Sisemine eraldusvõime",
"Options.RenderResolution.Desc": "Renderdab ekraanivälised sihid natiivsest madalamal eraldusvõimel ja skaleerib kuvamisel üles. Madalamad väärtused annavad pildikvaliteedi arvelt GPU-le jõudlusvaru; rakendub järgmisel käivitamisel.",
"Options.RenderResolution.Native": "100% (natiivne)",
"Options.WindowMode.Label": "Aknarežiim",
"Options.WindowMode.Desc": "Tavaline aken, äärteta töölauavaade või eksklusiivne täisekraan.",
"Options.WindowMode.Windowed": "Aken",
"Options.WindowMode.Borderless": "Äärteta",
"Options.WindowMode.Exclusive": "Eksklusiivne",
"Options.Resolution.Label": "Eraldusvõime",
"Options.Resolution.Desc": "Algne akna suurus või eksklusiivse täisekraani eraldusvõime.",
"Options.Display.Label": "Kuvar",
"Options.Display.Desc": "Monitor, mida kasutatakse tsentreerimiseks ja täisekraanrežiimiks.",
"Options.RefreshRate.Label": "Värskendussagedus",
"Options.RefreshRate.Desc": "Eksklusiivse täisekraani värskendussagedus. Automaatne valib lähima režiimi.",
"Options.RefreshRate.Automatic": "Automaatne",
"Options.Scaling.Label": "Skaleerimine",
"Options.Scaling.Desc": "Skaleeri algset mängupilti muutmata selle sisemist eraldusvõimet.",
"Options.Scaling.Fit": "Mahuta",
"Options.Scaling.Cover": "Kata",
"Options.Scaling.Stretch": "Venita",
"Options.Scaling.Integer": "Täisarvuline",
"Options.VSync.Label": "VSync",
"Options.VSync.Desc": "Kasuta FIFO-esitust pildi rebimise vältimiseks.",
"Options.Hdr.Label": "HDR-väljund",
"Options.Hdr.Desc": "Kasuta HDR-i, kui valitud kuvar ja graafikaliides seda toetavad. Automaatrežiim lülitub tagasi SDR-ile.",
"Options.Hdr.Auto": "Automaatne",
"Options.CpuEngine.Label": "CPU-mootor",
"Options.CpuEngine.Desc": "Käitusmootor, mida kasutatakse mängukoodi käivitamiseks.",
"Options.CpuEngine.Native": "Natiivne",
"Options.Strict.Label": "Range dynlib-lahendamine",
"Options.Strict.Desc": "Katkesta käivitamine, kui imporditud sümbolit ei õnnestu lahendada.",
"Options.LogLevel.Label": "Logimise tase",
"Options.LogLevel.Desc": "Emulaatori konsooliväljundi üksikasjalikkus.",
"Options.LogLevel.Trace": "Trace (Jälitus)",
"Options.LogLevel.Debug": "Debug (Silumine)",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Hoiatus",
"Options.LogLevel.Error": "Viga",
"Options.LogLevel.Critical": "Kriitiline",
"Options.TraceImports.Label": "Impordi jälituse piir",
"Options.TraceImports.Desc": "Jälita iga mooduli esimesi N importi (0 = väljas).",
"Options.LogToFile.Label": "Logi faili",
"Options.LogToFile.Desc": "Kopeeri emulaatori väljund logifaili.",
"Options.LogFilePath.Label": "Logifaili tee",
"Options.LogFilePath.Default": "Kohandatud tee puudub — logid salvestatakse emulaatori kõrvale kausta user/logs.",
"Options.LogFilePath.Select": "Vali…",
"Options.OverrideLogFile.Label": "Kirjuta logifail üle",
"Options.OverrideLogFile.Desc": "Kasuta täpset failiteed selle asemel, et lisada Title ID ja ajatempel.",
"Options.TitleMusic.Label": "Mängu muusika",
"Options.TitleMusic.Desc": "Esita valitud mängu eelvaatemuusikat teegis korduvalt.",
"Options.Discord.Label": "Discordi olek",
"Options.Discord.Desc": "Näita käimasolevat mängu oma Discordi profiilis.",
"Options.Language.Label": "Emulaatori keel",
"Options.Language.Desc": "Käivitis kasutatav keel. Rakendub kohe.",
"Common.On": "Sisse",
"Common.Off": "Välja",
"Common.Save": "Salvesta",
"Common.Cancel": "Tühista",
"Console.Title": "KONSOOL",
"Console.SearchWatermark": "Otsi…",
"Console.AutoScroll": "Automaatne kerimine",
"Console.Split": "Poolita",
"Console.Copy": "Kopeeri",
"Console.Clear": "Tühjenda",
"Console.WindowTitle": "SharpEmu konsool",
"Launch.NoGameSelected": "Mäng pole valitud",
"Launch.NoGameHint": "Vali mäng teegist või ava eboot.bin fail otse.",
"Launch.Idle": "Ootel",
"Launch.Console": "≡ Konsool",
"Launch.Launch": "▶ Käivita",
"Launch.Stop": "■ Peata",
"Launch.Running": "Käib — {0}",
"Launch.Stopping": "Peatamine…",
"Launch.Exited": "Väljus koodiga {0} ({1})",
"Launch.ExeNotFound": "SharpEmu käivitavat faili ei leitud. Ehita esmalt SharpEmu.CLI projekt (dotnet build).",
"Launch.LogFile": "Logifail: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emulaatori käivitamine nurjus: {0}",
"Launch.ProcessExited": "Protsess väljus koodiga {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "vigased argumendid",
"Exit.EbootNotFound": "eboot-faili ei leitud",
"Exit.RuntimeException": "käitusaegne erand",
"Exit.EmulationError": "emulatsioonitõrge",
"Exit.Unknown": "tundmatu",
"Status.EmulatorLocating": "Emulaator: otsimine…",
"Status.EmulatorPath": "Emulaator: {0}",
"Status.EmulatorNotFound": "Emulaator: SharpEmu käivitavat faili ei leitud — ehita esmalt SharpEmu.CLI.",
"Status.ScanningLibrary": "Teegi skaneerimine…",
"Status.AddFolderPrompt": "Lisa mängukaust teegi täitmiseks.",
"Status.LibraryScanned": "Teek skaneeritud: {0} mängu {1} kaustas.",
"Status.CouldNotOpenFolder": "Kausta ei saanud avada: {0}",
"Status.CopiedToClipboard": "{0} kopeeritud lõikelauale.",
"Status.RemovedFromLibrary": "Eemaldati “{0}” teegist. Taastamiseks lisa selle kaust uuesti.",
"Status.Running": "Käib: {0}",
"Status.Stopping": "Peatamine…",
"Status.Idle": "Ootel",
"Clipboard.Path": "Asukoht",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Mängib: {0}",
"Discord.Browsing": "Sirvib teeki",
"Dialog.ChooseGameFolder": "Vali mänge sisaldav kaust",
"Dialog.OpenExecutable": "Ava käivitatav fail käivitamiseks",
"Dialog.PsExecutables": "PS käivitusfailid",
"Dialog.SaveLogFile": "Vali logifaili salvestamiskoht",
"Dialog.PlainTextFiles": "Tavatekstifailid",
"Dialog.LogFiles": "Logifailid",
"Options.About": "Teave",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Lähtekood, vearaportid ja projekti arendus.",
"About.Github.LatestCommitLabel": "Viimane muudatus (commit)",
"About.Github.LatestCommitDescription": "Viimane muudatus peaharul",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Liitu kogukonnaga, saa tuge ja jälgi arendust.",
"About.GithubButton": "Löö GitHubis kaasa!",
"About.DiscordComingSoon": "Tulekul",
"Updater.Auto.Label": "Kontrolli värskendusi käivitamisel",
"Updater.Auto.Desc": "Kontrollib GitHubist värskendusi käivitamist viivitamata.",
"Updater.Label": "Värskendused",
"Updater.Check": "Kontrolli värskendusi",
"Updater.DownloadRestart": "Laadi alla ja taaskäivita",
"Updater.Status.Ready": "Praegune versioon: {0}",
"Updater.Status.Checking": "Värskenduste kontrollimine…",
"Updater.Status.Current": "Sinu versioon on ajakohane ({0}).",
"Updater.Status.Available": "Uus versioon on saadaval: {0}",
"Updater.Status.Downloading": "Värskenduse allalaadimine… {0}%",
"Updater.Status.Installing": "Värskenduse paigaldamine…",
"Updater.Status.Timeout": "Värskenduste kontroll aegus 10 sekundi järel.",
"Updater.Status.Failed": "Värskendusi ei saanud kontrollida.",
"Updater.Status.ChecksumFailed": "Allalaaditud värskendus ei läbinud SHA-256 kontrolli.",
"Updater.Status.Unsupported": "Automaatne värskendamine nõuab Windowsi, Linuxi või macOS-i x64 versiooni."
}
+213
View File
@@ -0,0 +1,213 @@
{
"_languageName": "Polski",
"Page.Library": "Biblioteka",
"Page.Options": "Opcje",
"Page.GameCount.One": "1 gra",
"Page.GameCount.Other": "{0} gier",
"Library.SearchWatermark": "Szukaj w bibliotece…",
"Library.AddFolder": "Dodaj folder",
"Library.OpenFile": "Otwórz plik…",
"Library.View.Grid": "Widok siatki",
"Library.View.Carousel": "Widok listy",
"Library.Context.Launch": "Uruchom",
"Library.Context.OpenFolder": "Otwórz folder gry",
"Library.Context.CopyPath": "Kopiuj ścieżkę",
"Library.Context.CopyTitleId": "Kopiuj numer seryjny",
"Library.Context.GameSettings": "Ustawienia gry…",
"Library.Context.Remove": "Usuń z biblioteki",
"Library.Empty.Title": "Twoja biblioteka jest pusta",
"Library.Empty.Hint": "Dodaj folder zawierający twoje gry, aby rozpocząć.",
"Library.Empty.SearchTitle": "Brak gier pasujących do twojego wyszukiwania",
"Library.Empty.SearchHint": "Nic w bibliotece nie pasuje do “{0}”.",
"Library.Empty.AddFolder": " Dodaj folder z grami",
"Library.Loading": "Ładowanie biblioteki…",
"Library.Stat.Version": "Wersja",
"Library.Stat.Installed": "Zainstalowano",
"Library.Stat.TitleId": "Numer seryjny",
"Common.Back": "Wstecz",
"Options.General": "Ogólne",
"Options.Logging": "Dziennik zdarzeń",
"Options.Env.Tab": "Środowisko",
"Options.Section.Environment": "ZMIENNE ŚRODOWISKOWE",
"Options.Env.Desc": "Przełączniki przekazywane emulatorowi jako zmienne środowiskowe przy uruchomieniu.",
"Options.Env.Bthid.Desc": "Zgłaszaj Bluetooth HID jako niedostępny dla tytułów, których middleware kierownicy/FFB odpytuje w nieskończoność.\nZostaw wyłączone normalnie. Niektóre tytuły zawieszają się, gdy inicjalizacja zawiedzie.",
"Options.Env.LoopGuard.Desc": "Nie wymuszaj zamknięcia tytułów, które powtarzają to samo wywołanie zbyt długo.\nSpróbuj tego, gdy gra zamyka się sama podczas ładowania.",
"Options.Env.WritableApp0.Desc": "Pozwól tytułom tworzyć i zapisywać pliki wewnątrz folderu instalacji.\nPotrzebne w przypadku niespakowanych zrzutów, które zapisują dane zapisu lub konfiguracji w /app0.",
"Options.Env.VkValidation.Desc": "Włącz warstwy walidacji Vulkan do debugowania GPU.\nWolne. Wymaga zainstalowanego Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "Zrzuć shadery AGC i ich translacje SPIR-V do folderu shader-dumps.\nUżyj przy zgłaszaniu błędów shaderów lub renderowania.",
"Options.Env.LogDirectMemory.Desc": "Loguj alokacje pamięci bezpośredniej i błędy do konsoli.\nUżyj, gdy gra przerywa działanie lub zamyka się podczas uruchamiania.",
"Options.Env.LogIo.Desc": "Loguj otwieranie plików, odczyt i rozwiązywanie ścieżek do konsoli.\nUżyj, gdy gra nie może znaleźć swoich plików danych podczas uruchamiania.",
"Options.Env.LogNp.Desc": "Loguj wywołania biblioteki NP (PlayStation Network) do konsoli.",
"Options.Env.Group.Debug": "Debugowanie",
"Options.Env.Group.General": "Ogólne",
"Options.Env.RenderDoc.Desc": "Załaduj API RenderDoc w aplikacji, aby można było przechwytywać klatki z wnętrza emulatora.\nNaciśnij F10 podczas działania gry, aby przechwycić jedną klatkę; przechwycone klatki trafiają do user/logs/capture_logs/<TITLE_ID>.\nWymaga zainstalowanego RenderDoc. Spowalnia GPU i zawiesza niektóre tytuły, więc zostaw wyłączone, chyba że debugujesz.",
"Options.Env.GuestImageCpuSync.Desc": "Ponownie przesyłaj powierzchnie gościa, które procesor gry nadpisuje.\nZostaw wyłączone normalnie. Włącz dla tytułów, których powierzchnie rysowane przez CPU nigdy nie docierają na ekran.\nKosztuje wydajność i pogarsza działanie niektórych tytułów, takich jak GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Dostarczaj preambuły buforów poleceń GPU nawet gdy ich docelowa kolejka ich nie odbiera.\nZostaw wyłączone normalnie. Włącz dla tytułów, które zawieszają się w oczekiwaniu na sygnał z GPU.",
"Options.DefaultProfile.Label": "Domyślna nazwa profilu",
"Options.DefaultProfile.Desc": "Nazwa używana, gdy gra prosi o wprowadzenie tekstu. Domyślnie Sharp.",
"Options.Section.Emulation": "EMULACJA",
"Options.Section.Logging": "DZIENNIK ZDARZEŃ",
"Options.Section.Launcher": "LAUNCHER",
"Options.Section.Rendering": "RENDEROWANIE",
"Options.Section.Display": "WYŚWIETLANIE",
"Options.Graphics": "Grafika",
"Options.RenderResolution.Label": "Rozdzielczość wewnętrzna",
"Options.RenderResolution.Desc": "Renderuj cele pozaekranowe poniżej natywnej rozdzielczości i skaluj w górę przy prezentacji. Niższe wartości zamieniają jakość obrazu na zapas GPU; działa od następnego uruchomienia.",
"Options.RenderResolution.Native": "100% (natywna)",
"Options.WindowMode.Label": "Tryb okna",
"Options.WindowMode.Desc": "Zwykłe okno, bezramkowe na pulpicie lub pełny ekran wyłączny.",
"Options.WindowMode.Windowed": "W oknie",
"Options.WindowMode.Borderless": "Bezramkowe",
"Options.WindowMode.Exclusive": "Wyłączne",
"Options.Resolution.Label": "Rozdzielczość",
"Options.Resolution.Desc": "Początkowy rozmiar okna lub rozdzielczość pełnego ekranu wyłącznego.",
"Options.Display.Label": "Wyświetlacz",
"Options.Display.Desc": "Monitor używany do centrowania i trybu pełnoekranowego.",
"Options.RefreshRate.Label": "Częstotliwość odświeżania",
"Options.RefreshRate.Desc": "Częstotliwość odświeżania w trybie pełnego ekranu wyłącznego. Automatyczna wybiera najbliższy tryb.",
"Options.RefreshRate.Automatic": "Automatyczna",
"Options.Scaling.Label": "Skalowanie",
"Options.Scaling.Desc": "Skaluj natywny obraz gościa bez zmiany jego wewnętrznej rozdzielczości.",
"Options.Scaling.Fit": "Dopasuj",
"Options.Scaling.Cover": "Wypełnij",
"Options.Scaling.Stretch": "Rozciągnij",
"Options.Scaling.Integer": "Całkowitoliczbowe",
"Options.VSync.Label": "VSync",
"Options.VSync.Desc": "Używaj prezentacji FIFO dla obrazu bez rozrywania.",
"Options.Hdr.Label": "Wyjście HDR",
"Options.Hdr.Desc": "Używaj HDR, gdy wybrany wyświetlacz i backend graficzny to obsługują. Automatyczny przełącza na SDR.",
"Options.Hdr.Auto": "Automatyczny",
"Options.CpuEngine.Label": "Silnik CPU",
"Options.CpuEngine.Desc": "Silnik wykonawczy używany do uruchamiania kodu gry.",
"Options.CpuEngine.Native": "Natywny",
"Options.Strict.Label": "Ścisłe rozwiązywanie dynlib",
"Options.Strict.Desc": "Przerwij uruchomienie, gdy importowany symbol nie może zostać rozwiązany.",
"Options.LogLevel.Label": "Poziom logowania",
"Options.LogLevel.Desc": "Szczegółowość wyjścia konsoli emulatora.",
"Options.LogLevel.Trace": "Śledzenie",
"Options.LogLevel.Debug": "Debugowanie",
"Options.LogLevel.Info": "Informacja",
"Options.LogLevel.Warning": "Ostrzeżenie",
"Options.LogLevel.Error": "Błąd",
"Options.LogLevel.Critical": "Krytyczny",
"Options.TraceImports.Label": "Limit śledzenia importów",
"Options.TraceImports.Desc": "Śledź pierwszych N importów na moduł (0 = wyłączone).",
"Options.LogToFile.Label": "Loguj do pliku",
"Options.LogToFile.Desc": "Kopiuj wyjście emulatora do pliku logu.",
"Options.LogFilePath.Label": "Ścieżka pliku logu",
"Options.LogFilePath.Default": "Brak niestandardowej ścieżki — logi trafiają do user/logs obok emulatora.",
"Options.LogFilePath.Select": "Wybierz…",
"Options.OverrideLogFile.Label": "Nadpisz plik logu",
"Options.OverrideLogFile.Desc": "Użyj dokładnej ścieżki pliku zamiast dopisywania ID tytułu i znacznika czasu.",
"Options.TitleMusic.Label": "Muzyka tytułowa",
"Options.TitleMusic.Desc": "Odtwarzaj w pętli muzykę podglądu wybranej gry w bibliotece.",
"Options.Discord.Label": "Discord Rich Presence",
"Options.Discord.Desc": "Pokazuj uruchomioną grę na swoim profilu Discord.",
"Options.Language.Label": "Język emulatora",
"Options.Language.Desc": "Język używany w całym launcherze. Działa natychmiast.",
"Common.On": "Włącz",
"Common.Off": "Wyłącz",
"Common.Save": "Zapisz",
"Common.Cancel": "Anuluj",
"Console.Title": "KONSOLA",
"Console.SearchWatermark": "Szukaj...",
"Console.AutoScroll": "Autoprzewijanie",
"Console.Split": "Podziel",
"Console.Copy": "Kopiuj",
"Console.Clear": "Wyczyść",
"Console.WindowTitle": "Konsola SharpEmu",
"Launch.NoGameSelected": "Nie wybrano gry",
"Launch.NoGameHint": "Wybierz grę z biblioteki lub otwórz bezpośrednio plik eboot.bin.",
"Launch.Idle": "Bezczynny",
"Launch.Console": "≡ Konsola",
"Launch.Launch": "▶ Uruchom",
"Launch.Stop": "■ Zatrzymaj",
"Launch.Running": "Działa — {0}",
"Launch.Stopping": "Zatrzymywanie…",
"Launch.Exited": "Zakończono z kodem {0} ({1})",
"Launch.ExeNotFound": "Nie znaleziono pliku wykonywalnego SharpEmu. Najpierw zbuduj projekt SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Plik logu: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Nie udało się uruchomić emulatora: {0}",
"Launch.ProcessExited": "Proces zakończony z kodem {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "nieprawidłowe argumenty",
"Exit.EbootNotFound": "nie znaleziono eboot",
"Exit.RuntimeException": "wyjątek czasu wykonania",
"Exit.EmulationError": "błąd emulacji",
"Exit.Unknown": "nieznany",
"Status.EmulatorLocating": "Emulator: lokalizowanie…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: nie znaleziono pliku wykonywalnego SharpEmu — najpierw zbuduj SharpEmu.CLI.",
"Status.ScanningLibrary": "Skanowanie biblioteki…",
"Status.AddFolderPrompt": "Dodaj folder z grami, aby wypełnić bibliotekę.",
"Status.LibraryScanned": "Biblioteka przeskanowana: {0} gier w {1} folderach.",
"Status.CouldNotOpenFolder": "Nie można otworzyć folderu: {0}",
"Status.CopiedToClipboard": "{0} skopiowano do schowka.",
"Status.RemovedFromLibrary": "Usunięto \u201c{0}\u201d z biblioteki. Dodaj ponownie folder, aby przywrócić.",
"Status.Running": "Uruchomiono {0}",
"Status.Stopping": "Zatrzymywanie…",
"Status.Idle": "Bezczynny",
"Clipboard.Path": "Ścieżka",
"Clipboard.TitleId": "ID tytułu",
"Discord.Playing": "Gra w {0}",
"Discord.Browsing": "Przegląda bibliotekę",
"Dialog.ChooseGameFolder": "Wybierz folder zawierający gry",
"Dialog.OpenExecutable": "Otwórz plik wykonywalny do uruchomienia",
"Dialog.PsExecutables": "Pliki wykonywalne PS",
"Dialog.SaveLogFile": "Wybierz, gdzie zapisać plik logu",
"Dialog.PlainTextFiles": "Pliki tekstowe",
"Dialog.LogFiles": "Pliki logów",
"Options.About" : "O programie",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Kod źródłowy, zgłoszenia i rozwój projektu.",
"About.Github.LatestCommitLabel": "Najnowszy commit",
"About.Github.LatestCommitDescription": "Najnowszy commit na gałęzi głównej",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Dołącz do społeczności, uzyskaj wsparcie i śledź rozwój.",
"About.GithubButton": "Wspieraj na GitHubie!",
"About.DiscordComingSoon": "Wkrótce",
"Updater.Auto.Label": "Sprawdzaj aktualizacje przy uruchomieniu",
"Updater.Auto.Desc": "Sprawdza GitHub bez opóźniania uruchomienia.",
"Updater.Label": "Aktualizacje",
"Updater.Check": "Sprawdź aktualizacje",
"Updater.DownloadRestart": "Pobierz i uruchom ponownie",
"Updater.Status.Ready": "Obecna wersja: {0}",
"Updater.Status.Checking": "Sprawdzanie aktualizacji…",
"Updater.Status.Current": "Masz najnowszą wersję ({0}).",
"Updater.Status.Available": "Dostępna nowa wersja: {0}",
"Updater.Status.Downloading": "Pobieranie aktualizacji… {0}%",
"Updater.Status.Installing": "Instalowanie aktualizacji…",
"Updater.Status.Timeout": "Sprawdzanie aktualizacji przekroczyło limit 10 sekund.",
"Updater.Status.Failed": "Nie udało się sprawdzić aktualizacji.",
"Updater.Status.ChecksumFailed": "Pobrana aktualizacja nie przeszła weryfikacji SHA-256.",
"Updater.Status.Unsupported": "Automatyczna aktualizacja wymaga wersji dla Windows, Linux lub macOS x64."
}
+14 -1
View File
@@ -5,7 +5,13 @@ namespace SharpEmu.HLE;
public sealed class ExportedFunction
{
public ExportedFunction(string libraryName, string nid, string name, Generation target, SysAbiFunction function)
public ExportedFunction(
string libraryName,
string nid,
string name,
Generation target,
SysAbiFunction function,
bool preferLle = false)
{
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
@@ -17,6 +23,7 @@ public sealed class ExportedFunction
Name = name;
Target = target;
Function = function;
PreferLle = preferLle;
}
public string LibraryName { get; }
@@ -28,4 +35,10 @@ public sealed class ExportedFunction
public Generation Target { get; }
public SysAbiFunction Function { get; }
/// <summary>
/// A loaded guest export is authoritative for this registration. The HLE function
/// remains available as an explicit fallback when no usable guest target exists.
/// </summary>
public bool PreferLle { get; }
}
+8 -1
View File
@@ -3,7 +3,7 @@
namespace SharpEmu.HLE;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class SysAbiExportAttribute : Attribute
{
public string LibraryName { get; set; } = "libKernel";
@@ -13,4 +13,11 @@ public sealed class SysAbiExportAttribute : Attribute
public string ExportName { get; set; } = string.Empty;
public Generation Target { get; set; } = Generation.None;
/// <summary>
/// Prefer a matching export from a loaded guest module and use this handler only
/// as the explicit fallback when that LLE provider is unavailable. Individual
/// handlers define whether that fallback is fail-closed or compatibility behavior.
/// </summary>
public bool PreferLle { get; set; }
}
+330 -14
View File
@@ -118,6 +118,29 @@ public static partial class AgcExports
// Multiple producers can share one target label; last-writer-wins would
// starve waits on the others.
private static readonly Dictionary<ulong, List<ulong>> _cbReleaseMemTargets = new();
// CMASK meta-state tracking: maps colour-buffer addresses to their
// compression metadata. Keyed by colour-buffer base address so the
// consumption path (which only knows the surface address) can query
// directly without a reverse lookup.
private record struct MetaSurfaceInfo(
ulong CmaskAddress,
uint ClearWord0,
uint ClearWord1,
bool IsCleared);
private static readonly Dictionary<ulong, MetaSurfaceInfo> _metaSurfaces = new();
// Reverse map: CMASK address → colour-buffer address. Needed so
// CheckCmaskWrite (which only sees the write target address) can
// find the owning surface.
private static readonly Dictionary<ulong, ulong> _cmaskToColorBuffer = new();
// Guards _metaSurfaces and _cmaskToColorBuffer. Two threads touch them:
// the parse thread (registration in TrackCmaskAddresses, CheckCmaskWrite
// from DMA/compute writes, EFC consumption) and the render thread
// (MarkAllSurfacesCleared at guest flip, IsMetaClearedForSurface /
// ConsumeMetaClear / GetMetaClearValue at pass-record time). Plain
// Dictionaries corrupt under concurrent write, so every access below
// holds this gate. Keep the critical sections tiny and never block on
// anything external while holding it.
private static readonly object _metaSurfaceGate = new();
// header -> {ring base, write cursor} of the last submitted slice.
// Submissions stay cursor-bounded since rings aren't zeroed. Lap
// distinguishes a stale cursor from a previous pass over the same base.
@@ -1095,15 +1118,20 @@ public static partial class AgcExports
private const uint CbColor0Base = 0x318;
private const uint CbColorRegisterStride = 15;
private const uint CbColor0Info = 0x31C;
private const uint CbColor0Cmask = 0x31F;
private const uint CbColor0ClearWord0 = 0x323;
private const uint CbColor0ClearWord1 = 0x324;
private const uint CbColor0DccBase = 0x325;
private const uint CbColor0BaseExt = 0x390;
private const uint CbColor0CmaskBaseExt = 0x398;
private const uint CbColor0DccBaseExt = 0x3A8;
private const uint CbColor0Attrib2 = 0x3B0;
private const uint CbColor0Attrib3 = 0x3B8;
// CB_COLORn_INFO.DCC_ENABLE (gc_10_1_0_sh_mask.h). On GFX10 the legacy
// FAST_CLEAR and COMPRESSION bits stay clear because DCC, not CMASK,
// carries the compression.
private const uint CbColorInfoDccEnableMask = 1u << 28;
private const uint CbColorInfoFastClearEnableMask = 1u << 12;
private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
@@ -1123,8 +1151,8 @@ public static partial class AgcExports
private const uint EsUserDataRegister = 0xCC;
private const uint ComputeUserDataRegister = 0x240;
private const uint NggUserDataScalarRegisterBase = 8;
private const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
internal const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
internal const uint Gen5TextureFormatR16G16B16A16Float = 12;
private const uint Gen5TextureType1D = 8;
private const uint Gen5TextureType2D = 9;
private const uint Gen5TextureType3D = 10;
@@ -5083,6 +5111,10 @@ public static partial class AgcExports
if (op == ItNop && register == RDmaData && length >= 7)
{
// Ensure CMASK addresses are tracked before DMA fills
var tempTargets = GetRenderTargets(state.CxRegisters);
TrackCmaskAddresses(state.CxRegisters, tempTargets);
ApplySubmittedDmaData(
ctx,
gpuState,
@@ -5243,6 +5275,7 @@ public static partial class AgcExports
{
TraceFramePacketSummary(state);
SyncCpuWrittenGuestImages(ctx);
GpuWaitRegistry.AdvanceFrame();
if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) ||
!TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) ||
!TryReadUInt32(ctx, currentAddress + 12, out var flipMode) ||
@@ -6264,6 +6297,13 @@ public static partial class AgcExports
ulong byteCount,
uint? fillValue)
{
// Check if this DMA write targets a CMASK address (shadPS4's FillBuffer
// logic: when a buffer fill targets CMASK metadata, mark it as "all clear")
if (fillValue is { } fillVal && fillVal == 0)
{
CheckCmaskWrite(destinationAddress, null);
}
var hasImage = GuestGpu.Current.TryGetGuestImageExtent(
destinationAddress,
out var width,
@@ -6478,6 +6518,25 @@ public static partial class AgcExports
var targetAddress = destinationAddress +
(incrementAddress ? (ulong)index * sizeof(uint) : 0);
wroteData = TryWriteUInt32(ctx, targetAddress, values[index]);
if (wroteData)
{
GpuWaitRegistry.RecordProduced(
ctx.Memory, targetAddress, values[index]);
}
}
// Like ReleaseMem dataSel=2: a 64-bit WAIT_REG_MEM watches an
// 8-byte label written as two 32-bit dwords. Record the combined
// 64-bit value so a 64-bit EQ can latch even though the writes
// landed as two 32-bit stores.
if (wroteData && dwordCount >= 2 && incrementAddress)
{
var combined = ((ulong)values[1] << 32) | values[0];
GpuWaitRegistry.RecordProduced(
ctx.Memory, destinationAddress, combined);
// Also latch the high half's address for symmetry: a stray
// 32-bit wait on the high dword should not be confused, but
// recording it does not hurt and mirrors the per-dword stores.
}
if (tracePacket)
@@ -7053,7 +7112,13 @@ public static partial class AgcExports
if (hasCurrent && GpuWaitRegistry.Compare(waiter, currentValue))
{
return false; // already satisfied — keep parsing
// Value satisfies the condition, but only bypass if the label was
// written in the current frame. A stale label from a previous frame
// means the producer hasn't written yet this frame — must wait.
if (GpuWaitRegistry.IsLabelFresh(ctx.Memory, waitAddress))
{
return false; // satisfied by current-frame write — keep parsing
}
}
if (!_gpuWaitSuspendEnabled)
@@ -8044,7 +8109,8 @@ public static partial class AgcExports
var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna);
var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr);
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
var renderTargets = GetRenderTargets(state.CxRegisters);
var renderTargets = GetRenderTargets(state.CxRegisters);
TrackCmaskAddresses(state.CxRegisters, renderTargets);
var drawSequence = ++gpuState.WorkSequence;
if (state.PendingTargetlessDraw is { } stalePendingDraw)
{
@@ -8064,6 +8130,31 @@ public static partial class AgcExports
if (TryGetCbColorControlMode(state.CxRegisters, out var cbMode) &&
IsCbMetadataColorMode(cbMode))
{
// EliminateFastClear: the game explicitly asks the CB to clear
// the fast-clear metadata and the colour buffer.
if (cbMode == (uint)CbColorMode.EliminateFastClear &&
renderTargets.Count > 0 &&
renderTargets[0].Address != 0)
{
var targetAddr = renderTargets[0].Address;
bool requestClear;
lock (_metaSurfaceGate)
{
requestClear =
_metaSurfaces.TryGetValue(targetAddr, out var meta) &&
meta.IsCleared;
if (requestClear)
{
_metaSurfaces[targetAddr] = meta with { IsCleared = false };
}
}
if (requestClear)
{
VulkanVideoPresenter.RequestGuestColorClear(targetAddr);
}
}
if (_traceAgcShader || ShouldTraceHotPath(ref _cbMetadataSkipTraceCount))
{
TraceAgcShader(
@@ -8278,6 +8369,34 @@ public static partial class AgcExports
return;
}
// DbRenderControl CLEARON (bit0): when set, the CB clears color
// targets on first draw. Handle color targets (depth is already
// handled by DecodeDepthState).
if (state.CxRegisters.TryGetValue(DbRenderControl, out var rc) && (rc & 0x1u) != 0)
{
foreach (var rt in translatedDraw.RenderTargets)
{
if (rt.Address != 0)
{
VulkanVideoPresenter.RequestGuestColorClear(rt.Address);
}
}
}
// CMASK fast clear: CB_COLORn_INFO.FAST_CLEAR (bit12) set on
// one or more targets. The CB clears via CMASK before the draw
// writes; mark targets for clear-on-first-use.
if (IsCmaskFastClearDraw(state.CxRegisters, translatedDraw.RenderTargets))
{
foreach (var rt in translatedDraw.RenderTargets)
{
if (rt.Address != 0)
{
VulkanVideoPresenter.RequestGuestColorClear(rt.Address);
}
}
}
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
if (firstTarget.Address != 0)
{
@@ -9570,6 +9689,193 @@ public static partial class AgcExports
CoversClipSpace(vertexInputs, vertexCount);
}
/// <summary>
/// GFX10 CMASK fast clear: CB_COLORn_INFO.FAST_CLEAR (bit 12) set on
/// one or more targets. The CB clears via CMASK before the draw writes;
/// mark targets for clear-on-first-use. Unlike DCC, the draw content
/// IS written (not dropped). Dead Cells uses DbRenderControl CLEARON
/// instead (bit0), not this mechanism.
/// </summary>
private static bool IsCmaskFastClearDraw(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> renderTargets)
{
foreach (var rt in renderTargets)
{
var stride = rt.Slot * CbColorRegisterStride;
if (registers.TryGetValue(CbColor0Info + stride, out var info) &&
(info & CbColorInfoFastClearEnableMask) != 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Registers the CMASK metadata mapping for each colour buffer.
/// Does NOT mark as cleared — clearing only happens on actual clear
/// events (DMA fill, compute write, EFC draw).
/// </summary>
private static void TrackCmaskAddresses(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> renderTargets)
{
foreach (var rt in renderTargets)
{
var stride = rt.Slot * CbColorRegisterStride;
// CMASK metadata address (legacy GCN path).
var cmaskRegAddr = CbColor0Cmask + stride;
registers.TryGetValue(cmaskRegAddr, out var cmaskLow);
var cmaskExtAddr = CbColor0CmaskBaseExt + rt.Slot;
registers.TryGetValue(cmaskExtAddr, out var cmaskExt);
var cmaskAddress = ((ulong)(cmaskExt & 0xFFu) << 40) |
((ulong)(cmaskLow & 0x1FFFFFFFu) << 8);
// DCC metadata address (GFX10+ primary path).
var dccRegAddr = CbColor0DccBase + stride;
registers.TryGetValue(dccRegAddr, out var dccLow);
var dccExtAddr = CbColor0DccBaseExt + rt.Slot;
registers.TryGetValue(dccExtAddr, out var dccExt);
var dccAddress = ((ulong)(dccExt & 0xFFu) << 40) |
((ulong)(dccLow & 0x1FFFFFFFu) << 8);
// Prefer CMASK if present; fall back to DCC.
var metaAddress = cmaskAddress != 0 ? cmaskAddress : dccAddress;
var cw0Addr = CbColor0ClearWord0 + stride;
var cw1Addr = CbColor0ClearWord1 + stride;
registers.TryGetValue(cw0Addr, out var cw0);
registers.TryGetValue(cw1Addr, out var cw1);
lock (_metaSurfaceGate)
{
_metaSurfaces[rt.Address] = new MetaSurfaceInfo(
metaAddress, cw0, cw1,
// Re-registration runs on every draw; keep the cleared state
// so a mark-clear event survives until the pass consumes it.
// If the metadata binding changed, the old state refers to
// the old metadata and must be reset.
IsCleared: _metaSurfaces.TryGetValue(rt.Address, out var prev) &&
prev.IsCleared &&
prev.CmaskAddress == metaAddress);
if (metaAddress != 0)
{
_cmaskToColorBuffer[metaAddress] = rt.Address;
}
}
}
}
/// <summary>
/// Checks if a write targets a registered CMASK address. If so,
/// marks the owning colour buffer's metadata as "all clear".
/// </summary>
private static void CheckCmaskWrite(
ulong writeAddress,
SubmittedGpuState? gpuState)
{
if (writeAddress == 0)
{
return;
}
lock (_metaSurfaceGate)
{
// Exact match: write directly to a registered CMASK address.
if (_cmaskToColorBuffer.TryGetValue(writeAddress, out var cbAddr))
{
if (_metaSurfaces.TryGetValue(cbAddr, out var meta))
{
_metaSurfaces[cbAddr] = meta with { IsCleared = true };
}
return;
}
// CMASK surfaces are small (typically ≤ 4 KiB). Check the ±1024
// window around each registered address to catch partial writes.
foreach (var (cmaskAddr, colorBufAddr) in _cmaskToColorBuffer)
{
if (writeAddress >= cmaskAddr && writeAddress < cmaskAddr + 1024)
{
if (_metaSurfaces.TryGetValue(colorBufAddr, out var meta))
{
_metaSurfaces[colorBufAddr] = meta with { IsCleared = true };
}
return;
}
}
}
}
/// <summary>
/// Returns true if the colour buffer at <paramref name="colorBufferAddress"/>
/// has pending CMASK "all clear" metadata — i.e. the surface was fast-cleared
/// but not yet rendered into.
/// </summary>
internal static bool IsMetaClearedForSurface(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
return _metaSurfaces.TryGetValue(colorBufferAddress, out var meta) &&
meta.IsCleared;
}
}
/// <summary>
/// Consumes the "all clear" state for the given surface, marking it dirty.
/// Called after the first render pass uses LoadOp.Clear.
/// </summary>
internal static void ConsumeMetaClear(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
if (_metaSurfaces.TryGetValue(colorBufferAddress, out var meta))
{
_metaSurfaces[colorBufferAddress] = meta with { IsCleared = false };
}
}
}
/// <summary>
/// Returns the CB clear word values for the given colour buffer.
/// </summary>
internal static (uint Cw0, uint Cw1) GetMetaClearValue(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
if (_metaSurfaces.TryGetValue(colorBufferAddress, out var meta))
{
return (meta.ClearWord0, meta.ClearWord1);
}
}
return (0, 0);
}
/// <summary>
/// Marks all registered surfaces as "all clear". Called at guest flip
/// (frame boundary). Real hardware applies a fast clear / load-clear to
/// its per-frame surfaces every frame; the emulator restores that
/// per-frame clear here, per surface, at flip time. This is the
/// per-surface successor of the removed flip-arm heuristic (which reset
/// only the first multi-attachment group).
/// </summary>
internal static void MarkAllSurfacesCleared()
{
lock (_metaSurfaceGate)
{
foreach (var (addr, meta) in _metaSurfaces)
{
_metaSurfaces[addr] = meta with { IsCleared = true };
}
}
}
/// <summary>
/// True when the draw's float32x3 position stream spans the full clip
/// rectangle, i.e. x and y both reach -1 and +1.
@@ -9982,7 +10288,7 @@ public static partial class AgcExports
private static readonly HashSet<ulong> _sampledRenderTargets = new();
private static readonly object _renderTargetProbeGate = new();
private static long _renderTargetSampleTraceCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawEmitCount;
private static long _indirectDrawEmitRejectCount;
private static long _indirectMultiProbeCount;
@@ -12174,15 +12480,18 @@ public static partial class AgcExports
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
{
// Indirect dispatches read their dimensions from a guest buffer a
// prior GPU dispatch fills. Zero here means that producer has not run
// yet — signal the caller to suspend on the dims buffer and retry,
// rather than dropping the work (which black-screens GPU-driven games
// like Astro Bot). Direct dispatches carry dims inline, so a zero is
// genuinely malformed and still rejected.
if (opcode == ItDispatchIndirect)
// For indirect dispatches (both absolute and base), zero dimensions are a valid outcome
// of GPU culling passes (0 workgroups). VulkanVideoPresenter handles groupCount = 0 as a clean no-op.
if (opcode == ItDispatchIndirect || dispatchSource is "absolute-indirect" or "base-indirect")
{
indirectDimsRetryAddress = dimensionsAddress;
var waveCount = (initiator & (1u << 15)) != 0 ? 32u : 64u;
dispatch = new ComputeDispatch(
0, 0, 0,
0, 0, 0,
waveCount,
IsIndirect: true,
0, 0, 0);
return true;
}
return RejectComputeDispatch(
@@ -12465,6 +12774,10 @@ public static partial class AgcExports
shaderAddress,
binding.Opcode);
// Check if this compute shader writes to a CMASK address
// (shadPS4's IsComputeMetaClear logic)
CheckCmaskWrite(texture.Address, gpuState);
TraceAgcShader(
$"agc.compute_writer addr=0x{texture.Address:X16} " +
$"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " +
@@ -13060,11 +13373,14 @@ public static partial class AgcExports
return;
}
GuestImageWriteTracker.Track(
GuestImageWriteTracker.Track(
destinationAddress,
(ulong)output.Length,
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
"agc.constant-fill");
VulkanVideoPresenter.RequestGuestColorClear(destinationAddress);
},
$"constant_fill dst=0x{destinationAddress:X16} bytes={output.Length}");
description =
+54 -117
View File
@@ -238,10 +238,12 @@ internal static class AgcVertexMetadata
}
/// <summary>
/// Patch IR-discovered fetches from the attrib table onto the V# format/offset.
/// Patch IR-discovered fetches from the attrib table onto the V# layout.
/// Prefer 1:1 Location pairing when counts match on one interleaved stream
/// (GTA UI glyphs). Otherwise match by stride + byte offset. Never rebases
/// BaseAddress/Data/Location/Pc/PerInstance.
/// (GTA UI glyphs). Otherwise match by the effective captured byte offset.
/// Never rebases BaseAddress/Data/Location/Pc or overwrites a discovered
/// offset: metadata may refine the format, stride and instance rate only
/// after both address keys independently resolve to the same attribute.
/// </summary>
internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata(
CpuContext ctx,
@@ -269,13 +271,13 @@ internal static class AgcVertexMetadata
var changed = false;
foreach (var input in discovered)
{
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource, out var fillOffset))
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource))
{
merged.Add(input);
continue;
}
var refined = ApplyMetadataFormat(input, resource, fillOffset);
var refined = ApplyMetadataFormat(input, resource);
changed |= refined != input;
merged.Add(refined);
}
@@ -286,7 +288,7 @@ internal static class AgcVertexMetadata
/// <summary>
/// When discovery and metadata describe the same interleaved stream with
/// equal attribute counts, pair by sorted Location (semantic order).
/// Keeps each binding's Pc/Location for SPIR-V; overlays format + offset.
/// Keeps each binding's Pc/Location/address for SPIR-V and overlays layout.
/// </summary>
private static bool TryMergeByLocationPairing(
IReadOnlyList<Gen5VertexInputBinding> discovered,
@@ -299,34 +301,36 @@ internal static class AgcVertexMetadata
return false;
}
var orderedInputs = discovered.OrderBy(static input => input.Location).ToArray();
var orderedInputs = discovered
.Select(static (input, originalIndex) => (Input: input, OriginalIndex: originalIndex))
.OrderBy(static entry => entry.Input.Location)
.ThenBy(static entry => entry.OriginalIndex)
.ToArray();
var orderedResources = resources.OrderBy(static resource => resource.Location).ToArray();
var streamBase = orderedResources[0].SharpBase;
var streamStride = orderedResources[0].Stride;
for (var index = 0; index < orderedResources.Length; index++)
{
var resource = orderedResources[index];
var input = orderedInputs[index];
var input = orderedInputs[index].Input;
if (resource.SharpBase != streamBase ||
resource.Stride != streamStride ||
(input.Stride != 0 && input.Stride != streamStride) ||
!IsSameVertexStream(input, resource))
!TryGetMetadataOffset(input, resource, out var resolvedOffset) ||
resolvedOffset != input.OffsetBytes)
{
return false;
}
}
var byPc = new Dictionary<uint, Gen5VertexInputBinding>(discovered.Count);
var result = discovered.ToArray();
var changed = false;
for (var index = 0; index < orderedInputs.Length; index++)
{
var input = orderedInputs[index];
var input = orderedInputs[index].Input;
var resource = orderedResources[index];
var fillOffset = input.BaseAddress == resource.SharpBase ||
IsAddressInsideCapturedSpan(input, resource.SharpBase);
var refined = ApplyMetadataFormat(input, resource, fillOffset);
var refined = ApplyMetadataFormat(input, resource);
changed |= refined != input;
byPc[input.Pc] = refined;
result[orderedInputs[index].OriginalIndex] = refined;
}
if (!changed)
@@ -334,20 +338,13 @@ internal static class AgcVertexMetadata
return false;
}
var result = new Gen5VertexInputBinding[discovered.Count];
for (var index = 0; index < discovered.Count; index++)
{
result[index] = byPc[discovered[index].Pc];
}
merged = result;
return true;
}
private static Gen5VertexInputBinding ApplyMetadataFormat(
Gen5VertexInputBinding input,
MetadataVertexResource resource,
bool fillOffsetBytes)
MetadataVertexResource resource)
{
var components = input.ComponentCount != 0 &&
input.ComponentCount < resource.ComponentCount
@@ -359,7 +356,8 @@ internal static class AgcVertexMetadata
DataFormat = resource.DataFormat,
NumberFormat = resource.NumberFormat,
ComponentCount = components,
OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes,
Stride = resource.Stride,
PerInstance = resource.PerInstance,
};
}
@@ -434,14 +432,11 @@ internal static class AgcVertexMetadata
Gen5VertexInputBinding input,
IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources,
out MetadataVertexResource resource,
out bool fillOffsetBytes)
out MetadataVertexResource resource)
{
resource = default;
fillOffsetBytes = false;
var bestScore = int.MinValue;
var bestIndex = -1;
var bestFillOffset = false;
for (var index = 0; index < resources.Count; index++)
{
if (usedResources[index])
@@ -450,100 +445,64 @@ internal static class AgcVertexMetadata
}
var candidate = resources[index];
if (candidate.Stride != 0 &&
input.Stride != 0 &&
candidate.Stride != input.Stride)
if (!TryGetMetadataOffset(input, candidate, out var resolvedOffset) ||
resolvedOffset != input.OffsetBytes)
{
continue;
}
if (!IsSameVertexStream(input, candidate))
{
continue;
}
// The effective captured offset (including any base rebasing done
// while coalescing adjacent vertex streams) is the primary key.
var score = 400;
var attrAddress = candidate.SharpBase + candidate.OffsetBytes;
var score = int.MinValue;
var fillOffset = false;
// Post-capture interleaved: shared BaseAddress, distinct OffsetBytes.
if (input.OffsetBytes == candidate.OffsetBytes &&
(input.BaseAddress == candidate.SharpBase ||
IsAddressInsideCapturedSpan(input, candidate.SharpBase)))
// Discovery can carry a stale inferred stride (notably 32 for a
// real stride-40 interleaved layout). Prefer a matching stride
// when candidates are otherwise equivalent, but do not reject an
// unambiguous metadata match: the V# descriptor is authoritative.
if (input.Stride == candidate.Stride)
{
score = 400;
}
// IR prolog baked attrib offset into the V# base.
else if (input.BaseAddress == attrAddress)
{
score = 350;
}
// Discovery never saw the attrib offset — only safe when this
// resource's offset uniquely identifies it among unused entries.
else if (input.BaseAddress == candidate.SharpBase &&
input.OffsetBytes == 0 &&
candidate.OffsetBytes != 0 &&
IsUniqueUnusedOffset(resources, usedResources, candidate.OffsetBytes, index))
{
score = 300;
fillOffset = true;
}
else if (input.BaseAddress == candidate.SharpBase &&
input.OffsetBytes == 0 &&
candidate.OffsetBytes == 0)
{
score = 250;
score += 25;
}
if (score > bestScore)
{
bestScore = score;
bestIndex = index;
bestFillOffset = fillOffset;
}
}
// Require an offset-aware match. Bare SharpBase ties (score 250) are
// only accepted when a single unused resource remains for that stream.
if (bestIndex < 0 || bestScore < 300)
if (bestIndex < 0)
{
if (bestIndex < 0 || bestScore < 250)
{
return false;
}
var unusedSameStream = 0;
for (var index = 0; index < resources.Count; index++)
{
if (!usedResources[index] && IsSameVertexStream(input, resources[index]))
{
unusedSameStream++;
}
}
if (unusedSameStream != 1)
{
return false;
}
return false;
}
usedResources[bestIndex] = true;
resource = resources[bestIndex];
fillOffsetBytes = bestFillOffset;
return true;
}
private static bool IsSameVertexStream(
private static bool TryGetMetadataOffset(
Gen5VertexInputBinding input,
MetadataVertexResource resource)
MetadataVertexResource resource,
out uint offsetBytes)
{
if (input.BaseAddress == resource.SharpBase ||
input.BaseAddress == resource.SharpBase + resource.OffsetBytes)
offsetBytes = input.OffsetBytes;
if (resource.SharpBase < input.BaseAddress ||
(!IsAddressInsideCapturedSpan(input, resource.SharpBase) &&
resource.SharpBase != input.BaseAddress))
{
return true;
return false;
}
return IsAddressInsideCapturedSpan(input, resource.SharpBase);
var relativeBase = resource.SharpBase - input.BaseAddress;
var resolvedOffset = relativeBase + resource.OffsetBytes;
if (resolvedOffset > uint.MaxValue)
{
return false;
}
offsetBytes = (uint)resolvedOffset;
return true;
}
private static bool IsAddressInsideCapturedSpan(
@@ -553,28 +512,6 @@ internal static class AgcVertexMetadata
address >= input.BaseAddress &&
address < input.BaseAddress + (ulong)input.DataLength;
private static bool IsUniqueUnusedOffset(
IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources,
uint offsetBytes,
int candidateIndex)
{
for (var index = 0; index < resources.Count; index++)
{
if (index == candidateIndex || usedResources[index])
{
continue;
}
if (resources[index].OffsetBytes == offsetBytes)
{
return false;
}
}
return true;
}
/// <summary>
/// Attrib-table format
/// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat.
+33
View File
@@ -59,6 +59,10 @@ internal static class GpuWaitRegistry
// cycle forever even though a real producer did signal it. Keyed by (memory,
// address) so distinct guest processes never alias.
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
// Frame-staleness guard: tracks the frame ID of each label write so that
// WAIT_REG_MEM in frame N+1 is not satisfied by a stale write from frame N.
private static readonly Dictionary<(object, ulong), long> _labelFrameIds = new();
private static long _currentFrameId;
private static object? Canonicalize(object? memory)
@@ -71,6 +75,34 @@ internal static class GpuWaitRegistry
return memory;
}
/// <summary>
/// Advances the frame counter. Called at each frame boundary (flip) so that
/// stale label writes from previous frames cannot satisfy WAIT_REG_MEM.
/// </summary>
public static void AdvanceFrame()
{
System.Threading.Interlocked.Increment(ref _currentFrameId);
}
/// <summary>
/// Returns true if the label at (memory, address) was written in the
/// current frame, or has never been written (uninitialized).
/// Only labels written in a PREVIOUS frame are considered stale.
/// </summary>
public static bool IsLabelFresh(object memory, ulong address)
{
memory = Canonicalize(memory)!;
lock (_gate)
{
if (!_labelFrameIds.TryGetValue((memory, address), out var frameId))
{
return true; // never written — treat as fresh (not stale)
}
return frameId >= System.Threading.Volatile.Read(ref _currentFrameId);
}
}
public static int Count
{
get
@@ -576,6 +608,7 @@ internal static class GpuWaitRegistry
}
_lastProduced[(memory, address)] = value;
_labelFrameIds[(memory, address)] = System.Threading.Volatile.Read(ref _currentFrameId);
}
return LatchSatisfiedByValue(memory, address, value);
@@ -5884,6 +5884,7 @@ internal static unsafe class VulkanVideoPresenter
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
{
Agc.AgcExports.MarkAllSurfacesCleared();
FlushBatchedGuestCommands();
_guestImages.TryGetValue(work.Address, out var source);
if (_deviceLost ||
@@ -12770,6 +12771,18 @@ internal static unsafe class VulkanVideoPresenter
targets[index].Initialized = false;
}
// CMASK meta-state: if the surface's metadata says "all clear",
// start this pass from LoadOp.Clear and consume the state.
// CPU-backed targets are skipped (their guest memory contents
// are uploaded, not cleared) — same rule the flip-arm used.
if (work.Targets[index].Address != 0 &&
!targets[index].IsCpuBacked &&
Agc.AgcExports.IsMetaClearedForSurface(work.Targets[index].Address))
{
targets[index].Initialized = false;
Agc.AgcExports.ConsumeMetaClear(work.Targets[index].Address);
}
if (work.Targets[index].Address != 0 &&
TakeGuestImageInitialData(work.Targets[index].Address) is { } initialData &&
!targets[index].Initialized &&
@@ -13031,13 +13044,31 @@ internal static unsafe class VulkanVideoPresenter
&toDepthAttachment);
}
ClearColorValue[]? metaClearValues = null;
for (var ci = 0; ci < targets.Length; ci++)
{
if (!targets[ci].Initialized &&
work.Targets[ci].Address != 0)
{
var (cw0, cw1) = Agc.AgcExports.GetMetaClearValue(
work.Targets[ci].Address);
if (cw0 != 0 || cw1 != 0)
{
metaClearValues ??= new ClearColorValue[targets.Length];
metaClearValues[ci] = UnpackMetaClearValue(
work.Targets[ci].Format, cw0, cw1);
}
}
}
BeginTranslatedRenderPass(
renderPass,
framebuffer,
extent,
colorAttachmentCount: targets.Length,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
clearDepth: depth?.ClearDepth ?? 1f);
clearDepth: depth?.ClearDepth ?? 1f,
colorClearValues: metaClearValues);
RecordTranslatedDrawInPass(resources, extent);
_vk.CmdEndRenderPass(_commandBuffer);
@@ -13828,16 +13859,10 @@ internal static unsafe class VulkanVideoPresenter
existing.LogicalDepth == depth &&
existing.Type == type &&
existing.MipLevels == mipLevels &&
(!requiresStorage || existing.SupportsStorageUsage) &&
(exactFormatMatch ||
(IsAliasableGuestImageFormat(existing.Format, format) &&
(!requiresStorage || existing.SupportsStorageUsage))))
IsAliasableGuestImageFormat(existing.Format, format)))
{
if (requiresStorage && !existing.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Guest image 0x{target.Address:X16} was created without storage usage.");
}
existing.IsCpuBacked = false;
existing.CpuContentFingerprint = 0;
if (existing.RenderPass.Handle == 0 &&
@@ -13870,14 +13895,9 @@ internal static unsafe class VulkanVideoPresenter
if (existing.Width == target.Width &&
existing.Height == target.Height &&
existing.MipLevels == mipLevels &&
(!requiresStorage || existing.SupportsStorageUsage) &&
IsCompatibleViewFormat(existing.Format, format))
{
if (requiresStorage && !existing.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Guest image 0x{target.Address:X16} was created without storage usage.");
}
if (_traceGuestImageEvents)
{
Console.Error.WriteLine(
@@ -13952,50 +13972,52 @@ internal static unsafe class VulkanVideoPresenter
{
if (requiresStorage && !retained.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Retained guest image 0x{target.Address:X16} was created without storage usage.");
// Do not reuse retained image if it lacks required storage usage
DestroyGuestImage(retained);
}
retained.IsCpuBacked = false;
retained.CpuContentFingerprint = 0;
_guestImages.Add(target.Address, retained);
var retainedByteCount = GetTextureByteCount(
target.Format,
target.Width,
target.Height,
depth);
lock (_gate)
else
{
_cpuBackedUploadGenerations.Remove(target.Address);
_guestImageExtents[target.Address] = (
retained.IsCpuBacked = false;
retained.CpuContentFingerprint = 0;
_guestImages.Add(target.Address, retained);
var retainedByteCount = GetTextureByteCount(
target.Format,
target.Width,
target.Height,
retainedByteCount);
}
depth);
lock (_gate)
{
_cpuBackedUploadGenerations.Remove(target.Address);
_guestImageExtents[target.Address] = (
target.Width,
target.Height,
retainedByteCount);
}
// Arm the exact extent the flip/acquire sync path would read
// back, budgeted by bytes rather than by resolution: the old
// 1920x1080 cap left every 4K surface permanently
// un-invalidated, so a guest CPU rewrite of one was never
// reflected and the sample served stale bytes.
if (ShouldTrackGuestImageWrites(retainedByteCount))
{
SharpEmu.HLE.GuestImageWriteTracker.Track(
target.Address,
retainedByteCount,
CurrentGuestWorkSequenceForDiagnostics,
"vulkan.render-target");
}
// Arm the exact extent the flip/acquire sync path would read
// back, budgeted by bytes rather than by resolution: the old
// 1920x1080 cap left every 4K surface permanently
// un-invalidated, so a guest CPU rewrite of one was never
// reflected and the sample served stale bytes.
if (ShouldTrackGuestImageWrites(retainedByteCount))
{
SharpEmu.HLE.GuestImageWriteTracker.Track(
target.Address,
retainedByteCount,
CurrentGuestWorkSequenceForDiagnostics,
"vulkan.render-target");
}
if (_traceGuestImageEvents)
{
Console.Error.WriteLine(
$"[GIMG] retained addr=0x{target.Address:X} " +
$"{target.Width}x{target.Height} fmt={format} " +
$"initialized={retained.Initialized}");
}
if (_traceGuestImageEvents)
{
Console.Error.WriteLine(
$"[GIMG] retained addr=0x{target.Address:X} " +
$"{target.Width}x{target.Height} fmt={format} " +
$"initialized={retained.Initialized}");
}
return retained;
return retained;
}
}
var imageInfo = new ImageCreateInfo
@@ -17697,20 +17719,67 @@ internal static unsafe class VulkanVideoPresenter
_vk.CmdEndRenderPass(_commandBuffer);
}
/// <summary>
/// Decodes the CB CLEAR_WORD0/1 pair into a float RGBA clear value
/// according to the surface pixel format. CLEAR_WORD holds the clear
/// colour packed in the surface's native layout, so the two 32-bit
/// words must be unpacked channel-by-channel; passing the raw word as
/// a single float channel clears to a garbage colour.
/// </summary>
private static ClearColorValue UnpackMetaClearValue(
uint format, uint cw0, uint cw1)
{
switch (format)
{
// Gen5 8_8_8_8 (R8G8B8A8): four UNORM bytes packed in WORD0,
// little-endian channel order R,G,B,A.
case Agc.AgcExports.Gen5TextureFormatR8G8B8A8Unorm:
return new ClearColorValue(
float32_0: ((cw0 >> 0) & 0xFF) / 255f,
float32_1: ((cw0 >> 8) & 0xFF) / 255f,
float32_2: ((cw0 >> 16) & 0xFF) / 255f,
float32_3: ((cw0 >> 24) & 0xFF) / 255f);
// Gen5 16_16_16_16 float (R16G16B16A16F): R,G as halfs in
// WORD0 and B,A as halfs in WORD1.
case Agc.AgcExports.Gen5TextureFormatR16G16B16A16Float:
return new ClearColorValue(
float32_0: HalfToFloat((ushort)(cw0 >> 0)),
float32_1: HalfToFloat((ushort)(cw0 >> 16)),
float32_2: HalfToFloat((ushort)(cw1 >> 0)),
float32_3: HalfToFloat((ushort)(cw1 >> 16)));
default:
// Unknown format: fall back to the common 8_8_8_8 layout.
return new ClearColorValue(
float32_0: ((cw0 >> 0) & 0xFF) / 255f,
float32_1: ((cw0 >> 8) & 0xFF) / 255f,
float32_2: ((cw0 >> 16) & 0xFF) / 255f,
float32_3: ((cw0 >> 24) & 0xFF) / 255f);
}
}
private static float HalfToFloat(ushort halfBits) =>
(float)BitConverter.UInt16BitsToHalf(halfBits);
private void BeginTranslatedRenderPass(
RenderPass renderPass,
Framebuffer framebuffer,
Extent2D extent,
int colorAttachmentCount = 1,
bool hasDepthAttachment = false,
float clearDepth = 1f)
float clearDepth = 1f,
ClearColorValue[]? colorClearValues = null)
{
colorAttachmentCount = Math.Max(colorAttachmentCount, 1);
var clearValueCount = colorAttachmentCount + (hasDepthAttachment ? 1 : 0);
var clearValues = stackalloc ClearValue[clearValueCount];
for (var index = 0; index < colorAttachmentCount; index++)
{
clearValues[index] = default;
clearValues[index] = colorClearValues is not null &&
index < colorClearValues.Length
? new ClearValue { Color = colorClearValues[index] }
: default;
}
// Reverse-Z is not assumed; clear depth to 1.0 (far) so a standard
// LessOrEqual/Less test keeps the nearest fragment.
@@ -144,11 +144,35 @@ public static partial class Gen5MslTranslator
// ---- float arithmetic ----
"VAddF32" => FloatResult(instruction, $"{F(instruction, 0)} + {F(instruction, 1)}"),
"VAddF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 0)} + {F16(instruction, 1)}"),
"VSubF32" => FloatResult(instruction, $"{F(instruction, 0)} - {F(instruction, 1)}"),
"VSubrevF32" => FloatResult(instruction, $"{F(instruction, 1)} - {F(instruction, 0)}"),
"VSubF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 0)} - {F16(instruction, 1)}"),
"VSubrevF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 1)} - {F16(instruction, 0)}"),
"VMulF32" => FloatResult(instruction, $"{F(instruction, 0)} * {F(instruction, 1)}"),
"VMulF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 0)} * {F16(instruction, 1)}"),
"VMinF32" => FloatResult(instruction, $"fmin({F(instruction, 0)}, {F(instruction, 1)})"),
"VMaxF32" => FloatResult(instruction, $"fmax({F(instruction, 0)}, {F(instruction, 1)})"),
"VMinF16" => Float16Result(
instruction,
destination,
$"fmin({F16(instruction, 0)}, {F16(instruction, 1)})"),
"VMaxF16" => Float16Result(
instruction,
destination,
$"fmax({F16(instruction, 0)}, {F16(instruction, 1)})"),
// The decoder normalizes mk/ak literal placement, so every MAD/FMA
// form is fma(src0, src1, src2) exactly like the SPIR-V translator.
"VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" =>
@@ -578,23 +602,46 @@ public static partial class Gen5MslTranslator
{
condition = EmitCompareClass(instruction);
}
else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32")
else if (opcode is
"VCmpTruF32" or "VCmpxTruF32" or
"VCmpTruF16" or "VCmpxTruF16" or
"VCmpTI32" or "VCmpTU32")
{
condition = "true";
}
else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32")
else if (opcode is
"VCmpFF32" or "VCmpxFF32" or
"VCmpFF16" or "VCmpxFF16" or
"VCmpFI32" or "VCmpFU32")
{
condition = "false";
}
else if (opcode is "VCmpOF32" or "VCmpxOF32")
else if (opcode is
"VCmpOF32" or "VCmpxOF32" or
"VCmpOF16" or "VCmpxOF16")
{
condition = $"(!isnan({F(instruction, 0)}) && !isnan({F(instruction, 1)}))";
var left = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 0)
: F(instruction, 0);
var right = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 1)
: F(instruction, 1);
condition = $"(!isnan({left}) && !isnan({right}))";
}
else if (opcode is "VCmpUF32" or "VCmpxUF32")
else if (opcode is
"VCmpUF32" or "VCmpxUF32" or
"VCmpUF16" or "VCmpxUF16")
{
condition = $"(isnan({F(instruction, 0)}) || isnan({F(instruction, 1)}))";
var left = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 0)
: F(instruction, 0);
var right = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 1)
: F(instruction, 1);
condition = $"(isnan({left}) || isnan({right}))";
}
else if (opcode.EndsWith("F32", StringComparison.Ordinal))
else if (opcode.EndsWith("F32", StringComparison.Ordinal) ||
opcode.EndsWith("F16", StringComparison.Ordinal))
{
// Ordered compares are the plain C operators (false on NaN);
// the Nxx forms are their unordered negations (true on NaN).
@@ -620,7 +667,13 @@ public static partial class Gen5MslTranslator
return false;
}
var comparison = $"({F(instruction, 0)} {op} {F(instruction, 1)})";
var left = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 0)
: F(instruction, 0);
var right = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 1)
: F(instruction, 1);
var comparison = $"({left} {op} {right})";
condition = unordered ? $"(!{comparison})" : comparison;
}
else
@@ -1570,6 +1623,80 @@ public static partial class Gen5MslTranslator
return expression;
}
/// <summary>Reads the selected 16-bit half as a widened float.</summary>
private string F16(Gen5ShaderInstruction instruction, int sourceIndex)
{
var operand = instruction.Sources[sourceIndex];
string expression;
if (operand.Kind == Gen5OperandKind.EncodedConstant &&
Gen5InlineConstants.TryDecode(operand.Value, out var inline))
{
expression = operand.Value switch
{
>= 128 and <= 192 => $"{operand.Value - 128}.0f",
>= 193 and <= 208 => $"(-{operand.Value - 192}.0f)",
_ => AsFloat(FormatUInt(inline)),
};
}
else
{
var raw = RawSource(
instruction,
sourceIndex,
applySdwaIntegerModifiers: false);
var shift = instruction.Control is Gen5Vop3Control control &&
(control.OperandSelect & (1u << sourceIndex)) != 0
? 16
: 0;
expression =
$"(float)as_type<half>((ushort)((({raw}) >> {shift}) & 0xFFFFu))";
}
var (absoluteMask, negateMask) = instruction.Control switch
{
Gen5Vop3Control control => (control.AbsoluteMask, control.NegateMask),
Gen5SdwaControl control => (control.AbsoluteMask, control.NegateMask),
Gen5DppControl control => (control.AbsoluteMask, control.NegateMask),
_ => (0u, 0u),
};
if ((absoluteMask & (1u << sourceIndex)) != 0)
{
expression = $"fabs({expression})";
}
if ((negateMask & (1u << sourceIndex)) != 0)
{
expression = $"(-{expression})";
}
return expression;
}
/// <summary>Rounds to f16 and preserves the unselected VGPR half.</summary>
private string Float16Result(
Gen5ShaderInstruction instruction,
uint destination,
string expression)
{
var control = instruction.Control as Gen5Vop3Control;
expression = (control?.OutputModifier ?? 0) switch
{
1 => $"(({expression}) * 2.0f)",
2 => $"(({expression}) * 4.0f)",
3 => $"(({expression}) * 0.5f)",
_ => expression,
};
if (control?.Clamp == true)
{
expression = $"clamp({expression}, 0.0f, 1.0f)";
}
var packed = $"(uint)as_type<ushort>(half({expression}))";
return ((control?.OperandSelect ?? 0) & 8) != 0
? $"((v[{destination}] & 0x0000FFFFu) | (({packed}) << 16))"
: $"((v[{destination}] & 0xFFFF0000u) | ({packed}))";
}
/// <summary>
/// Wraps a float expression with VOP3/SDWA output modifiers and clamp,
/// then bitcasts back to the register file's uint domain.
@@ -340,21 +340,43 @@ public static partial class Gen5SpirvTranslator
case "VAddF32":
result = EmitFloatBinary(instruction, SpirvOp.FAdd);
break;
case "VAddF16":
result = EmitFloat16Binary(instruction, destination, SpirvOp.FAdd);
break;
case "VSubF32":
result = EmitFloatBinary(instruction, SpirvOp.FSub);
break;
case "VSubrevF32":
result = EmitFloatBinary(instruction, SpirvOp.FSub, reverse: true);
break;
case "VSubF16":
result = EmitFloat16Binary(instruction, destination, SpirvOp.FSub);
break;
case "VSubrevF16":
result = EmitFloat16Binary(
instruction,
destination,
SpirvOp.FSub,
reverse: true);
break;
case "VMulF32":
result = EmitFloatBinary(instruction, SpirvOp.FMul);
break;
case "VMulF16":
result = EmitFloat16Binary(instruction, destination, SpirvOp.FMul);
break;
case "VMinF32":
result = EmitFloatExtBinary(instruction, 37);
break;
case "VMaxF32":
result = EmitFloatExtBinary(instruction, 40);
break;
case "VMinF16":
result = EmitFloat16ExtBinary(instruction, destination, 37);
break;
case "VMaxF16":
result = EmitFloat16ExtBinary(instruction, destination, 40);
break;
case "VMadF32":
case "VFmaF32":
case "VMadMkF32":
@@ -1609,29 +1631,72 @@ public static partial class Gen5SpirvTranslator
condition,
SignedClass(0x020, 0x040, zero));
}
else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32")
else if (opcode is
"VCmpFF32" or "VCmpxFF32" or
"VCmpFF16" or "VCmpxFF16" or
"VCmpFI32" or "VCmpFU32")
{
condition = _module.ConstantBool(false);
}
else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32")
else if (opcode is
"VCmpTruF32" or "VCmpxTruF32" or
"VCmpTruF16" or "VCmpxTruF16" or
"VCmpTI32" or "VCmpTU32")
{
condition = _module.ConstantBool(true);
}
else if (opcode is
"VCmpOF32" or "VCmpxOF32" or
"VCmpUF32" or "VCmpxUF32")
"VCmpUF32" or "VCmpxUF32" or
"VCmpOF16" or "VCmpxOF16" or
"VCmpUF16" or "VCmpxUF16")
{
var left = GetFloatSource(instruction, 0);
var right = GetFloatSource(instruction, 1);
var isHalf = opcode.EndsWith("F16", StringComparison.Ordinal);
var left = isHalf
? GetFloat16Source(instruction, 0)
: GetFloatSource(instruction, 0);
var right = isHalf
? GetFloat16Source(instruction, 1)
: GetFloatSource(instruction, 1);
var unordered = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
_module.AddInstruction(SpirvOp.IsNan, _boolType, right));
condition = opcode is "VCmpUF32" or "VCmpxUF32"
condition = opcode is
"VCmpUF32" or "VCmpxUF32" or
"VCmpUF16" or "VCmpxUF16"
? unordered
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
}
else if (opcode.EndsWith("F16", StringComparison.Ordinal))
{
var left = GetFloat16Source(instruction, 0);
var right = GetFloat16Source(instruction, 1);
var operation = opcode switch
{
"VCmpLtF16" or "VCmpxLtF16" => SpirvOp.FOrdLessThan,
"VCmpEqF16" or "VCmpxEqF16" => SpirvOp.FOrdEqual,
"VCmpLeF16" or "VCmpxLeF16" => SpirvOp.FOrdLessThanEqual,
"VCmpGtF16" or "VCmpxGtF16" => SpirvOp.FOrdGreaterThan,
"VCmpLgF16" or "VCmpxLgF16" => SpirvOp.FOrdNotEqual,
"VCmpGeF16" or "VCmpxGeF16" => SpirvOp.FOrdGreaterThanEqual,
"VCmpNeqF16" or "VCmpxNeqF16" => SpirvOp.FUnordNotEqual,
"VCmpNltF16" or "VCmpxNltF16" => SpirvOp.FUnordGreaterThanEqual,
"VCmpNleF16" or "VCmpxNleF16" => SpirvOp.FUnordGreaterThan,
"VCmpNgtF16" or "VCmpxNgtF16" => SpirvOp.FUnordLessThanEqual,
"VCmpNgeF16" or "VCmpxNgeF16" => SpirvOp.FUnordLessThan,
"VCmpNlgF16" or "VCmpxNlgF16" => SpirvOp.FUnordEqual,
_ => SpirvOp.Nop,
};
if (operation == SpirvOp.Nop)
{
error = $"unsupported half compare {opcode}";
return false;
}
condition = _module.AddInstruction(operation, _boolType, left, right);
}
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
opcode.EndsWith("F32", StringComparison.Ordinal))
{
@@ -3108,6 +3173,70 @@ public static partial class Gen5SpirvTranslator
sourceAllowsWrite));
}
private uint GetFloat16Source(
Gen5ShaderInstruction instruction,
int sourceIndex)
{
var operand = instruction.Sources[sourceIndex];
uint value;
if (operand.Kind == Gen5OperandKind.EncodedConstant &&
operand.Value is >= 128 and <= 192)
{
value = Float(operand.Value - 128);
}
else if (operand.Kind == Gen5OperandKind.EncodedConstant &&
operand.Value is >= 193 and <= 208)
{
value = Float(-(operand.Value - 192));
}
else if (operand.Kind == Gen5OperandKind.EncodedConstant &&
Gen5InlineConstants.TryDecode(operand.Value, out var inline))
{
value = Bitcast(_floatType, UInt(inline));
}
else
{
var raw = GetRawSource(
instruction,
sourceIndex,
applySdwaIntegerModifiers: false);
if (instruction.Control is Gen5Vop3Control control &&
(control.OperandSelect & (1u << sourceIndex)) != 0)
{
raw = ShiftRightLogical(raw, UInt(16));
}
value = Bitcast(_floatType, EmitHalfToFloat(raw));
}
uint absoluteMask = 0;
uint negateMask = 0;
switch (instruction.Control)
{
case Gen5Vop3Control control:
absoluteMask = control.AbsoluteMask;
negateMask = control.NegateMask;
break;
case Gen5SdwaControl control:
absoluteMask = control.AbsoluteMask;
negateMask = control.NegateMask;
break;
case Gen5DppControl control:
absoluteMask = control.AbsoluteMask;
negateMask = control.NegateMask;
break;
}
if ((absoluteMask & (1u << sourceIndex)) != 0)
{
value = Ext(4, _floatType, value);
}
return (negateMask & (1u << sourceIndex)) != 0
? _module.AddInstruction(SpirvOp.FNegate, _floatType, value)
: value;
}
private uint GetFloatSource(
Gen5ShaderInstruction instruction,
int sourceIndex)
@@ -3232,6 +3361,33 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(SpirvOp.UConvert, _uintType, high));
}
private uint EmitFloat16Binary(
Gen5ShaderInstruction instruction,
uint destination,
SpirvOp operation,
bool reverse = false)
{
var left = GetFloat16Source(instruction, reverse ? 1 : 0);
var right = GetFloat16Source(instruction, reverse ? 0 : 1);
return EmitFloat16Result(
instruction,
destination,
_module.AddInstruction(operation, _floatType, left, right));
}
private uint EmitFloat16ExtBinary(
Gen5ShaderInstruction instruction,
uint destination,
uint operation) =>
EmitFloat16Result(
instruction,
destination,
Ext(
operation,
_floatType,
GetFloat16Source(instruction, 0),
GetFloat16Source(instruction, 1)));
private uint EmitFloatBinary(
Gen5ShaderInstruction instruction,
SpirvOp operation,
@@ -3753,6 +3909,35 @@ public static partial class Gen5SpirvTranslator
UInt(0));
}
private uint EmitFloat16Result(
Gen5ShaderInstruction instruction,
uint destination,
uint value)
{
var control = instruction.Control as Gen5Vop3Control;
value = (control?.OutputModifier ?? 0) switch
{
1 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(2)),
2 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(4)),
3 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(0.5f)),
_ => value,
};
if (control?.Clamp == true)
{
value = Ext(43, _floatType, value, Float(0), Float(1));
}
var half = EmitFloatToHalf(Bitcast(_uintType, value));
var current = LoadV(destination);
return ((control?.OperandSelect ?? 0) & 8) != 0
? BitwiseOr(
BitwiseAnd(current, UInt(0x0000_FFFF)),
ShiftLeftLogical(half, UInt(16)))
: BitwiseOr(
BitwiseAnd(current, UInt(0xFFFF_0000)),
half);
}
private uint EmitFloatResult(
Gen5ShaderInstruction instruction,
uint value)
@@ -2366,17 +2366,16 @@ public static partial class Gen5SpirvTranslator
return;
}
// GLOBAL_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, so read/write dwords directly instead of the per-byte loop.
for (uint index = 0; index < control.DwordCount; index++)
{
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreBufferBytes(
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreBufferWord(
bindingIndex,
address,
LoadV(control.VectorData + index),
sizeof(uint),
0);
indexedDwordAddress,
LoadV(control.VectorData + index));
}
});
return true;
@@ -2404,12 +2403,12 @@ public static partial class Gen5SpirvTranslator
for (uint index = 0; index < control.DwordCount; index++)
{
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreV(
control.VectorData + index,
LoadUnalignedBufferWord(bindingIndex, address));
LoadBufferWord(bindingIndex, indexedDwordAddress));
}
return true;
@@ -2510,17 +2509,16 @@ public static partial class Gen5SpirvTranslator
return;
}
// BUFFER_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, same as the GLOBAL case above — no per-byte reassembly needed.
for (uint index = 0; index < control.DwordCount; index++)
{
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreBufferBytes(
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreBufferWord(
bindingIndex,
address,
LoadV(control.VectorData + index),
sizeof(uint),
0);
indexedDwordAddress,
LoadV(control.VectorData + index));
}
});
@@ -2576,12 +2574,12 @@ public static partial class Gen5SpirvTranslator
for (uint index = 0; index < control.DwordCount; index++)
{
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreV(
control.VectorData + index,
LoadUnalignedBufferWord(bindingIndex, address));
LoadBufferWord(bindingIndex, indexedDwordAddress));
}
return true;
@@ -1015,6 +1015,12 @@ public static class Gen5ShaderTranslator
0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32",
0x32 => "VAddF16",
0x33 => "VSubF16",
0x34 => "VSubrevF16",
0x35 => "VMulF16",
0x39 => "VMaxF16",
0x3A => "VMinF16",
_ => string.Empty,
};
@@ -1086,6 +1092,14 @@ public static class Gen5ShaderTranslator
0xC5 => "VCmpNeU32",
0xC6 => "VCmpGeU32",
0xC7 => "VCmpTU32",
0xC8 => "VCmpFF16",
0xC9 => "VCmpLtF16",
0xCA => "VCmpEqF16",
0xCB => "VCmpLeF16",
0xCC => "VCmpGtF16",
0xCD => "VCmpLgF16",
0xCE => "VCmpGeF16",
0xCF => "VCmpOF16",
0xD0 => "VCmpxFU32",
0xD1 => "VCmpxLtU32",
0xD2 => "VCmpxEqU32",
@@ -1094,6 +1108,30 @@ public static class Gen5ShaderTranslator
0xD5 => "VCmpxNeU32",
0xD6 => "VCmpxGeU32",
0xD7 => "VCmpxTU32",
0xD8 => "VCmpxFF16",
0xD9 => "VCmpxLtF16",
0xDA => "VCmpxEqF16",
0xDB => "VCmpxLeF16",
0xDC => "VCmpxGtF16",
0xDD => "VCmpxLgF16",
0xDE => "VCmpxGeF16",
0xDF => "VCmpxOF16",
0xE8 => "VCmpUF16",
0xE9 => "VCmpNgeF16",
0xEA => "VCmpNlgF16",
0xEB => "VCmpNgtF16",
0xEC => "VCmpNleF16",
0xED => "VCmpNeqF16",
0xEE => "VCmpNltF16",
0xEF => "VCmpTruF16",
0xF8 => "VCmpxUF16",
0xF9 => "VCmpxNgeF16",
0xFA => "VCmpxNlgF16",
0xFB => "VCmpxNgtF16",
0xFC => "VCmpxNleF16",
0xFD => "VCmpxNeqF16",
0xFE => "VCmpxNltF16",
0xFF => "VCmpxTruF16",
_ => string.Empty,
};
@@ -102,17 +102,16 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
ConcurrentDictionary<string, IMethodSymbol> exportsByNid)
{
var method = (IMethodSymbol)context.Symbol;
AttributeData? exportAttribute = null;
var exportAttributes = ImmutableArray.CreateBuilder<AttributeData>();
foreach (var attribute in method.GetAttributes())
{
if (SysAbiExportShape.IsSysAbiExportAttribute(attribute.AttributeClass))
{
exportAttribute = attribute;
break;
exportAttributes.Add(attribute);
}
}
if (exportAttribute is null)
if (exportAttributes.Count == 0)
{
return;
}
@@ -135,6 +134,28 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
SysAbiDiagnostics.HandlerNotAccessible, location, methodDisplay));
}
foreach (var exportAttribute in exportAttributes)
{
AnalyzeExportAttribute(
context,
catalogNames,
exportsByNid,
method,
exportAttribute,
location,
methodDisplay);
}
}
private static void AnalyzeExportAttribute(
SymbolAnalysisContext context,
HashSet<string>? catalogNames,
ConcurrentDictionary<string, IMethodSymbol> exportsByNid,
IMethodSymbol method,
AttributeData exportAttribute,
Location location,
string methodDisplay)
{
var arguments = SysAbiExportShape.ReadArguments(exportAttribute);
var hasNid = !string.IsNullOrWhiteSpace(arguments.Nid);
var hasName = !string.IsNullOrWhiteSpace(arguments.ExportName);
@@ -188,9 +209,9 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
}
}
var existing = exportsByNid.GetOrAdd(effectiveNid, method);
if (!SymbolEqualityComparer.Default.Equals(existing, method))
if (!exportsByNid.TryAdd(effectiveNid, method))
{
var existing = exportsByNid[effectiveNid];
context.ReportDiagnostic(Diagnostic.Create(
SysAbiDiagnostics.DuplicateNid,
location,
@@ -27,7 +27,16 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
private sealed class ExportModel : IEquatable<ExportModel>
{
public ExportModel(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target)
public ExportModel(
string containingType,
string methodName,
SysAbiExportShape.HandlerShape shape,
string typedParameterKinds,
string libraryName,
string nid,
string exportName,
int target,
bool preferLle)
{
ContainingType = containingType;
MethodName = methodName;
@@ -37,6 +46,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
Nid = nid;
ExportName = exportName;
Target = target;
PreferLle = preferLle;
}
public string ContainingType { get; }
@@ -51,6 +61,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
public string Nid { get; }
public string ExportName { get; }
public int Target { get; }
public bool PreferLle { get; }
public bool Equals(ExportModel? other) =>
other is not null &&
@@ -61,7 +72,8 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
LibraryName == other.LibraryName &&
Nid == other.Nid &&
ExportName == other.ExportName &&
Target == other.Target;
Target == other.Target &&
PreferLle == other.PreferLle;
public override bool Equals(object? obj) => Equals(obj as ExportModel);
@@ -73,6 +85,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
hash = (hash * 31) + ContainingType.GetHashCode();
hash = (hash * 31) + MethodName.GetHashCode();
hash = (hash * 31) + Nid.GetHashCode();
hash = (hash * 31) + PreferLle.GetHashCode();
return hash;
}
}
@@ -80,80 +93,90 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var exports = context.SyntaxProvider
var exportGroups = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeMetadataName,
static (node, _) => node is MethodDeclarationSyntax,
static (attributeContext, _) => CreateModel(attributeContext))
.Where(static model => model is not null)
static (attributeContext, _) => CreateModels(attributeContext))
.Where(static models => !models.IsDefaultOrEmpty)
.Collect();
var assemblyName = context.CompilationProvider
.Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly");
context.RegisterSourceOutput(
exports.Combine(assemblyName),
exportGroups.Combine(assemblyName),
static (productionContext, source) => Emit(productionContext, source.Left!, source.Right));
}
private static ExportModel? CreateModel(GeneratorAttributeSyntaxContext context)
private static ImmutableArray<ExportModel> CreateModels(GeneratorAttributeSyntaxContext context)
{
if (context.TargetSymbol is not IMethodSymbol method ||
!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
{
return null;
return ImmutableArray<ExportModel>.Empty;
}
var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds);
if (shape == SysAbiExportShape.HandlerShape.Invalid)
{
return null;
return ImmutableArray<ExportModel>.Empty;
}
var attribute = context.Attributes[0];
var arguments = SysAbiExportShape.ReadArguments(attribute);
var nid = arguments.Nid;
var exportName = arguments.ExportName;
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the export
// name (algorithmically — equivalent to the runtime catalog lookup, which was
// built with the same computation); a missing name falls back to the method name.
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
var models = ImmutableArray.CreateBuilder<ExportModel>(context.Attributes.Length);
foreach (var attribute in context.Attributes)
{
nid = Ps5Nid.Compute(exportName);
var arguments = SysAbiExportShape.ReadArguments(attribute);
var nid = arguments.Nid;
var exportName = arguments.ExportName;
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the
// export name. A missing name falls back to the method name.
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
{
nid = Ps5Nid.Compute(exportName);
}
if (string.IsNullOrWhiteSpace(nid))
{
continue;
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
models.Add(new ExportModel(
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
method.Name,
shape,
typedParameterKinds,
libraryName,
nid!,
exportName!,
arguments.Target,
arguments.PreferLle));
}
if (string.IsNullOrWhiteSpace(nid))
{
return null;
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
return new ExportModel(
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
method.Name,
shape,
typedParameterKinds,
libraryName,
nid!,
exportName!,
arguments.Target);
return models.ToImmutable();
}
private static void Emit(
SourceProductionContext context,
ImmutableArray<ExportModel?> exports,
ImmutableArray<ImmutableArray<ExportModel>> exportGroups,
string assemblyName)
{
// No exports, no registry: an assembly that merely references the analyzer
// (e.g. SharpEmu.HLE itself) must not mint a colliding
// SharpEmu.Generated.SysAbiExportRegistry type.
if (exports.IsDefaultOrEmpty)
var exportCount = 0;
foreach (var group in exportGroups)
{
exportCount += group.Length;
}
if (exportCount == 0)
{
return;
}
@@ -175,24 +198,23 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<global::SharpEmu.HLE.ExportedFunction> CreateExports(");
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)");
builder.AppendLine(" {");
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exports.Length});");
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exportCount});");
foreach (var export in exports)
foreach (var group in exportGroups)
{
if (export is null)
foreach (var export in group)
{
continue;
var function = export.Shape switch
{
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
_ => TypedThunk(export),
};
builder.AppendLine(
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, " +
$"{(export.PreferLle ? "true" : "false")}, {function});");
}
var function = export.Shape switch
{
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
_ => TypedThunk(export),
};
builder.AppendLine(
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, {function});");
}
builder.AppendLine(" return exports;");
@@ -205,6 +227,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" string nid,");
builder.AppendLine(" string exportName,");
builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,");
builder.AppendLine(" bool preferLle,");
builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)");
builder.AppendLine(" {");
builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;");
@@ -213,7 +236,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" return;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function));");
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function, preferLle));");
builder.AppendLine(" }");
builder.AppendLine("}");
@@ -18,18 +18,20 @@ public static class SysAbiExportShape
public readonly struct Arguments
{
public Arguments(string libraryName, string nid, string exportName, int target)
public Arguments(string libraryName, string nid, string exportName, int target, bool preferLle)
{
LibraryName = libraryName;
Nid = nid;
ExportName = exportName;
Target = target;
PreferLle = preferLle;
}
public string LibraryName { get; }
public string Nid { get; }
public string ExportName { get; }
public int Target { get; }
public bool PreferLle { get; }
}
/// <summary>
@@ -205,6 +207,7 @@ public static class SysAbiExportShape
var nid = string.Empty;
var exportName = string.Empty;
var target = 0;
var preferLle = false;
foreach (var argument in attribute.NamedArguments)
{
switch (argument.Key)
@@ -221,9 +224,12 @@ public static class SysAbiExportShape
case "Target":
target = argument.Value.Value is int value ? value : 0;
break;
case "PreferLle":
preferLle = argument.Value.Value is bool boolValue && boolValue;
break;
}
}
return new Arguments(libraryName, nid, exportName, target);
return new Arguments(libraryName, nid, exportName, target, preferLle);
}
}
@@ -74,7 +74,7 @@ public sealed class AgcVertexMetadataTests
}
[Fact]
public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture()
public void MergeVertexInputs_OverlaysLayoutWithoutRebasingCapture()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
@@ -114,7 +114,7 @@ public sealed class AgcVertexMetadataTests
NumberFormat: 7,
BaseAddress: sharpBase,
Stride: 16,
OffsetBytes: 0,
OffsetBytes: 12,
Data: data,
DataLength: data.Length,
DataPooled: false),
@@ -135,6 +135,137 @@ public sealed class AgcVertexMetadataTests
Assert.Equal(0x40u, merged[0].Pc);
}
[Fact]
public void MergeVertexInputs_MetadataCorrectsStaleStride40()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var data = new byte[160];
var discovered = new[]
{
new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, sharpBase, 32, 12, data, data.Length, false),
};
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
discovered);
Assert.Single(merged);
Assert.Equal(40u, merged[0].Stride);
Assert.Equal(12u, merged[0].OffsetBytes);
Assert.Equal(sharpBase, merged[0].BaseAddress);
Assert.Same(data, merged[0].Data);
Assert.Equal(0x40u, merged[0].Pc);
}
[Fact]
public void MergeVertexInputs_ConflictingMetadataOffsetDoesNotMoveBinding()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var original = new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, sharpBase, 32, 0, new byte[160], 160, false);
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
[original]);
Assert.Same(original, Assert.Single(merged));
}
[Fact]
public void MergeVertexInputs_UsesOffsetRelativeToCapturedBase()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong capturedBase = memoryBase + 0x7F8;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var data = new byte[160];
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
[new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, capturedBase, 32, 20, data, data.Length, false)]);
Assert.Equal(40u, Assert.Single(merged).Stride);
Assert.Equal(20u, merged[0].OffsetBytes);
Assert.Equal(capturedBase, merged[0].BaseAddress);
Assert.Same(data, merged[0].Data);
}
[Fact]
public void MergeVertexInputs_AcceptsVertexAttribFormatEnums()
{
@@ -0,0 +1,60 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class DirectExecutionBackendLlePreferenceTests
{
[Fact]
public void ExplicitLlePreference_AllowsNonKernelRegisteredExport()
{
var export = Export("libSceNpCppWebApi", preferLle: true);
Assert.True(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: false));
}
[Fact]
public void ExplicitLlePreference_CannotOverrideKernelHleBoundary()
{
var export = Export("libKernel", preferLle: true);
Assert.False(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: true));
}
[Fact]
public void RegisteredExportWithoutLlePreference_RemainsHle()
{
var export = Export("libSceNpCppWebApi", preferLle: false);
Assert.False(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: false));
}
[Fact]
public void ExistingLibcPolicy_CanStillSelectRegisteredFirmwareExport()
{
var export = Export("libSceLibcInternal", preferLle: false);
Assert.True(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: true));
}
private static ExportedFunction Export(string libraryName, bool preferLle) =>
new(
libraryName,
"Zxa0VhQVTsk",
"sceKernelWaitSema",
Generation.Gen5,
static _ => 0,
preferLle);
}
@@ -0,0 +1,75 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class TlsLoadPatchBoundaryTests
{
[Fact]
public void RejectsGtaShortJumpDisplacementAsTlsPrefix()
{
byte[] code =
[
0x90,
0xEB, 0x66,
0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.True(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 2));
Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 3));
}
[Fact]
public void KeepsDreamingSarahTlsInstructionAfterBackwardJnz()
{
byte[] code =
[
0x48, 0x8B, 0x1C, 0xD0,
0x4C, 0x39, 0x2B,
0x0F, 0x84, 0x10, 0x01, 0x00, 0x00,
0x48, 0xFF, 0xC2,
0x48, 0x39, 0xD1,
0x75, 0xEB,
0x66, 0x66, 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21));
}
[Theory]
[InlineData(0x70)]
[InlineData(0x7F)]
[InlineData(0xE0)]
[InlineData(0xE3)]
[InlineData(0xEB)]
public void KeepsTlsInstructionAfterRel8ControlFlow(byte opcode)
{
byte[] code =
[
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
opcode, 0xEB,
0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21));
}
[Fact]
public void Rel8OpcodeByteInsidePreviousInstructionDoesNotBypassGuard()
{
byte[] code =
[
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x6A, 0x75,
0xEB, 0x66,
0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.True(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21));
}
}
@@ -14,7 +14,7 @@ public sealed class GuestMemoryAllocatorTests
public void FreedRangesAreReusedAndCoalesced()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
const ulong usableArenaSize = 0x2000_0000 - 0x1000;
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
@@ -34,6 +34,16 @@ public sealed class GuestMemoryAllocatorTests
Assert.Equal(first, coalesced);
}
[Fact]
public void ArenaSupportsAllocationsBeyondLegacySixteenMiBLimit()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
Assert.True(memory.TryAllocateGuestMemory(0x0100_0000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x0020_0000, 0x1000, out var beyondLegacyLimit));
Assert.Equal(first + 0x0100_0000, beyondLegacyLimit);
}
[Fact]
public void SegmentProtectionIsAppliedInContiguousRuns()
{
@@ -0,0 +1,85 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Metal;
using Xunit;
namespace SharpEmu.ShaderCompiler.Metal.Tests;
public sealed class Gen5MslF16CompareTests
{
public static TheoryData<string> Opcodes = new()
{
"VCmpFF16",
"VCmpLtF16",
"VCmpEqF16",
"VCmpLeF16",
"VCmpGtF16",
"VCmpLgF16",
"VCmpGeF16",
"VCmpOF16",
"VCmpxFF16",
"VCmpxLtF16",
"VCmpxEqF16",
"VCmpxLeF16",
"VCmpxGtF16",
"VCmpxLgF16",
"VCmpxGeF16",
"VCmpxOF16",
"VCmpUF16",
"VCmpNgeF16",
"VCmpNlgF16",
"VCmpNgtF16",
"VCmpNleF16",
"VCmpNeqF16",
"VCmpNltF16",
"VCmpTruF16",
"VCmpxUF16",
"VCmpxNgeF16",
"VCmpxNlgF16",
"VCmpxNgtF16",
"VCmpxNleF16",
"VCmpxNeqF16",
"VCmpxNltF16",
"VCmpxTruF16",
};
[Theory]
[MemberData(nameof(Opcodes))]
public void F16CompareOpcodeLowersToMsl(string opcode)
{
var compare = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Vopc,
opcode,
[0u],
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(0x1000, [compare]),
[],
null);
var scalars = new uint[256];
var evaluation = new Gen5ShaderEvaluation(scalars, scalars, [], []);
Assert.True(
Gen5MslTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
Assert.NotEmpty(shader.Source);
if (opcode is not (
"VCmpFF16" or "VCmpxFF16" or
"VCmpTruF16" or "VCmpxTruF16"))
{
Assert.Contains("half", shader.Source, StringComparison.Ordinal);
}
}
}
@@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Xunit;
namespace SharpEmu.ShaderCompiler.Metal.Tests;
public sealed class MslFloat16ArithmeticTests
{
[Fact]
public void CompactFloat16ArithmeticUsesHalfOperandsAndPreservesRegisterShape()
{
var fixture = new Gen5ComputeFixture(
"compact-f16-arithmetic",
[
0x64000501,
0x66060B04,
0x680C1107,
0x6A12170A,
0x72181D0D,
0x741E2310,
0xBF810000,
],
StoreScalarResourceBase: 0,
StoreBackingBytes: 0);
var shader = Gen5ComputeFixtures.CompileOrThrow(fixture);
Assert.Contains("as_type<half>", shader.Source, StringComparison.Ordinal);
Assert.Contains("fmin(", shader.Source, StringComparison.Ordinal);
Assert.Contains("fmax(", shader.Source, StringComparison.Ordinal);
Assert.Contains("& 0xFFFF0000u", shader.Source, StringComparison.Ordinal);
}
}
@@ -0,0 +1,153 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5Float16ArithmeticTests
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const uint SEndpgm = 0xBF810000;
[Fact]
public void CompactFloat16ArithmeticDecodesAndCompilesWithoutNativeFloat16()
{
var program = Decode(
[
0x64000501, // v_add_f16 v0, v1, v2
0x66060B04, // v_sub_f16 v3, v4, v5
0x680C1107, // v_subrev_f16 v6, v7, v8
0x6A12170A, // v_mul_f16 v9, v10, v11
0x72181D0D, // v_max_f16 v12, v13, v14
0x741E2310, // v_min_f16 v15, v16, v17
SEndpgm,
]);
Assert.Equal(
["VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"],
program.Instructions.Select(instruction => instruction.Opcode));
var state = new Gen5ShaderState(program, [], null);
var scalarRegisters = new uint[256];
var evaluation = new Gen5ShaderEvaluation(
scalarRegisters,
scalarRegisters,
[],
[]);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
var opcodes = ReadOpcodes(shader.Spirv);
Assert.Contains((ushort)SpirvOp.FAdd, opcodes);
Assert.Contains((ushort)SpirvOp.FSub, opcodes);
Assert.Contains((ushort)SpirvOp.FMul, opcodes);
Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 2);
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
}
private static Gen5ShaderProgram Decode(IReadOnlyList<uint> words)
{
var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint));
var bytes = new byte[words.Count * sizeof(uint)];
for (var index = 0; index < words.Count; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
bytes.AsSpan(index * sizeof(uint)),
words[index]);
}
Assert.True(memory.TryWrite(ShaderAddress, bytes));
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
context,
ShaderAddress,
out var program,
out var error),
error);
return program;
}
private static IReadOnlyList<ushort> ReadOpcodes(byte[] spirv) =>
ReadInstructions(spirv)
.Select(instruction => instruction.Opcode)
.ToArray();
private static IReadOnlyList<ushort> ReadCapabilities(byte[] spirv) =>
ReadInstructions(spirv)
.Where(instruction => instruction.Opcode == (ushort)SpirvOp.Capability)
.Select(instruction => (ushort)instruction.FirstOperand)
.ToArray();
private static IReadOnlyList<(ushort Opcode, uint FirstOperand)> ReadInstructions(
byte[] spirv)
{
Assert.Equal(0x07230203u, BinaryPrimitives.ReadUInt32LittleEndian(spirv));
var instructions = new List<(ushort Opcode, uint FirstOperand)>();
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
{
var header = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
var wordCount = checked((int)(header >> 16));
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
var firstOperand = wordCount > 1
? BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset + sizeof(uint)))
: 0;
instructions.Add(((ushort)header, firstOperand));
offset += wordCount * sizeof(uint);
}
return instructions;
}
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
{
private readonly byte[] _storage = new byte[size];
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
private bool TryResolve(ulong address, int length, out int offset)
{
offset = 0;
if (address < baseAddress || address - baseAddress > int.MaxValue)
{
return false;
}
offset = (int)(address - baseAddress);
return offset <= _storage.Length - length;
}
}
}
@@ -0,0 +1,154 @@
// 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.ShaderCompiler.Tests;
public sealed class Gen5VopcF16Tests
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const uint SEndpgm = 0xBF810000;
public static TheoryData<uint, string> Opcodes = new()
{
{ 0xC8, "VCmpFF16" },
{ 0xC9, "VCmpLtF16" },
{ 0xCA, "VCmpEqF16" },
{ 0xCB, "VCmpLeF16" },
{ 0xCC, "VCmpGtF16" },
{ 0xCD, "VCmpLgF16" },
{ 0xCE, "VCmpGeF16" },
{ 0xCF, "VCmpOF16" },
{ 0xD8, "VCmpxFF16" },
{ 0xD9, "VCmpxLtF16" },
{ 0xDA, "VCmpxEqF16" },
{ 0xDB, "VCmpxLeF16" },
{ 0xDC, "VCmpxGtF16" },
{ 0xDD, "VCmpxLgF16" },
{ 0xDE, "VCmpxGeF16" },
{ 0xDF, "VCmpxOF16" },
{ 0xE8, "VCmpUF16" },
{ 0xE9, "VCmpNgeF16" },
{ 0xEA, "VCmpNlgF16" },
{ 0xEB, "VCmpNgtF16" },
{ 0xEC, "VCmpNleF16" },
{ 0xED, "VCmpNeqF16" },
{ 0xEE, "VCmpNltF16" },
{ 0xEF, "VCmpTruF16" },
{ 0xF8, "VCmpxUF16" },
{ 0xF9, "VCmpxNgeF16" },
{ 0xFA, "VCmpxNlgF16" },
{ 0xFB, "VCmpxNgtF16" },
{ 0xFC, "VCmpxNleF16" },
{ 0xFD, "VCmpxNeqF16" },
{ 0xFE, "VCmpxNltF16" },
{ 0xFF, "VCmpxTruF16" },
};
[Theory]
[MemberData(nameof(Opcodes))]
public void F16CompareOpcodeDecodes(uint opcode, string expectedName)
{
var memory = new TestCpuMemory(ShaderAddress, 0x100);
Span<byte> shader = stackalloc byte[2 * sizeof(uint)];
var word = (0x3Eu << 25) | (opcode << 17) | (1u << 9);
BinaryPrimitives.WriteUInt32LittleEndian(shader, word);
BinaryPrimitives.WriteUInt32LittleEndian(shader[sizeof(uint)..], SEndpgm);
Assert.True(memory.TryWrite(ShaderAddress, shader));
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
ctx,
ShaderAddress,
out var program,
out var error),
error);
var instruction = Assert.Single(
program.Instructions,
candidate => candidate.Encoding == Gen5ShaderEncoding.Vopc);
Assert.Equal(expectedName, instruction.Opcode);
}
[Theory]
[MemberData(nameof(Opcodes))]
public void F16CompareOpcodeLowersToSpirv(uint _, string opcode)
{
var compare = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Vopc,
opcode,
[0u],
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(ShaderAddress, [compare]),
[],
null);
var scalars = new uint[256];
var evaluation = new Gen5ShaderEvaluation(scalars, scalars, [], []);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
Assert.NotEmpty(shader.Spirv);
}
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
{
private readonly byte[] _storage = new byte[size];
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
private bool TryResolve(ulong virtualAddress, int length, out int offset)
{
offset = 0;
if (virtualAddress < baseAddress)
{
return false;
}
var relative = virtualAddress - baseAddress;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
}
@@ -53,6 +53,40 @@ public sealed class SysAbiExportAnalyzerTests
AssertSingle(diagnostics, "SHEM001");
}
[Fact]
public void DuplicateNidOnTheSameMultiAttributeHandlerIsReported()
{
var diagnostics = Analyze("""
using SharpEmu.HLE;
public static class Exports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
public static int Shared(CpuContext ctx) => 0;
}
""");
AssertSingle(diagnostics, "SHEM001");
}
[Fact]
public void EveryAttributeOnAMultiAttributeHandlerIsAnalyzed()
{
var diagnostics = Analyze("""
using SharpEmu.HLE;
public static class Exports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
[SysAbiExport(Nid = "not_a_nid")]
public static int Shared(CpuContext ctx) => 0;
}
""");
AssertSingle(diagnostics, "SHEM002");
}
[Fact]
public void MalformedNidIsReported()
{
@@ -36,6 +36,11 @@ public sealed class SysAbiExportGeneratorTests
// Guest string marshalling: the thunk reads the pointer before the handler.
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
// A single fail-closed handler may back a catalog of LLE-preferred exports.
[SysAbiExport(Nid = "5fbPUzoA2fM", ExportName = "sceLleFirst", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
[SysAbiExport(Nid = "L9NfM+f4f1Y", ExportName = "sceLleSecond", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
public static int LleFallback(CpuContext ctx) => -1;
}
""";
@@ -123,6 +128,22 @@ public sealed class SysAbiExportGeneratorTests
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
}
[Fact]
public void MultipleLlePreferredAttributesShareOneFailClosedHandler()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
Assert.Contains("\"5fbPUzoA2fM\"", generated, StringComparison.Ordinal);
Assert.Contains("\"L9NfM+f4f1Y\"", generated, StringComparison.Ordinal);
Assert.Equal(
2,
generated.Split("global::TestExports.SampleExports.LleFallback", StringSplitOptions.None).Length - 1);
Assert.Contains(
", true, global::TestExports.SampleExports.LleFallback",
generated,
StringComparison.Ordinal);
}
[Fact]
public void AssemblyWithoutExportsEmitsNoRegistry()
{
+73 -1
View File
@@ -17,7 +17,9 @@
// failure that must stay loud. Any unexpected outcome makes the tool exit
// non-zero, so it can gate scripts/CI.
//
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
// Usage:
// SharpEmu.Tools.ShaderDump [output-directory]
// SharpEmu.Tools.ShaderDump --inspect <shader.bin> [byte-count]
using System.Buffers.Binary;
using SharpEmu.HLE;
@@ -26,6 +28,76 @@ using SharpEmu.ShaderCompiler.Vulkan;
const ulong ProgramAddress = 0x100000;
if (args.Length >= 1 && string.Equals(args[0], "--inspect", StringComparison.Ordinal))
{
if (args.Length is < 2 or > 3)
{
Console.Error.WriteLine(
"Usage: SharpEmu.Tools.ShaderDump --inspect <shader.bin> [byte-count]");
Environment.ExitCode = 2;
return;
}
var inputPath = Path.GetFullPath(args[1]);
var input = File.ReadAllBytes(inputPath);
var requestedByteCount = input.Length;
if (args.Length >= 3)
{
var value = args[2];
requestedByteCount = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? Convert.ToInt32(value[2..], 16)
: Convert.ToInt32(value, 10);
}
if (requestedByteCount <= 0 ||
requestedByteCount > input.Length ||
requestedByteCount % sizeof(uint) != 0)
{
Console.Error.WriteLine(
$"Invalid byte count {requestedByteCount}; expected a positive, " +
$"4-byte-aligned value no larger than {input.Length}.");
Environment.ExitCode = 2;
return;
}
var words = new uint[requestedByteCount / sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
words[index] = BinaryPrimitives.ReadUInt32LittleEndian(
input.AsSpan(index * sizeof(uint), sizeof(uint)));
}
var memory = new FakeMemory();
memory.AddRegion(ProgramAddress, words);
var ctx = new CpuContext(memory, Generation.Gen5);
if (!Gen5ShaderTranslator.TryDecodeProgram(
ctx,
ProgramAddress,
out var program,
out var decodeError))
{
Console.Error.WriteLine($"Decode failed: {decodeError}");
Environment.ExitCode = 1;
return;
}
Console.WriteLine(
$"path={inputPath} bytes={requestedByteCount} " +
$"instructions={program!.Instructions.Count}");
foreach (var instruction in program.Instructions)
{
Console.WriteLine(
$"pc=0x{instruction.Pc:X} enc={instruction.Encoding} " +
$"op={instruction.Opcode} " +
$"words={string.Join(',', instruction.Words.Select(word => $"{word:X8}"))} " +
$"src={string.Join('/', instruction.Sources)} " +
$"dst={string.Join('/', instruction.Destinations)} " +
$"control={instruction.Control?.ToString() ?? "-"}");
}
return;
}
(string Name, bool ExpectTranslate, uint[] Words)[] testPrograms =
[
("fmac", true, [