Files
sharpemu/tests/SharpEmu.Libs.Tests
kuba 97bd8c422e Combined: Pad, AGC, Metal, Vulkan, Ampr, overlay, and CPU hot-path fixes (#727)
* VideoOut: track guest-flip FPS and label load/stall gaps

Headline FPS follows VideoOut submit cadence rather than host presents, and long gaps show LOAD/STALL instead of a stale multi-second MS average.

* Metal: skip waitUntilCompleted when the command buffer is already Completed

Tiny write-back batches often finish before the wait; checking status avoids redundant ordered-queue round-trips.

* Metal: opt-in long-edge drawable cap via SHARPEMU_METAL_CAP_DRAWABLE

Default presentation resolution is unchanged; set SHARPEMU_METAL_CAP_DRAWABLE=1 to cap the drawable long edge at 1920.

* Pad: implement scePadGetTriggerEffectState under its own NID

NID znaWI0gpuo8 was mapped to sceUserServiceGetUserName as a "title-captured
alias". It is not that symbol. Recomputing the NID of every catalogued name
(base64 of the reversed first eight sha1 bytes of name+salt) resolves
znaWI0gpuo8 to scePadGetTriggerEffectState, and sceUserServiceGetUserName
hashes to 1xxcMiGu2fo instead. Auditing all 1087 export declarations the same
way found this to be the only NID whose declared name is wrong.

The consequence was not a missing export but a wrong one: the user-service
handler rejected the pad's arguments and returned
SCE_USER_SERVICE_ERROR_INVALID_PARAMETER about eighteen thousand times per
run in PPSA10112, so every poll fell back to a cached button bitmask. It
also hid the calls from every search for pad activity, which is why this
title was believed never to touch scePad at all.

The state size is taken from the caller's own frame rather than assumed: the
guest points the out-param at rbp-0x30 and stores its stack cookie at
rbp-0x28, leaving eight bytes for the state. Writing the sixteen the frame
superficially suggests would land on the cookie and fail the guest's stack
check - the same failure this codebase has already hit three times from
oversized HLE writes - so the test pins the size by asserting the cookie
survives.

No host pad exposes DualSense adaptive-trigger feedback, so the neutral
all-zero state is reported as success, which lets the caller take its normal
path instead of the fallback.

* Apply SDWA ABS/NEG as float sign-bit, not integer, modifiers

SDWA's ABS and NEG source modifiers are floating-point sign-bit
operations on GCN: ABS clears the sign bit, NEG flips it. We applied
them as integer operations instead - SAbs, and a two's-complement
negate of the raw bit pattern. That turns 1.0 into -4.0 and -3.0 into
1.5.

UE4 compiles the final line of DrawRectangle, OutPosition.xy *=
float2(1,-1), into a single V_MOV_B32 with SDWA NEG, so every UE
fullscreen pass had its clip-space Y silently skewed. The canonical
fullscreen triangle (1,-1) (-3,-1) (1,3) became (1,-4) (-3,-4) (1,1.5),
which covers 6/11 of the viewport instead of all of it.

That reproduces the measured defect exactly, on four independent
quantities: hypotenuse slope 8/11, crossings of y=+1 and y=-1 at
x=+7/11 and x=-9/11, and covered area 6/11 = 54.55% of the 2304x1296
viewport. It also explains why the edge was resolution-independent and
identical across six unrelated shaders - it is the same instruction in
every one of them.

The distinguishing evidence is the transform's fixed point. A wrong
scale would hold NDC -1 in x and +1 in y; the observed transform holds
the opposite corner in both. Independently, and using only the measured
line rather than any assumed vertex position: a wrong multiplier alone
leaves a residual of -18 whatever the multiplier, and a wrong addend
alone forces slope 1, not 8/11. Both terms had to be wrong at once,
which only a Y-only sign-bit corruption produces.

Float instructions are unaffected: GetFloatSource passes
applySdwaIntegerModifiers: false and applies its own modifiers, so this
path only ever fed raw-source reads - where the hardware behaviour is
the sign-bit one regardless of the opcode being a bit-move.

The sign bit is selected by the SDWA source-select width so a 16-bit
select flips bit 15 rather than bit 31.

Verified: emitted SPIR-V for the same shader changes from
OpISub %uint %uint_0 %2147 to OpBitwiseXor %uint %2147 %uint_2147483648;
25-program synthetic conformance gate passes; 805 tests green; three
100-110s live runs with no crashes and no new shader failures. The
end-to-end pixel re-measurement is NOT yet closed - see task #26.

* gpu: size single-channel 16-bit and two-channel 8-bit formats

GetFormatCompatibilityClass listed only R16Sfloat in the 16-bit class, so
GetVulkanImageByteCount computed zero bytes for R16Unorm, R16SNorm, R16Uint,
R16Sint and the R8G8 family. UploadGuestImageInitialData treats a zero
expected size as an incompatible upload and drops it, which leaves the
texture blank for the rest of the run rather than failing loudly.

Silent Hill uploads R16Unorm at 144x81, 240x135, 256x256, 512x512 and
1024x1024, and every one was rejected: the guest supplied exactly
width*height*2 bytes each time (23328 for 144x81) against an expected zero.

Also adds R8SNorm to the 8-bit class, which was missing for the same reason.

* Vulkan: enable textureCompressionBC when the device supports it

Guest BC1–BC7 textures can be sampled directly when the feature is available; warn when it is not.

* Share one guest image across sRGB/UNORM aliases

Ported from origin/fix/view-compatible-guest-images 7fb8fdf.

Rendering as sRGB and ImageLoad/Store-ing as UNORM at the same guest
address are the same surface accessed through different number formats.
Recreating the guest image per number format ping-pongs content between
two VkImages and loses the rendered pixels on every transition; the
mutable-format image now accepts the counterpart identity and serves it
through alias views. The commit names AvPlayer movie copies as the
pattern that needs this.

Adapted for this base: GetOrCreateGuestImage has since grown resolution
scaling and 3D/array support, so the alias accept is folded into the
current predicate (LogicalWidth/LogicalHeight/LogicalDepth/Type) rather
than the old Width/Height pair, and the storage-counterpart widening is
placed before the physical-dimension computation. The helper functions it
relies on (GetStorageImageFormat, IsCompatibleViewFormat) already existed.

* fix(gpu): one vertex attribute per guest stream view

The scalar evaluator gave every buffer_load_format instruction its own
attribute location. Two things multiply those: the CFG walk visits one
instruction on several paths, and an uber vertex shader fetches the same
stream from every material branch. UE's larger shaders reached 56 bindings
from 8 distinct views, and one reached 701 from 5.

Metal caps a vertex function at 31 attributes, so MoltenVK failed the MSL
compile with "'attribute' attribute parameter is out of bounds" and the
surrounding vkCreateGraphicsPipelines returned ErrorInitializationFailed.
Every draw using those pipelines was dropped, which is why Silent Hill:
The Short Message rendered a black scene. The vertex buffer count drove
Metal's buffer indices out of range too, giving the companion
"cannot reserve 'buffer' resource location at index 0" failures.

Key attributes by the guest stream view they read - absolute element
address, record stride and format - and alias every other fetch that
resolves to the same view onto that binding, so both translators map
those instruction PCs to one input variable. On PPSA10112 this takes the
worst shader from 56 attributes to 8 and pipeline failures from 840 to 0.

* AGC: deliver compute-queue completion events at the queue fence

sceAgcDriverSubmitAcb submissions never produced a completion interrupt.
NotifySubmittedDcbCompleted returned early for anything that was not the
graphics queue, so an ACB reaching its ordered-queue fence published
nothing. UE 4.27's dynamic-resolution GPU-timing heuristic parks the game
thread on exactly that interrupt, so the render side never advanced and
Silent Hill: The Short Message deadlocked after its first frame.

Give every queue a CompletionEventId: 0 for graphics (what it already
published) and the owner handle from sceAgcDriverSubmitAcb rdi for a
compute queue, which is the same value the guest passes to
sceAgcDriverAddEqEvent. Publish under that ident from the existing fence
point, which both PumpSubmittedQueue and ResumeSuspendedDcb already reach
only after the submission is fully parsed.

Delivery is synchronous on the ordered guest-action queue rather than on a
ThreadPool hop with a sleep. That action runs after the logical queue has
flushed and waited for its latest fence, which is the moment hardware
would raise end-of-pipe. Deferring past it can only make the interrupt
late and reorder it against registration changes.

Gating: the per-queue completion event is unconditional, because
completion interrupts do fire on real hardware and because delivery is
registration-gated -- TriggerRegisteredEvents only queues onto equeues
that registered this exact (ident, graphics filter) pair, and
sceAgcDriverAddEqEvent is the only producer of graphics registrations. A
title that never registers its ACB owner handle observes no change.
SHARPEMU_AGC_SUBMIT_COMPLETION_EVENT is left to gate only the broad
ident-ignoring fan-out (TriggerRegisteredEventsDistinct), which is a
compatibility guess rather than hardware behavior; it also stays scoped to
the graphics queue where it was measured, so enabling the flag does not
newly fan out across compute queues.

* fix(gpu): reflect guest CPU writes into large and render-target-aliased images

Ports the still-applicable half of the archived fork's stale-texture fix
(a42ccae) onto current main. Silent Hill: The Short Message (PPSA10112)
shows both faces: black title-screen UI (glyph atlas frozen at its first
upload) and stale/garbage rows on the brightness screen (a 3840x2160 UI
sheet whose backing bytes were never re-read).

1. Write tracking is armed under a byte budget, not a resolution cap.

   GetOrCreateGuestImage armed GuestImageWriteTracker only when
   target.Width <= 1920 && target.Height <= 1080. Silent Hill renders at
   3840x2160, so every one of its render targets was excluded from
   CPU-write tracking and no guest rewrite of one could ever invalidate
   it: SyncCpuWrittenGuestImages (the flip / ACQUIRE_MEM re-upload path)
   only ever visits ranges the tracker armed.

   The cap presumably existed as a perf guard, but resolution is the
   wrong proxy for the cost. Arming is one mprotect over the range, and
   the fault handler unprotects the whole range on the first store, so a
   write burst costs one fault regardless of size. What actually scales
   with the surface is the dirty re-upload: one byte[byteCount]
   allocation plus a full guest-memory read per dirty flip. So the guard
   is now a byte budget, set equal to the 128 MiB limit that
   SyncCpuWrittenGuestImages itself enforces before re-uploading. Above
   that, arming can only cost faults; it can never produce a re-upload.
   That is generous for 4K (RGBA8 32 MiB, RGBA16F 63 MiB, RGBA32F
   127 MiB all fit) while still excluding volume textures that a
   resolution cap could not see at all (512^3 RGBA8 is 512 MiB behind a
   "512x512" surface).

   Both sites now also arm the exact extent recorded in
   _guestImageExtents (GetTextureByteCount) instead of
   Width*Height*depth*GetTextureBytesPerPixel, so the armed range and
   the range the sync path reads back are the same bytes; the old
   expression over-counted block-compressed and unknown formats.

2. The CPU-texture refresh path no longer gates on IsCpuBacked alone.

   TryCreateCpuTextureRefreshResource bailed on !guestImage.IsCpuBacked.
   That flag is a latch: it flips false the first time an address is
   used as a render target and never flips back. A surface that was
   rendered into once and is afterwards rewritten by the guest CPU was
   therefore frozen at its last GPU content forever, even when the parse
   thread had already shipped fresh texels for it.

   The gate is replaced by ShouldRefreshGuestImageFromCpu: CPU-backed,
   or the parse-time write generation is above zero and differs from the
   generation recorded by the last upload. Keeping the positive-
   generation requirement preserves the pure GPU-feedback case (render
   into an image, then sample it) that IsCpuBacked used to protect: such
   a surface is now tracked (change 1) but never CPU-written, so its
   generation stays zero and its live image is left alone. This matters
   more than it did in the fork, precisely because change 1 arms
   tracking on far more render targets. Dropping the gate outright, as
   the fork did, would let a target sampled under a format tag that the
   availability map does not match be overwritten once with whatever
   sits in guest memory.

   The existing content fingerprint still suppresses redundant uploads,
   and MarkSampledImagesInitialized records the uploaded generation, so
   a rewritten surface re-uploads exactly once per guest write burst.

Not ported: the fork's third part added a PeekDirty guard to the
parse-time snapshot fast path in AgcExports. It is superseded. That path
now calls IsGuestImageUploadKnown, which already compares
_cpuBackedUploadGenerations against the tracker's write generation - a
monotonic value that survives another owner consuming the dirty flag,
unlike PeekDirty, which both EvictDirtyCachedTextures and
SyncCpuWrittenGuestImages clear. Guest images with no generation entry
are covered instead by SyncCpuWrittenGuestImages. Adding a non-consuming
PeekDirty there would also make every draw between a CPU write and the
next flip fall through to a full texel re-read of the surface (33 MiB
for a 4K sheet), since Track re-arms without clearing the dirty flag.
The fork's promotion-path Track ("vulkan.cpu-backed-image") is likewise
superseded: AgcExports already arms every sampled texture's backing
extent as "agc.decoded-texture" before reading its texels.

Both decisions are extracted as pure internal predicates so they can be
unit-tested; the Vulkan device code around them needs a real device.

* Kernel: correct equeue event delivery and waiter lifetime

Ports the event-queue rework from the archived silent-hill-minimal branch
(968e9606, 9e3abb12, 2be9cfbf, f157a115) onto current upstream. The equeue
implementation had not been touched upstream since that branch forked, so
none of it had landed.

Behaviour fixed:

- Per-waiter event reservation. A blocked waiter used to wake on "the queue
  has any pending event" (TryWake => HasPendingEvents) and then re-read the
  queue on resume, so a woken waiter could find the event already drained by
  another waiter and park again with the wake consumed. Events are now
  reserved to the waiter they are delivered to.
- Level-triggered events are preserved instead of being cleared by an
  unrelated read; only events that declare clear-on-read reset their trigger
  state.
- Queued interrupts are bound to the registration generation that produced
  them, so an event registered after a queue was reused cannot consume an
  interrupt raised for the previous registration.
- Deleting an equeue now terminates its waiters instead of leaving them
  blocked on a handle that no longer resolves.
- sceKernelTriggerUserEvent stores its third argument in the event's udata
  (0x18) rather than its data word (0x10). The guest reads it back with
  sceKernelGetEventUserData, which loads 0x18, so every triggered user event
  previously read back as 0. This matches the reference behaviour in shadPS4,
  where TriggerEvent takes udata and sceKernelGetEventUserData returns
  ev->udata. The upstream test asserting the data word is updated, since it
  encoded the inconsistency rather than the ABI.

KernelPthreadState gains TryGetCurrentThreadIdentity and a new
KernelSyncTraceFormatter carries the shared, opt-in diagnostic formatting the
ported code calls; both are gated behind the existing trace flag and do no
work when it is off.

Tests: 662 pass, 0 fail (SharpEmu.Libs.Tests 588 -> 598).

* Implement VOP3 0x149 V_BFE_I32 and 0x36A V_CVT_PKRTZ_F16_F32

PPSA10112 dropped three shaders per run on two unimplemented VOP3
opcodes. The translator fails a whole shader on an opcode it does not
know, so each one costs a dropped draw rather than wrong pixels.

0x149 sits between 0x148 V_BFE_U32 and 0x14A V_BFI_B32, and 0x36A
between 0x369 V_CVT_PKNORM_U16_F32 and 0x36D V_ADD3_U32; the
surrounding table entries already match the canonical map densely on
both sides of each gap. V_CVT_PKRTZ_F16_F32 needed no emitter - it was
already implemented for the VOP2 form at 0x2F and its VOP3 alias at
0x12F, and only the VOP3-only opcode number was missing.

V_BFE_I32 mirrors its unsigned sibling, masking offset and width to 5
bits, and differs only in extracting through a signed type so the field
sign-extends. GCN defines width 0 as returning 0 where SPIR-V leaves a
zero Count unspecified; VBfeU32 has the same gap, so this matches it
deliberately rather than diverging - fix both together if it matters.

Also widen ReportGuestPointerSplit, which filtered to an incoming value
of exactly 1. The crash being hunted leaves 0x0000007000000000, whose
low dword is 0, so the one detector built to catch this bug class could
never have reported it.

Verified: unsupported-opcode errors 3-4 per run -> 0, over a 130s run
that survives to the same stage. Metal translator still lacks VBfeI32;
Vulkan/MoltenVK is the macOS path so it is a divergence, not a blocker.

* AGC: retain active label producer records

Ported from the archived silent-hill-minimal branch (274ccfdf).

RegisterLabelProducer bounded its history with RemoveRange(0, 1024) once it
reached 4096 entries, which evicts the oldest records regardless of whether
their label write has completed. Those records are not a diagnostic cache:
a suspended DCB resolves its wait_reg_mem by finding the producer that will
write the watched label. Dropping an active record hides an earlier
same-submission label write, so a legitimate in-stream fence suspends
forever and the graphics queue stops.

Compact only completed history, and let the list exceed the soft bound when
every record is still active — correctness over a diagnostic limit.

Two hardening changes over the ported version, because a suspended queue is
exactly when every record is active:
- compaction is a single order-preserving pass instead of repeated RemoveAt,
  which would shift the tail per eviction (quadratic) while the label gate is
  held;
- when a pass frees nothing the bound is raised to twice the current count, so
  registration does not rescan the whole list on every subsequent add, and it
  is reset once compaction can make progress again.

Tests: 664 pass, 0 fail (SharpEmu.Libs.Tests 598 -> 600).

* AGC: keep produced label values a suspended wait still needs

GpuWaitRegistry.RecordProduced dropped the entire produced-value table once it
reached 8192 entries:

    if (_lastProduced.Count >= 8192) { _lastProduced.Clear(); }

Those entries are release state, not a cache. CollectDeadlockBroken is the only
way out for a DCB suspended on a WAIT_REG_MEM whose label the guest has since
recycled for other data: it replays the value a real producer already wrote to
that label. Clearing the table wholesale erases exactly the records live waiters
depend on, so every such waiter is stranded permanently — the suspended graphics
queue never resumes, and with it the render thread and the whole engine thread
graph park.

That is what Silent Hill (PPSA10112) hits. Its final unsatisfied waits watch
labels holding guest allocator metadata (each reads a pointer into its own
region rather than the expected 1), i.e. recycled memory, which is precisely
the case the deadlock breaker exists to handle — yet agc.deadlock_break fires 0
times across a run with 490 wait suspensions and 1068 successful release_mem
writes, because the producing values had already been cleared.

Prune only values no registered waiter is watching, and let the table exceed the
soft bound while they are all watched. Same reasoning as the label-producer
history in the preceding commit: correctness of synchronization state takes
precedence over a diagnostic bound.

Tests: 666 pass, 0 fail. The regression test fails against the previous
wholesale Clear() and passes with the prune, verified by reverting the fix.

* Correct VOP3 0x36A: V_CVT_PK_U16_U32, not V_CVT_PKRTZ_F16_F32

a2d186e mapped 0x36A to V_CVT_PKRTZ_F16_F32 on the strength of a gap in
our own opcode table. That was wrong, and wrong in the worst available
way: an emitter for V_CVT_PKRTZ_F16_F32 already existed, so instead of
failing loudly like an unknown opcode, the mapping would have emitted
float-pack semantics for an integer-pack instruction and produced
silently incorrect results.

LLVM is unambiguous:
    defm V_CVT_PK_U16_U32    : VOP3Only_Real_gfx10<0x36a>;
    defm V_CVT_PKRTZ_F16_F32 : VOP2_Real_gfx6_gfx7_gfx10<0x02f>;
so on Gen5 the float pack is VOP2 0x2F with VOP3 alias 0x12F - both of
which our table already had - and there is no VOP3-only encoding of it
to add. 0x36A sits with the other integer/normalised pack conversions
at 0x368/0x369/0x36B.

silent-hill-minimal-rebased had this right all along.

The V_BFE_I32 half of a2d186e stands: LLVM confirms 0x149, and that
branch maps it identically.

Lesson, since I had just warned someone else about exactly this: a gap
in a table is evidence about numbering, not about identity. Inference
from neighbouring entries is fine for narrowing candidates and worth
nothing as a conclusion - especially when a plausible emitter already
exists to swallow the mistake quietly.

* AGC: report release-label writes that never reach guest memory

Both release_mem handlers recorded the produced label value only on success:

    if (wroteData && dataSelection is 1 or 2) GpuWaitRegistry.RecordProduced(...)

with no else. A failed write is not benign here. That packet is the producer a
suspended WAIT_REG_MEM is waiting for, so when it fails the label is neither
written nor recorded, CollectDeadlockBroken has no value to replay, and the
graphics queue stays suspended forever — the render thread and the engine
thread graph park behind it with nothing in the log to say why.

Measured on Silent Hill (PPSA10112): the fatal AccessViolation lands exactly
here, on the presenter thread, in ApplySubmittedReleaseMem's label write into a
protected guest page. The faulting variant kills the process; the
returns-false variant wedges the GPU silently. Only the first was ever visible.

Report the failure (rate-limited: first 16, then powers of two) instead of
dropping it. This does not fix the underlying write failure — it makes a
permanently suspended queue diagnosable rather than silent.

Tests: 666 pass, 0 fail.

* CPU: gate periodic import logs and fast-path memcpy/memmove leaves

SHARPEMU_LOG_IMPORT_PERIODIC=1 re-enables periodic Import# tracing. memcpy/memmove use a thin TryCopy leaf that skips register marshalling.

* Ampr: cooked-id index preload and host FD LRU cache

Preload the app0 APR index during bind and bound the open host-file cache so streaming reads stay responsive under large title archives.

* Vulkan: abandon timed-out guest fences without blocking later frames

Move fence-timeout submissions out of the blocking queue, keep GPU objects alive until the fence signals, and retire them from an abandoned list so one hung dispatch cannot re-block capacity waits.

* AGC: follow command-buffer branches across arena switches

Implement sceAgcCbBranch and walk INDIRECT_BUFFER so submissions that continue in a linked buffer keep their flip and end-of-frame labels.
2026-07-31 12:14:02 +03:00
..
2026-07-28 03:33:26 +03:00
2026-07-28 03:33:26 +03:00
2026-07-18 23:44:57 +03:00
2026-07-28 03:33:26 +03:00