Fix for DS,Little Nightmares, Little Nightmares III and Astro Bot shader (#913)
* shader(decode): fill MIMG float-atomic and SOP1 opcode gaps
Gen5ShaderTranslator's opcode name tables were missing several real
RDNA2 opcodes that Ghost of Yotei's shaders use: MIMG 0x1D-0x1F
(ImageAtomicFcmpswap/Fmin/Fmax) and SOP1 0x15/0x1B/0x1C/0x1E/0x34/0x3B
(SFlbitI32B32, SBitset0/1 B32/B64, SAbsI32, SBitreplicateB64B32).
Without names these decoded as unknown instructions and aborted
translation before the pipeline ever reached GPU submission.
* shader(resources): accept float-numeric-class atomic image descriptors
ResourceMaterializer rejected any atomic image descriptor whose guest
format wasn't exactly Format32Uint, but real atomic-float images
(ImageAtomicFmin/Fmax) arrive with a float-numeric-class format. Relax
the check to also accept formats whose SampledNumericClass is Float,
and default the null-descriptor's NumericClass to the image's own
class for atomics instead of always Float.
Also surface the actual materialization failure reason in
ShaderProgramCache's error message instead of a generic string, so
each subsequent blocking bug could be identified from the log without
extra instrumentation.
* shader(bindings): add a dedicated atomic-float image binding range
BindingLayout only had a single Atomic-Uint binding range (32-36), so
any atomic image with a float numeric class had nowhere to bind and
ForImage() returned null for it. Add AtomicFloatBinding (37-40),
bump ImageBindingCount 36->41, and split Describe()'s atomic range
into its Uint and Float sub-ranges to match.
Also add an Atomic flag to ShaderImageShape (TargetRegisterWords.cs)
so downstream view-format resolution can distinguish atomic images
from ordinary sampled/storage ones.
* spirv: emit ImageAtomicFmin/Fmax and DsSwizzleB32, fill SOP1 ALU gaps
Vulkan has no native float image-atomic-min/max instruction, so emit
them the way KytyPS5 does: OpImageTexelPointer (typed uint) ->
OpAtomicLoad -> compare-and-swap loop (OpPhi/OpFOrdLessThan/OpSelect
on the bitcast float value) -> OpAtomicCompareExchange, returning the
pre-exchange value from EmitAtomicWordUpdate (now non-void, valid SSA
at the loop's merge block).
Add DsSwizzleB32 (bit-mode/quad-mode lane shuffle via
GroupNonUniformShuffle) to the LDS-opcode switch and to
UsesSubgroupShuffle(), and add the SFlbitI32B32/SAbsI32/SBitset0B32/
SBitset1B32 SOP1 cases so the newly-decoded opcodes from
Gen5ShaderTranslator actually translate to SPIR-V instead of hitting
the generic "unsupported instruction" fallback.
* spirv: declare atomic image OpTypeImage with a Uint sampled type
DeclareImageClass previously derived the SPIR-V OpTypeImage's sampled
type purely from the binding's logical NumericClass, so an
atomic-float image (logical class Float, backed by our new
compare-and-swap emission on a uint texel pointer) still declared its
OpTypeImage as float-sampled. That violates
VUID-StandaloneSpirv-Image-04965: the R32ui image format and a float
sampled type disagree on numeric class. Force the sampled type (and
format, already correct as R32ui) to Uint whenever the image is
atomic, regardless of its logical numeric class.
* gpu: back atomic-float images with a real Uint VkImageView
The SPIR-V side now declares atomic images as Uint (OpTypeImage), but
the actual VkImageView bound at dispatch time was still created with
the descriptor's own float-numeric-class format, tripping
VUID-vkCmdDispatch-format-07753 (the shader's declared sampled type
must match the bound view's format numeric class).
Fix both resolution paths:
- VulkanVideoPresenter.Descriptors.cs: NumericClassOf() now forces
Uint for any atomic image (this covers the null-descriptor
fallback path), and ShapeOf() forwards the Atomic flag.
- ImageRequestBuilders.Texture.cs: the real, non-null descriptor path
resolves its view format independently from the descriptor's raw
GuestPixelFormat and was untouched by the above. Extend the
existing Sint-storage override (which already forces such images to
Format.R32Uint) to also cover shape.Atomic.
Confirmed via a fresh Vulkan-validation-layer run against Ghost of
Yotei that VUID-vkCmdDispatch-format-07753 no longer appears.
* fix(shader): reconcile atomic SPIR-V emitters with upstream
* spirv: fix ImageGetResinfo querying a non-existent sampled-image combo
ImageGetResinfo never goes through UsesSampler() (it only matches
ImageSample*/ImageGather* opcodes), so TryResolveLayoutImage always
resolves imageObject as the raw OpTypeImage value for this
instruction, never a sampled-image combo. The size-query code still
called OpImage on it whenever resource.IsStorage was false, expecting
to extract a combo that was never built, which spirv-val rejects:
"Expected Sample Image to be of type OpTypeSampleImage". Just use
imageObject directly in both branches.
This surfaced only after the earlier atomic-image fixes let Ghost of
Yotei's shader compilation get this far; it is an unrelated,
pre-existing bug in the base PR.
* gpu: remap compute workgroup axes past the Z-dimension device limit
Ghost of Yotei dispatches a compute shader with NUM_THREAD (1,1,256).
Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64):
the Z axis has a much tighter guaranteed limit than X/Y, and this ran
into it for real: "LocalSize Z (256) exceeds device limit
maxComputeWorkGroupSize[2] (64)" at vkCreateComputePipelines, even
though the NVIDIA RTX 5070 here allows up to 1024 total invocations.
Fix by remapping logical GNM axes onto physical SPIR-V/dispatch axes
at shader-compile time: the largest NUM_THREAD axis always lands on
physical X (most headroom), the smallest on physical Z (tightest
limit). Gen5SpirvTranslator.ComputeWorkgroupAxisOrder computes this
permutation purely from the three thread-group sizes, so:
- The translator uses it to declare the physical LocalSize execution
mode and to read gl_LocalInvocationId/gl_WorkGroupId's components
back into the correct logical VGPR/system-register slots, so guest
shader code (which only ever sees logical TID/WorkGroupId values)
is unaffected by the remap.
- RenderExecutor.Compute.cs recomputes the identical permutation from
the same thread-group sizes (input.ThreadsX/Y/Z) to reorder
vkCmdDispatch's group counts the same way, with no new metadata
plumbing between the shader compiler and the dispatch path.
* shader(resources): drop redundant ImageHandle ScalarBufferWord guard; add opt-in resource-tracker diagnostics
GetHandleSource had a blanket rejection of any ImageHandle whose dword
was a ScalarBufferWord, predating ValidateSource/RuntimeValueValidator
being able to generically resolve such dwords at bind time (the same
mechanism MaterializationSources already uses for buffer descriptors).
Drop the redundant, narrower guard and let ValidateSource be the
single source of truth, so legitimately resolvable image descriptors
loaded from a scalar buffer (not just the two hardcoded heap/direct
shapes TryMakeIndirectImage/TryMakeDirectImage recognize) aren't
rejected outright.
This did not resolve Ghost of Yotei's current blocker: its compute
shader at hash=0xCECFBCC210CB2372 pc=0x59C resolves an image
descriptor's byte offset through a loop-carried Phi (a hash-table/
linear-probe style bindless material lookup) that is not loop-
invariant, so it genuinely cannot be pre-evaluated as a single
per-dispatch runtime value the way MakeRuntimeBufferSource/
RuntimeValueEvaluator expect. That needs real GPU-side dynamic
descriptor indexing support, which doesn't exist yet in this
resource-tracking pass.
Also add SHARPEMU_RESOURCE_TRACKER_DIAG=1, gating verbose stderr
tracing in TryMakeIndirectImage's rejection paths and
RuntimeValueValidator's node-validation failures. This is what
localized the loop-carried Phi as the actual root cause; keeping it
opt-in and off by default costs nothing at runtime.
* shader(resources): degrade control-dependent bindless image/sampler descriptors to null instead of failing compilation
Ghost of Yotei's material lookup resolves an image descriptor's
offset through a loop-carried phi (a hash-table/linear-probe bindless
scheme) that RuntimeValueValidator correctly can't treat as a single
compile-time source - this resource tracker has no GPU-side dynamic
descriptor indexing to fall back on. Track *why* validation failed
(RuntimeValueValidator.ControlDependent, set when the failure traces
to a non-invariant phi) and, for image/sampler handles specifically,
degrade to a null descriptor instead of hard-failing shader
recompilation. Buffer handles and any other failure reason still fail
as before.
This ports KytyPS5's fix for the identical case
(feat/shader-control-dependent-descriptor, "shader: fall back to a
null image when its descriptor is control-dependent") rather than
attempting full dynamic descriptor indexing support, which doesn't
exist in either codebase.
Also add SHARPEMU_RESOURCE_TRACKER_DIAG tracing in
RuntimeValueEvaluator's evaluation failure paths (per-source-dword,
flattened table reads, and raw memory reads), used to localize the
next blocker down this same path.
* gpu: keep image shader types compatible with Vulkan views
* shader: decode and emit VOP1 0x39 (VFfbhU32, find-first-bit-high)
Another decode gap in the same family as the earlier MIMG/SOP1 fixes:
VOP1 0x39 was missing from the opcode name table, aborting
translation of Ghost of Yotei's next compute shader before it reached
GPU submission. Ported the fix straight from our own tree's
Gen5ShaderTranslator.cs/Gen5SpirvTranslator.Alu.cs, which already
decodes and emits it: reverse the bits then use the GLSL FindILsb
extended instruction, since RDNA reports the leading-zero count with
the architecturally required 0xFFFFFFFF sentinel for a zero source.
* shader(resources): treat a null-based SRT raw read as zero instead of faulting
A bindless pointer the guest leaves unbound for a dispatch resolves
its address-handle dwords to zero; EvaluateRawRead's raw-address
branch (as opposed to the buffer-descriptor branch) then dereferenced
that near-null address directly, and the guest memory reader
correctly refused it as unmapped, failing the whole materialization
with a generic "reason=Other". Ghost of Yotei's next compute shader
hit exactly this: a flattened SRT table read through a null-resolved
48-bit pointer plus a small immediate offset landed at address 0x14.
Treat any raw-address read below a small guest null-page threshold
(0x10000) as reading zero, so the value (and whatever descriptor it
feeds) collapses to null instead of faulting materialization. Ported
directly from KytyPS5's identical fix for the identical symptom
("shader: treat a null-based SRT constant read as zero").
* shader: decode/emit VOPC 64-bit compares and the 64-bit LDS atomics
Ghost of Yotei's cs 0x80003AF600 emits V_CMPX_NE_U64 (VOPC 0xF5) and DS_ADD_U64/DS_OR_B64 (0x40/0x4A), which the decoder refused. The VOPC 0xE0-0xE7 / 0xF0-0xF7 forms are the same V_CMP/V_CMPX U64 operations the VOP3 table already names and the emitter already lowers, so mirror them into the two-operand VOPC table.
The 64-bit LDS atomics sit just ahead of DS_WRITE_B64. LDS is modelled as an array of dwords, so each is lowered to the two 32-bit atomics that make it up. Neither opcode returns the old value, which makes the split exact: only the final memory state is observable. OR is bitwise-independent, so two 32-bit ORs are the 64-bit OR; ADD folds exactly one carry per low-dword wrap into the high dword, so the high word converges on the true wrap count regardless of lane interleaving. The RTN variants cannot be split without losing the returned pair's atomicity, so they stay undecoded and keep failing loudly.
* shader: decode and emit VOP3-encoded 64-bit VOPC compares (0xE0-E7, 0xF0-F7)
Ghost of Yotei's material-lookup shader issues V_CMP_*_U64/V_CMPX_*_U64
through the VOP3 encoding (opcode < 0x100, the same numbering VOPC
uses), which DecodeVop3's own opcode table never listed - it only
handled VOP3-native opcodes >= 0x101. Ported the fix straight from our
own tree, which already special-cases this exact sub-range (see the
"VOP3-encoded 64-bit VOPC" comment there): decode 0xE0-E7/0xF0-F7 to
the VCmp*U64/VCmpx*U64 names, and emit them as a 64-bit integer
compare (or a constant true/false for the F/T pseudo-compares),
reusing the existing generic VCmpx exec-mask handling.
* shader: implement GFX10 double-precision conversions and arithmetic
Decode VOP1 0x04/0x0F/0x16/0x2F/0x30/0x34 and their VOP3-encoded forms (V_CVT_F64_I32, V_CVT_F64_U32, V_CVT_F32_F64, V_RCP_F64, V_RSQ_F64, V_SQRT_F64) and emit them as real SPIR-V doubles. A 64-bit result is split across the destination register pair: the low word goes through the common destination store and the high word is stored alongside it, both under the EXEC guard. V_MUL_F64 (VOP3 0x165) and V_FMA_F64 (VOP3 0x14C) are added as well.
Source absolute/negate modifiers and the VOP3 output modifier and clamp are honoured. FP64 inline constants and literals are rejected explicitly rather than read as the wrong bits, following the gfx10 expansion rules. The shaderFloat64 device feature is enabled only when the physical device offers it, and the Float64 capability is declared only for modules that use a double.
Ghost of Yotei compute shader 0x8000393600 (hash 0xB90E2024732C6111) now translates and compiles instead of stopping at decode.
* shader: decode and emit VOP2 V_PK_FMAC_F16
V_PK_FMAC_F16 (VOP2 0x3C) reads the destination register as the addend for both packed f16 lanes and writes D = S0 * S1 + D back as a packed pair. Reuse the existing single-rounding fused-f16 path per lane and merge the two halves, so the result matches the hardware's f16 fused behaviour.
Ghost of Yotei compute shader 0x8000393600 reaches this opcode after the decoder is past the f64 gap.
* shader: decode and emit VOP3 V_MAD_I32_I24
V_MAD_I32_I24 (VOP3 0x142) differs from the already-handled V_MAD_U32_U24 only in that both multiplicands are signed 24-bit values. Sign-extend each operand from bit 23, multiply with 32-bit wrap and add the third source.
Ghost of Yotei compute shader 0x8000393600 reaches this opcode after the decoder is past the f64 gap.
* shader(resources): address runtime-fetched buffer descriptors via the device-address page table
A V# whose four dwords are all read from a runtime scalar-memory address (a descriptor-array entry indexed by a loop counter, as in Ghost of Yotei's cs 0x80003F2A00 at pc=0x1B4) cannot be bound ahead of time, so resource tracking used to fail the whole shader. Its dword reads are not control-dependent in any other way: they are real guest memory loads emitted at runtime through the device-address page table.
Detect that case in ResourceTracker and mark the access RuntimeBufferDescriptor instead of building a dense buffer resource. The SPIR-V backend then builds the guest address from the descriptor's runtime base (SGPR pair), the MUBUF offset/soffset/vector offset and, when indexed, the descriptor's stride, and stores/loads through the existing physical-storage-buffer path. num_records/stride bound the access so a null or empty descriptor binds nothing, matching the static path's range check. Formatted, typed and atomic accesses keep failing loudly because their descriptor format cannot be reconstructed from the raw words.
This completes the former d8959888aafd2552 dispatch.
* shader(resources): recognize runtime V# descriptors built from SRT reads and loop phis
The first runtime-descriptor detection only accepted four direct raw ScalarAddressWord dwords. Ghost of Yotei's cs 0x8000289300 at pc=0x78C builds its V# from a loop-carried phi that merges two SBufferLoadDwordx4 reads of the SRT buffer at a dynamic offset, so the access was still rejected even though the descriptor is a real runtime value.
Make the detection generic: a BufferHandle is a runtime descriptor when each dword is a plan-time-known value (constant, user data, shader base, flattened table word) or a guest-memory read, recursing through phis, selects and operations, and at least one leaf is a raw read. A cycle through a phi is accepted the same way the read planner treats cyclic phis. Anything else keeps hard-failing.
The raw BufferLoadUshort at pc=0x78C now lowers through the device-address page table. The formatted BufferLoadFormatX at pc=0x18F4 correctly escalates to BoundedCandidateTable, which is still unimplemented and is now the next required feature.
* shader(resources): plan bounded runtime V# candidate tables
* shader(resources): materialize bounded runtime V# candidate tables
* shader(resources): address a runtime scalar buffer V# through the device page table
* shader(resources): model the guest V# separately from its Vulkan lowering; real 64-bit LDS atomics
Replace MemoryAccessInfo.RuntimeBufferDescriptor with a GuestBufferDescriptor that carries the V#'s provenance (Static or Runtime) and, for static descriptors, the dense-buffer index. The backend now chooses an explicit BufferLoweringStrategy from the descriptor and the access shape via GuestBufferDescriptor.ChooseStrategy, instead of hard-coding a single path: NativeBinding for compile-time descriptors, PhysicalStorageBuffer for a runtime descriptor used by a raw dword/subword access, and BoundedCandidateTable for a runtime descriptor whose format/type cannot be reconstructed from the raw words. The last one is not implemented and fails loudly with a precise reason, which is now a single, discoverable place rather than a boolean scattered through the translator.
DS_ADD_U64/DS_OR_B64 are now emitted as true 64-bit workgroup atomics when the device exposes shaderSharedInt64Atomics (queried through VkPhysicalDeviceShaderAtomicInt64Features, declared with the Int64Atomics capability, addressed through a 64-bit LDS view). The previous two-32-bit split is kept only as a documented fallback for devices without the feature; it is not atomic as a pair. Device setup reports the capability once.
* shader/image: accept 11_11_10 UNORM sampling and 8-bit sRGB render targets
The unified image format 30 is 11_11_10_UNORM, a packed 11/11/10 normalized float format the rendering side already knows as GuestPixelFormat.Bits11_11_10UNorm (mapped to VK_FORMAT_B10G11R11_UFLOAT_PACK32). It was missing from the shader-side sampled-format table, so cs 0x8000265D00 failed specialization with 'sampled image descriptor 7 uses unsupported format 30'.
Bits8Srgb and Bits8_8Srgb also had RenderTargetBytesPerElement = 0 while their element sizes are 1 and 2 bytes, which made TryGetTextureSize reject a RenderTarget-tiled 8-bit sRGB surface (tile mode 27) that the guest creates. Their render-target footprint is the same as the matching UNORM formats, and the host view already substitutes R8/R8G8 UNORM, so give them the real byte counts.
* shader: allow inline constants and literals as VOP3P packed-f16 operands
The VOP3P packed-f16 path only accepted register operands and refused constants, so cs 0x80003AF600 stopped at V_PK_FMA_F16 with src[128]. An inline constant or literal is a single f32 value rather than a packed register pair, so there is no half to select: both lanes take the exactly-converted f16 of that constant, with the lane's negate modifier still applied. Keeps the register path unchanged.
* shader: support Yotei f16 shifts bitsets and safe LDS atomics
* shader(vulkan): dispatch bounded runtime V# candidate tables
* shader: support additional LDS operations
* shader: support RDNA LDS store encoding
* shader: handle LDS operands and RDNA buffer cache op
* shader: extend strict GFX9 buffer and ALU decoding
* [VideoOut] added output mode events and profiling fixes
* [Gpu] fixed image, render, and dispatch handling
* shader: track SDWA and runtime buffer values
* [Shader] expanded Gen5 resource and interpolation support
* gpu: reconcile PR893 metadata and indirect dispatch
* gpu(commands): consume unknown NOP marker ids instead of failing the submission
Demon's Souls emits markers 0x6EC and 0x80C, which the interpreter treated as
fatal. The packets carry no work for the ids without a known side effect
(flips, user-data marker context), so they are consumed and each id is
reported once (command_stream.marker_ignored). voidborn, which gets much
further on this title, likewise ignores NOP marker payloads it has no
handler for.
* kernel: implement sceKernelGetOperationMode (PS5 base console, no submode)
The import was unresolved, so callers polling it (Demon's Souls does it at very
high frequency) got an unresolved-import result instead of the mode. Behaviour
matches KytyPS5 KernelGetOperationMode: *mode = 2, *submode = 0.
* loader: protect red-zone functions with enclosing patch spans
The red-zone patcher only built a trampoline span forward from the faulting
instruction, so it gave up whenever the next instruction was a branch target,
touched RSP, or changed control flow. Those refusals never reached FailedSites,
and the summary line reported sites=11472/11472 failed=0 while 777 faultable
instructions inside red-zone functions stayed unprotected.
Demon's Souls died on one of them. A GPU write-tracking fault at 0x800AD91F8
dispatched on the guest stack with RSP unshifted, which let Windows overwrite
the 128 bytes below RSP; the function reloaded its pointer from [rsp-0x10],
got 0, and faulted writing through it. The fault census confirms the same RSP
in the faulting frame as in the crash report.
Add TryBuildEnclosingPatchSpan: when the forward search refuses, take the
span's bytes from before the faulting instruction so the span ends at that
instruction and the one after it is never rewritten, which keeps every existing
branch into it valid. Two invariants make this safe and both are exercised by
the new tests:
- the RSP shift brackets only the faultable core, so a relocated
RSP-relative access still reads the slot it meant to;
- a span may not start before the end of the previous site, because a
backward span can otherwise reach instructions an earlier site already
replaced with its own jump.
Patch() also refuses overlapping sites outright rather than corrupting guest
code, and the scan now reports unrelocatable sites by category.
Unprotected sites drop from 777 to 270. The title runs past the main menu into
its opening cinematic and stops on an unrelated shader resource error.
The fiber tests move into a shared collection: they mutate the same static
FiberExports state as FiberExportsTests, and xUnit runs separate classes in
parallel, which made them fail intermittently.
* cpu: count guest faults and make access-violation reports readable
Guest code is commonly mapped PAGE_EXECUTE, which no host read can satisfy, so
IsReadableHostRange refused it and the whole call-site section of the access
violation report was skipped: every crash in such a title printed "Could not
read code at RIP" and nothing else. Read those windows through the guest memory
view instead, and print the faulting region so a future refusal explains
itself.
Add a census of the recoverable faults taken on guest threads, by guest RIP and
by the handler that resolved them. Windows has no alternate exception-dispatch
stack, so each of those faults overwrites the 128 bytes below RSP; knowing which
guest instructions fault is what turns a red-zone corruption from a hypothesis
into a measurement. Counters are allocated once per RIP and are inert unless
SHARPEMU_FAULT_CENSUS is set, since the handler is hot.
Also fix the guest context transfer frame: it published the stack bounds in the
wrong order, storing the low address in NT_TIB.StackBase and the high one in
StackLimit on every fiber switch.
Note: these two files also carried uncommitted work from earlier in this
worktree (guest stack sizing, the 22-qword transfer frame, import and exception
paths); that work is included here rather than separated, because the hunks
interleave.
* gpu: stop handing image ownership to a buffer that was only bound
Binding a formatted writable buffer over an image's memory invalidated every
image covering that range. The shader only declares that it may write there, so
this fired on every bind of every draw, and when the range belonged to a render
target the image was refreshed from guest memory the GPU never wrote - its
tiled backing read as linear, over the frame that had just been rendered.
A Demon's Souls capture showed 412 buffer-dirty refreshes and 207 GB of image
traffic. Suppressing the two bind-time call sites removed the banding entirely
and took the frame time from 7471 ms to 4272 ms, with GPU occupancy going from
29% to 45%; buffer-dirty refreshes dropped to zero.
Writes that actually happened still invalidate, from GuestBufferCache's fill and
DMA-copy paths, which is what the image ownership tests cover. The declared-write
case needs a reconciliation after the submission, once the GPU-modified ranges
are known; that does not exist yet, so
SHARPEMU_SPECULATIVE_IMAGE_INVALIDATION=1 restores the old behaviour meanwhile.
Note: these two files also carried uncommitted work from earlier in this
worktree.
* shader: name the instruction behind an undefined descriptor dword
A descriptor dword that resolves to an undefined value fails the resource plan
with nothing to act on. Undefined nodes are interned per type, so one shared
node stands for every instruction the builder could not model, and the builder
has about twenty places that produce one: the message names a category where it
should name an opcode.
Record the instruction the builder is translating, and under
SHARPEMU_RESOURCE_TRACKER_DIAG intern undefined nodes per instruction as well.
Revisiting a block reaches the same instruction, so values stay as stable across
visits as before, and the rejection can walk the failing dword back to the
undefined leaves it rests on and name them. Without the diagnostic the interning
and the message are unchanged.
A first attempt recorded every undefined created while an instruction was in
flight, which listed 1410 instructions for one shader because many are created
legitimately, for SCC among others. Only the leaves the failing value actually
depends on are reported.
Test counts are unchanged at 614 passed and 95 pre-existing failures.
* shader: carry over the Gen5 translation work from the port
Pre-existing work in this worktree, recorded here rather than authored in this
session: VOP3B carry/borrow paths for VSubCoCiU32 and VSubrevCoCiU32, the high
opcode bit for MIMG and the ray-tracing names it selects, DS/GDS flags, the
wave32 EXEC mask, and the scalar value graph and runtime evaluator changes that
go with them, across the Vulkan and Metal back ends.
* gpu: carry over the AGC and VideoOut work from the port
Pre-existing work in this worktree, recorded here rather than authored in this
session: tracked image caching in place of unused snapshots, depth/stencil
handling, snapshot reuse for feedback loops, read-only depth, the Vulkan
feedback target resolver, the GuestDepth and draw recording paths, GPU barriers
and image cache tracking, the storage resolver, BDA and GPU memory tracking in
VideoOut, and the submission and indirect dispatch corrections.
* hle: carry over the library export work from the port
Pre-existing work in this worktree, recorded here rather than authored in this
session: an HTTP connect timeout, an offline MAC, NP signalling context, the
asynchronous NP stubs and entitlement key, NP WebAPI context deletion, a CFW
stub, and the added aliases for sceImeKeyboardGetInfo, unlink, sched_yield,
scePadResetOrientation, scePadIsRemoteController and the solo scheduling
attributes, along with the Ampr file registry and kernel compatibility exports.
* cpu: carry over the guest execution and memory work from the port
Pre-existing work in this worktree, recorded here rather than authored in this
session: the guest stack size the pthread attribute asks for and its cap, the
PthreadYield fallback, the multi-thread stall watchdog, import and handoff
instrumentation, the host view and guest space owner changes, and the physical
virtual memory and runtime adjustments that go with them.
* base: verified pre-rebase gameplay baseline
* fix: materialize indirect shader image resources
* gpu,shader: Yotei bring-up changes (GPT session, unreviewed)
Work from a separate GPT session aimed at Ghost of Yotei, committed as-is so
it can be reviewed block by block. It clearly improves Demon's Souls rendering
(the scene becomes visible), but review found problems to fix before a PR:
- S_BITREPLICATE_B64_B32 copies the dword into both halves; the ISA doubles
each bit (bit i -> bits 2i, 2i+1). Wrong in the translator and the graph.
- FindMatchingMipLevel no longer matches 2D array views of 3D volumes; the
test expectation was flipped from 0 to -1 instead of keeping the case.
- VectorCompareResourceTests lost five passing tests, replaced by
compile-only smoke tests.
- 11_11_10 UNorm is mapped to a packed-float Vulkan format.
- Unknown BufferLoadFormat descriptors now read zeros / null images silently.
- Undefined scalar values are interned per instruction for every title.
Also adds 16-bit integer VOPC compares, V_RSQ_F16, IMAGE_GATHER4_C_LZ,
IMAGE_SAMPLE_A decode, Yotei base entitlements, richer image-creation
failure messages and flip-source diagnostics.
* diagnostics(renderdoc): capture automatically after N guest flips
SHARPEMU_RENDERDOC_CAPTURE_AFTER_FLIPS=N requests one capture once N guest
flip boundaries have been seen, so an unattended run can capture without the
F12 key. The count is kept here because the flip image version is per display
buffer, not a global frame index.
* fix(import-guard): recognize polling progress boundaries (#895)
* port shader GDS byte reads and float atomics
* port kernel debug exception exports
* Revert "port kernel debug exception exports"
This reverts commit 71d605e82c82a4a921862bfc865a104305cf746d.
* Revert "port shader GDS byte reads and float atomics"
This reverts commit 8f4e7664d7fc81a28ba5c47c86cbc03dba61d47b.
* [usbd] implement event timeout
* [usbd] add initialization stubs
* [share] add feature and overlay exports
* [kernel] implement pthread cond signalto
* [font] add renderer and kerning exports
* [agc] implement ACB rewind exports
* [share] add running status export
* [stubs] add game service exports
* [kernel] implement high resolution timer events
* [gpu] support polygon primitive rendering
* chore: bump version to 0.0.4
* [kernel] implement debug exception exports
* [vulkan] adopt synchronization2 submission and barriers
* docs: update upstream README
* Revert "[vulkan] adopt synchronization2 submission and barriers"
This reverts commit 7e44f90d624c05ccac3ba8d14f94bfa63c225b1d.
* Reapply "[vulkan] adopt synchronization2 submission and barriers"
This reverts commit 66b456dd2a4562b081970404034ab3cc3135f66a.
* Revert "[kernel] implement high resolution timer events"
This reverts commit faf105d9a0f93120f9954ff30890701683d79a67.
* Reapply "[kernel] implement high resolution timer events"
This reverts commit 9858506307cfdc43e875180f21727bea936ac888.
* Revert "[kernel] implement pthread cond signalto"
This reverts commit 83ad3b0ac313b01c7d1493e685423a5a419b8766.
* Reapply "[kernel] implement pthread cond signalto"
This reverts commit 4bc31dabf7885687dc3777aefede6b4122f936a4.
* [ampr] fall back to executable-local index cache
* [ampr] probe cache directory writability
* [media] keep host movies progressing when Vulkan bindings lag
* [media] release stalled host movies independently of rendering
* [vulkan] reuse pipeline caches across emulator builds
* [media] propagate stalled movie EOF to the guest
* [ampr] return guest path ids from resolve exports
* Revert "[vulkan] reuse pipeline caches across emulator builds"
This reverts commit f0f8e21382b402b510645acb66cf122ad765cd4a.
* [vulkan] reuse pipeline caches across emulator builds
* Revert "[vulkan] reuse pipeline caches across emulator builds"
This reverts commit fd667b6320b74be1e5d71b37a3c29fc11af713bb.
* fix(media): honor full Bink movie duration
* fix(vulkan): schedule wave64 barriers uniformly
* fix(vulkan): emit wave64 loop merge before branch
* fix(vulkan): predicate wave64 image stores
* Revert "fix(vulkan): predicate wave64 image stores"
This reverts commit 5842298743822d1b128e8b4dcb3f9d0a8fd03c35.
* Revert "fix(vulkan): emit wave64 loop merge before branch"
This reverts commit d2b98e0a8439e41c8faf04335320f202740fe7f9.
* Revert "fix(vulkan): schedule wave64 barriers uniformly"
This reverts commit 976d5a5957080de6568417c9d12235570b1503ef.
* baseline: preserve good Demon's Souls rendering
* Revert "[vulkan] adopt synchronization2 submission and barriers"
This reverts commit 7e44f90d624c05ccac3ba8d14f94bfa63c225b1d.
* fix(ampr): keep APR resolve ids unique on path-hash collisions
The APR resolve exports now return the 31-bit FNV hash of the guest path
(upstream 5c347d2c). Demon's Souls ships 13 same-hash file pairs under
/app0/ alone, and it resolves 32 colliding paths before the main menu.
Colliding files shared one id and read each other's bytes, and the
first level load after the menu never finished.
Hand out the hash only while it names one host file, keep it in an
authoritative table that later alias publishes cannot poison, and fall
back to a collision-safe Register() handle otherwise. Log both the
fallback and reads of unknown ids, which previously failed silently.
* fix(media): size the Bink watchdog to the movie, restore Gen4 export
c77746bd reverted the duration-based Bink watchdog to a fixed 90 s, and
MediaFramePlayback also capped every movie (AvPlayer too) at 120 s.
Demon's Souls ships a 171 s intro, 117-138 s attract movies and 756 s
credits, which both bounds cut short. Arm the watchdog for the movie
length plus 60 s (minimum 90 s, no upper bound) and drop the fixed
playback cap, as in the pre-rebase baseline.
Also keep cfwBSQyr5Ys exported for Gen4 as the baseline did.
* chore: trim added comments, keep upstream's cfwBSQyr5Ys target
Restore upstream's APR ABI comment verbatim, keep each added comment to
one line, and leave sceKernelDebugWriteCppExceptionInfo Gen5-only as
upstream declares it.
* fix(vulkan): query image levels only when the result is read
ImageGetResinfo emitted OpImageQueryLevels for every sampled image even
when the dmask never reads component 3. On a 3D image, the unused query
crashes NVIDIA's pipeline compiler inside vkCreateComputePipelines:
Demon's Souls compute shader 0xC553F37E46481A6B (dmask 3) takes the
process down right after the first frame.
* fix(shader): reconcile branch resource planning with upstream contracts
- Keep upstream's 42-binding image ABI. A float min/max image atomic
binds as the uint atomic image its compare-exchange loop already uses.
- A null-page read turns into zero only after a null base pointer fails
to read, not for every address below 0x10000.
- An empty V# reads as zero; overrunning a bound buffer stays a failure.
- A wave-indexed image table reads its mask through the clean reader,
like every other slot its descriptor depends on.
- A buffer load through a lane-varying address stays undefined, and a
constant exec_lo write keeps s_cbranch_execz decidable.
- An image descriptor from a scalar buffer must be one contiguous
record, and a vector access through a draw-evaluable V# keeps its
explicit binding instead of the runtime-descriptor path.
* fix(shader): drop the null-page read rule, encode GDS at bit 17 in tests
KytyPS5 does not turn a null SRT pointer read into zero: an unmapped
read fails materialization, which FailedResourceReadsRemainFatal pins.
Remove the rule.
GFX10 DS encodes GDS at bit 17 with OP at 25:18; bit 16 is GFX9's
position (LLVM DS_Real_gfx10). The decoder already reads bit 17, so the
DataShare tests set GDS there, and the LDS wave-counter tests clear it.
* fix(vulkan): keep bind-time image invalidation coherent with GPU contents
Binding a formatted writable buffer over an image marks the image
buffer-modified, so its next use refreshes it from the buffer. The
buffer was only synchronized from the image for read-only views, so
over a render target a writable view refreshed the image from stale
guest bytes. That is the Demon's Souls banding 48cd723a worked around
by skipping the invalidation altogether.
Synchronize writable formatted views from the image as well, and at bind
time invalidate every covered image except one whose only current copy
is still on the GPU and could not be copied into the buffer. Drop the
SHARPEMU_SPECULATIVE_IMAGE_INVALIDATION switch.
* test(shader): expect the header immediate in the indirect selector offset
The materializer reads the selector word at selector * stride +
SelectorOffset, so the offset must include the material read's
immediate. With the header immediate 4 and the +4 in the address
arithmetic it is 8; the test expected 4 since it was written.
* chore: remove session diagnostics
Drop the ad-hoc instrumentation added while bringing Demon's Souls and
Ghost of Yotei up:
- SHARPEMU_RESOURCE_TRACKER_DIAG traces in the resource tracker, dense
image tables, runtime evaluator and validator
- unconditional materialize_snapshot_probe warnings and the
SHARPEMU_TRACE_INDIRECT_IMAGES wave-table trace
- the guest fault census (SHARPEMU_FAULT_CENSUS / SHARPEMU_FAULT_WATCH)
- red-zone patcher address trace, unprotected-site file and short-jump
estimate (SHARPEMU_REDZONE_TRACE / SHARPEMU_REDZONE_SITES_FILE)
- RenderDoc capture after N flips and the flip-source refresh traces
The red-zone coverage counters and the readable access-violation report
stay.
* fix(kernel): sparse virtual reservations and HLE compatibility fixes
- sceKernelReserveVirtualRange: keep very large non-fixed reservations
in the guest mapping table without one contiguous host placeholder.
- Event flags: read names and write handles through the kernel
compatibility helpers so read-only image pages work.
- sceAjmInitialize: only validate the 32-bit reserved argument.
- Pthread: only free opaque objects the compatibility layer allocated.
- Route libc memset to the HLE implementation (int 45h path in LLE).
* fix(gpu): tiling, color targets, VideoOut pipelines and primitive stage masks
- Tile geometry and tiler shader fixes for detiling.
- Color target request fixes for 3D slice ranges.
- VideoOut descriptor, draw recording and pipeline fixes.
- Accept the compact primitive-shader stage masks (0x2000, 0x2030)
and report the rejected state when draws are skipped.
* fix(shader): Little Nightmares III decoding and translation
- Decode S_SCRATCH_STORE_DWORDX2 (SMRD op 0x16) as a scalar scratch
store instead of a load.
- Raise the decoder instruction limit to the maximum shader size.
- Stop linear decoding at S_SETPC_B64 and treat the tail as
unreachable; find fused GS pairs across the full 1 MiB object and
resolve continuation image descriptors from its entry SGPRs.
- Translate S_SETPC_B64 in linearised fused programs as a marker.
- Implement S_MUL_HI_I32 and V_DOT2C_F32_F16.
* fix(shader): Astro Bot debugger stubs and wave32 primitive pipelines
- S_CBRANCH_CDBG* only fire under an attached GPU debugger; do not let
them extend the decoded program past S_ENDPGM into debugger stubs
that load descriptors from devkit-only addresses.
- Stop decoding after an unconditional backward S_BRANCH with no
pending forward target instead of reading the "sl00" metadata
trailer as instructions.
- Ignore the GS_W32_EN/VS_W32_EN wave-size bits when matching
primitive-shader stage masks; graphics stages always compile as
wave32, so these draws were skipped for no reason.
* fix(kernel): keep sparse reservations out of the macOS graphics window
macOS reserves 0x10_0000_0000..0x70_0000_0000 for graphics memory, so a
sparse reservation starting at 0x40_0000_0000 could not be mapped there
(LargeVirtualReservationFallsBackToSparseGuestRange failed on osx-x64).
Start the sparse aperture at 0x70_0000_0000 on macOS.
* fix(shader): declare guest 1D textures as 1D images
Since #903 the image request builder binds guest 1D textures through
1D Vulkan views, but the SPIR-V translator still declared them as 2D.
Sampling a 1D view through a 2D image type is invalid, and on NVIDIA
Astro Bot's final composite pass wrote black to the flip buffer.
Declare Dim1D/Dim1DArray as SPIR-V 1D images with Sampled1D/Image1D,
matching upstream.
* revert(gpu): fail on written device-address ranges that cannot be planned
The Little Nightmares III work skipped unplannable written ranges on the
host and allowed those stores unconditionally in the shader. Stores whose
pages were not already in the BDA page table were then dropped silently.
Restore upstream's fatal and remove UnplannableWrittenMemoryIndices, so
each such shader is reported and fixed on its own.
* fix(shader): route FLAT accesses through the LDS and scratch apertures
GFX10 sends a FLAT access to LDS or scratch when bits 63:48 of its
address match the shared or private aperture (SH_MEM_BASES), and to
global memory otherwise. src_shared_base/src_private_base decoded to 0,
so an LDS pointer became a device address the host could not plan
(Little Nightmares traversal stack, shader 0x9669394374AAE5BC).
- Give the apertures distinct high dwords no guest address can reach,
consistent for 32-bit (high dword) and 64-bit (address) reads.
- Keep aperture identity in the scalar value graph (MemoryAperture).
- The device-address planner classifies a FLAT access whose high dword
derives from an aperture and gives it no device range; plain device
pointers keep the range and the fatal.
- The SPIR-V backend routes each lane by bits 63:48 to LDS, scratch or
the page-table global path.
* fix(shader): use the PS5 apertures and prove clamped FLAT addresses local
Little Nightmares builds its BVH traversal-stack addresses without
src_shared_base: v_med3_u32(0x80000000, 0x70000000, sp) clamps the high
dword, and the pop path reads the 0x7xxxxxxx entries with ds_read and
the 0x8xxxxxxx ones with scratch_load. These are the PS5 shared and
private apertures, so:
- SharedApertureHigh = 0x70000000, PrivateApertureHigh = 0x80000000;
the backend routes a FLAT lane by the top nibble of its high dword.
- Model v_med3_u32/v_med3_i32 in the scalar value graph.
- The planner bounds the high dword (Phi/Select/UMin/UMax, cycles
ignored since each yields one of its inputs) and classifies it local
when the bounds stay inside the apertures.
- Record the EXEC mask of each global/flat access: a select on that
same mask is a lane-masked register write whose inactive arm never
reaches a lane that accesses memory.
Little Nightmares shaders 0x9669..., 0xE9F1... and 0x392B... now plan
without the removed unplanned-write skip.
* feat(shader): emulate the GFX10 BVH node test for image_bvh_intersect_ray
Follows AMD GPURT's RT IP 1.1 fallback: triangle nodes 0/1 with barycentric
swizzle, FP16/FP32 box slab test with grow, NaN rule and sorting network.
Nodes are read through the device-address page table.
* fix(gpu): move a depth surface sampled as color into a color image
A texture whose format reads neither the depth nor the stencil plane
reinterprets the memory; Little Nightmares samples a D32S8 surface as BGRA8.
* fix(shader): make s_barrier order buffer and image memory within the workgroup
Guest code publishes stores to its workgroup with s_waitcnt vmcnt(0) +
s_barrier; the host barrier only ordered shared memory.
* fix(gpu): match sampler border color and filtering to the sampled view
Border colors were always integer, which Vulkan leaves undefined on float
views (an opaque-white border can read back as black). The numeric class
now comes from the bound view formats; integer views also drop filtering.
* fix(shader): decode the VOP1 and VOP3 half-precision transcendentals
Silent Hill's pixel shaders use v_log_f16; add rcp, sqrt, log, exp,
floor, ceil, trunc, rndne, fract, sin and cos for f16 in both encodings.
* fix(shader): decode v_min3_f16, v_max3_f16 and v_med3_f16
Silent Hill's pixel shaders use v_min3_f16; the draw was skipped.
* fix(agc): give a fused shader the larger user SGPR count of its halves
sceAgcFuseShaderHalves kept the back half's SPI_SHADER_PGM_RSRC2_GS, so a
front half reading its vertex tables from user SGPRs 26-29 saw only 12
(Silent Hill: front 30, back 12) and the draw faulted on null tables.
A vertex program without input semantics no longer needs the tables, and
the null-table and PRT-mapping messages name the registers involved.
* fix(shader): read buffers past the binding budget through their device descriptors
A Silent Hill compute shader binds more than 32 distinct buffers. Past the
budget an untyped access reads its V# from its SGPRs at run time, as the
hardware does, instead of failing the whole shader.
* fix(gpu): treat a compute constant fill of DCC metadata as a fast clear
Unreal fast-clears DCC render targets with a compute shader that stores one
dword from a constant buffer into every metadata record (0x40404040 clears
to 0,0,0,1). The fill only reached plain memory, so Silent Hill's
translucency layer stayed 0 and its composite multiplied the scene by 0.
The exact instruction sequence is recognised and routed to the existing
DCC clear tracking.
* fix(gpu): invalidate every image under a bound writable formatted buffer
The bound-write path skipped images whose only current copy was on the GPU.
Such an image kept stale contents after the guest rewrote it through the
buffer: Dead Cells lost its background and Demon's Souls showed wrong
surfaces. Bisected to ed3de27b; upstream invalidates unconditionally, and
52fbe275 already synchronizes writable views from the image, which fixed the
banding the exception was meant to hide.
---------
Co-authored-by: ParantezTech <12572227+par274@users.noreply.github.com>
Co-authored-by: Mathias <06mathias6@gmail.com>
This commit is contained in:
parent
ab0e77e5d6
commit
e17fb6fed1
|
|
@ -365,7 +365,13 @@ public sealed partial class DirectExecutionBackend
|
|||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
|
||||
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
||||
byte[] code = new byte[16];
|
||||
if (TryReadHostBytes(rip, code))
|
||||
DumpHostRegion("RIP", rip);
|
||||
// Guest code can be mapped execute-only, which makes every
|
||||
// host-side read of it fail. The guest memory view still
|
||||
// resolves those pages, so prefer it here: otherwise the
|
||||
// whole call-site report below is silently skipped on
|
||||
// exactly the faults that need it.
|
||||
if (TryReadGuestOrHostBytes(rip, code))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
||||
if (code[0] == 100)
|
||||
|
|
@ -383,12 +389,12 @@ public sealed partial class DirectExecutionBackend
|
|||
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
|
||||
}
|
||||
byte[] before = new byte[16];
|
||||
if (rip > 16 && TryReadHostBytes(rip - 16, before))
|
||||
if (rip > 16 && TryReadGuestOrHostBytes(rip - 16, before))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
||||
}
|
||||
byte[] window = new byte[64];
|
||||
if (rip > 32 && TryReadHostBytes(rip - 32, window))
|
||||
if (rip > 32 && TryReadGuestOrHostBytes(rip - 32, window))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||
}
|
||||
|
|
@ -405,7 +411,7 @@ public sealed partial class DirectExecutionBackend
|
|||
continue;
|
||||
}
|
||||
byte[] callSiteWindow = new byte[48];
|
||||
if (TryReadHostBytes(candidate - 24, callSiteWindow))
|
||||
if (TryReadGuestOrHostBytes(candidate - 24, callSiteWindow))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
|
||||
|
|
@ -417,6 +423,11 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||
}
|
||||
// The faulting instruction is the first thing an access
|
||||
// violation report has to answer, so decode it through guest
|
||||
// memory as well: that path still works when the host code
|
||||
// page refuses a plain read.
|
||||
DumpGuestInstructionStream("fault", rip, 4);
|
||||
DumpRecentImportTrace();
|
||||
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||
DumpGuestRegisterWindowDiagnostics(
|
||||
|
|
@ -1463,6 +1474,44 @@ public sealed partial class DirectExecutionBackend
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads guest bytes for a crash report, preferring the guest memory view
|
||||
/// over a raw host read. Guest code pages are commonly committed
|
||||
/// PAGE_EXECUTE, which no host read can satisfy.
|
||||
/// </summary>
|
||||
private bool TryReadGuestOrHostBytes(ulong address, byte[] buffer)
|
||||
{
|
||||
if (_cpuContext is { } context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context.Memory.TryRead(address, buffer))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to the host reader.
|
||||
}
|
||||
}
|
||||
|
||||
return TryReadHostBytes(address, buffer);
|
||||
}
|
||||
|
||||
private unsafe static void DumpHostRegion(string name, ulong address)
|
||||
{
|
||||
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] {name} region: <VirtualQuery failed> address=0x{address:X16}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] {name} region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} " +
|
||||
$"state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08} type=0x{mbi.Type:X08}");
|
||||
}
|
||||
|
||||
private unsafe static bool IsReadableHostRange(ulong address, int byteCount, bool allowExecuteOnly = false)
|
||||
{
|
||||
if (byteCount < 0 || !IsCanonicalUserAddress(address))
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
private static readonly ulong GuestThreadTlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||
|
||||
private const ulong GuestThreadStackSize = 0x0020_0000UL;
|
||||
private const ulong GuestThreadStackHeadroom = 0x0010_0000UL;
|
||||
private const ulong GuestThreadStackSizeLimit = 0x0400_0000UL;
|
||||
|
||||
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
|
||||
|
||||
|
|
@ -285,7 +287,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
private readonly List<nint> _importHandlerTrampolines = new List<nint>();
|
||||
|
||||
private const int GuestContextTransferFrameQwords = 20;
|
||||
private const int GuestContextTransferFrameQwords = 22;
|
||||
|
||||
private readonly object _guestContextTransferStubGate = new();
|
||||
|
||||
|
|
@ -1833,7 +1835,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
private static bool IsHlePreferredNid(string nid)
|
||||
{
|
||||
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
||||
// The shipped libc memset body contains a PS5-specific int 45h path
|
||||
// which is not a Windows host instruction. Use the bounds-aware HLE
|
||||
// implementation instead of redirecting this import to LLE.
|
||||
return string.Equals(nid, "8zTFvBIAIN8", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
|
@ -2140,6 +2146,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
error = $"guest context transfer slot is not writable at 0x{target.Rsp - sizeof(ulong):X16}";
|
||||
return false;
|
||||
}
|
||||
ulong stackBottom = 0;
|
||||
ulong stackTop = 0;
|
||||
if (OperatingSystem.IsWindows() && !TryGetGuestStackBounds(target.Rsp, out stackBottom, out stackTop))
|
||||
{
|
||||
error = $"guest context transfer target rsp=0x{target.Rsp:X16} has no committed host stack mapping";
|
||||
return false;
|
||||
}
|
||||
|
||||
transferStub = GetOrCreateGuestContextTransferStub();
|
||||
if (transferStub == 0)
|
||||
|
|
@ -2176,6 +2189,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
frame[17] = target.Mxcsr == 0 ? 0x1F80u : target.Mxcsr;
|
||||
frame[18] = target.FpuControlWord == 0 ? 0x037Fu : target.FpuControlWord;
|
||||
frame[19] = target.RestoreFullFpuState ? 1u : 0u;
|
||||
// The TEB stores NT_TIB.StackBase (the high address) at +0x08 and
|
||||
// NT_TIB.StackLimit (the low address) at +0x10. Keep the frame in that
|
||||
// same order so the transfer stub can store the slots straight through.
|
||||
frame[20] = stackTop;
|
||||
frame[21] = stackBottom;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2243,6 +2261,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
|
||||
Emit(0x49); Emit(0x89); Emit(0xC3); // mov r11, rax
|
||||
// Fiber switches replace the guest stack without passing through a
|
||||
// guest-entry stub, so keep the Windows TEB bounds in sync here.
|
||||
EmitLoadFromR11Disp32(10, 160); // r10 = target stack top
|
||||
EmitStackBound(code, ref offset, 10, 8, store: true);
|
||||
EmitLoadFromR11Disp32(10, 168); // r10 = target stack bottom
|
||||
EmitStackBound(code, ref offset, 10, 16, store: true);
|
||||
// A new >=3.50 fiber receives the SDK-defined MXCSR verbatim. A
|
||||
// resumed fiber follows _sceFiberLongJmp: preserve status bits 0-5
|
||||
// while restoring the saved control bits.
|
||||
|
|
@ -5342,7 +5366,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
error = "creator context memory is not backed by IVirtualMemory";
|
||||
return false;
|
||||
}
|
||||
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, GuestThreadStackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
|
||||
var requestedStackSize = request.StackSize;
|
||||
if (requestedStackSize > GuestThreadStackSizeLimit)
|
||||
{
|
||||
requestedStackSize = GuestThreadStackSizeLimit;
|
||||
}
|
||||
|
||||
var stackSize = requestedStackSize == 0
|
||||
? GuestThreadStackSize
|
||||
: AlignUp(requestedStackSize + GuestThreadStackHeadroom, 0x1000UL);
|
||||
if (stackSize < GuestThreadStackSize)
|
||||
{
|
||||
stackSize = GuestThreadStackSize;
|
||||
}
|
||||
|
||||
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, stackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -5359,7 +5397,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
FsBase = tlsBase,
|
||||
GsBase = tlsBase,
|
||||
};
|
||||
context[CpuRegister.Rsp] = stackBase + GuestThreadStackSize - sizeof(ulong);
|
||||
context[CpuRegister.Rsp] = stackBase + stackSize - sizeof(ulong);
|
||||
context[CpuRegister.Rdi] = request.Argument;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
|
|
@ -5382,7 +5420,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
AffinityMask = request.AffinityMask,
|
||||
Context = context,
|
||||
StackBase = stackBase,
|
||||
StackSize = GuestThreadStackSize,
|
||||
StackSize = stackSize,
|
||||
State = GuestThreadRunState.Ready,
|
||||
};
|
||||
error = null;
|
||||
|
|
@ -6774,6 +6812,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
MarkExecutionProgress();
|
||||
continue;
|
||||
}
|
||||
if (AreAllLiveGuestThreadsExpectedlyBlocked())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s, but every live guest thread is parked in an expected blocking primitive; continuing.");
|
||||
LogStallWatchdogSnapshot();
|
||||
Console.Error.Flush();
|
||||
MarkExecutionProgress();
|
||||
continue;
|
||||
}
|
||||
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -6841,6 +6888,41 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool AreAllLiveGuestThreadsExpectedlyBlocked()
|
||||
{
|
||||
using (LockGate("AreAllLiveGuestThreadsExpectedlyBlocked"))
|
||||
{
|
||||
var sawBlockedThread = false;
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (thread.State != GuestThreadRunState.Blocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref thread.LastImportNid) is not (
|
||||
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||
"WKAXJ4XBPQ4" or // scePthreadCondWait
|
||||
"BmMjYxmew1w" or // scePthreadCondTimedwait
|
||||
"fzyMKs9kim0" or // sceKernelWaitEqueue
|
||||
"Zxa0VhQVTsk")) // sceKernelWaitSema
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sawBlockedThread = true;
|
||||
}
|
||||
|
||||
return sawBlockedThread;
|
||||
}
|
||||
}
|
||||
|
||||
private void StopStallWatchdog()
|
||||
{
|
||||
_stallWatchdogStop = true;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ internal static class GuestRedZonePatcher
|
|||
private const ulong AllocationAlignment = 0x10000;
|
||||
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
|
||||
|
||||
internal enum SpanRefusal
|
||||
{
|
||||
None,
|
||||
ControlFlow,
|
||||
BranchTargetAfter,
|
||||
StackAfter,
|
||||
TooShort,
|
||||
}
|
||||
|
||||
public static PatchResult Patch(
|
||||
IVirtualMemory memory,
|
||||
PhysicalVirtualMemory physicalMemory,
|
||||
|
|
@ -58,7 +67,8 @@ internal static class GuestRedZonePatcher
|
|||
if (sites.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] {hostName} red-zone scan: functions={scan.Functions} red_zone={scan.RedZoneFunctions} sites=0.");
|
||||
$"[LOADER] {hostName} red-zone scan: functions={scan.Functions} red_zone={scan.RedZoneFunctions} " +
|
||||
$"sites=0 unrelocatable={scan.UnrelocatableSites}.");
|
||||
return scan;
|
||||
}
|
||||
|
||||
|
|
@ -83,14 +93,28 @@ internal static class GuestRedZonePatcher
|
|||
var trampolineEnd = trampolineBase + requiredBytes;
|
||||
var patched = 0;
|
||||
var failed = 0;
|
||||
var previousEnd = 0UL;
|
||||
foreach (var site in sites)
|
||||
{
|
||||
// Sites are produced in increasing address order. Patching two that
|
||||
// overlap would write one jump over another and send the trampoline
|
||||
// into whatever follows, so refuse rather than corrupt guest code.
|
||||
if (site.Address < previousEnd)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] {hostName} red-zone site 0x{site.Address:X16} overlaps the previous one " +
|
||||
$"ending at 0x{previousEnd:X16}; left unpatched.");
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryPatchSite(memory, site, ref trampolineCursor, trampolineEnd, splitVectorStores, rewriteSha))
|
||||
{
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
previousEnd = site.Address + (ulong)site.ByteLength;
|
||||
patched++;
|
||||
}
|
||||
|
||||
|
|
@ -99,11 +123,19 @@ internal static class GuestRedZonePatcher
|
|||
CandidateSites = sites.Count,
|
||||
PatchedSites = patched,
|
||||
FailedSites = failed,
|
||||
UnrelocatableSites = scan.UnrelocatableSites,
|
||||
ControlFlowRefusals = scan.ControlFlowRefusals,
|
||||
BranchTargetRefusals = scan.BranchTargetRefusals,
|
||||
StackRefusals = scan.StackRefusals,
|
||||
TooShortRefusals = scan.TooShortRefusals,
|
||||
TrampolineBytes = trampolineCursor - trampolineBase,
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] {hostName} red-zone patch: functions={result.Functions} red_zone={result.RedZoneFunctions} " +
|
||||
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
|
||||
$"unrelocatable={result.UnrelocatableSites} " +
|
||||
$"(control_flow={result.ControlFlowRefusals} branch_target_after={result.BranchTargetRefusals} " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
|
||||
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
|
||||
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
|
||||
return result;
|
||||
|
|
@ -124,6 +156,8 @@ internal static class GuestRedZonePatcher
|
|||
var redZoneFunctionCount = 0;
|
||||
var instructionCount = 0;
|
||||
var vectorStoreCount = 0;
|
||||
var unrelocatableSites = 0;
|
||||
var refusalCounts = new int[5];
|
||||
var shaInstructionCount = 0;
|
||||
|
||||
foreach (var header in programHeaders)
|
||||
|
|
@ -174,6 +208,7 @@ internal static class GuestRedZonePatcher
|
|||
functionCount++;
|
||||
instructionCount += decoded.Count;
|
||||
var usesRedZone = protectRedZone && decoded.Any(static entry => UsesRedZone(entry.Instruction));
|
||||
|
||||
if (!usesRedZone && !splitVectorStores && !rewriteSha)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -184,19 +219,43 @@ internal static class GuestRedZonePatcher
|
|||
redZoneFunctionCount++;
|
||||
}
|
||||
var branchTargets = CollectBranchTargets(decoded);
|
||||
|
||||
var lastSiteEnd = 0UL;
|
||||
|
||||
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
|
||||
{
|
||||
var instruction = decoded[instructionIndex].Instruction;
|
||||
var isShaSite = rewriteSha && ShaInstructionRewrite.CanRewrite(instruction);
|
||||
if ((!isShaSite &&
|
||||
((!usesRedZone && !(splitVectorStores && RosettaVectorStorePatch.RequiresStoreSplit(instruction))) ||
|
||||
!IsFaultableGuestMemoryInstruction(instruction))) ||
|
||||
!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span))
|
||||
if (!isShaSite &&
|
||||
((!usesRedZone && !(splitVectorStores && RosettaVectorStorePatch.RequiresStoreSplit(instruction))) ||
|
||||
!IsFaultableGuestMemoryInstruction(instruction)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// A backward span reaches instructions that an earlier site
|
||||
// may already have replaced with its own jump. Two jumps
|
||||
// written over each other corrupt control flow, so a span is
|
||||
// only usable when it starts at or after the end of the last
|
||||
// one. Forward spans cannot overlap by construction; the
|
||||
// check costs nothing and documents the invariant.
|
||||
var backward = false;
|
||||
if (!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span, out var refusal))
|
||||
{
|
||||
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
}
|
||||
|
||||
if ((!backward && refusal != SpanRefusal.None) || span.Address < lastSiteEnd)
|
||||
{
|
||||
// Refused spans never enter the site list, so they are
|
||||
// invisible in FailedSites. Count them separately.
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
sites.Add(span);
|
||||
lastSiteEnd = span.Address + (ulong)span.ByteLength;
|
||||
if (splitVectorStores)
|
||||
{
|
||||
vectorStoreCount += span.Instructions.Count(static instruction => RosettaVectorStorePatch.RequiresStoreSplit(instruction));
|
||||
|
|
@ -205,7 +264,13 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
|
||||
}
|
||||
instructionIndex += span.Instructions.Count - 1;
|
||||
|
||||
// A backward span already ends at the current instruction, so
|
||||
// only a forward one consumes the instructions that follow.
|
||||
if (!backward)
|
||||
{
|
||||
instructionIndex += span.Instructions.Count - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -217,6 +282,11 @@ internal static class GuestRedZonePatcher
|
|||
Instructions = instructionCount,
|
||||
CandidateSites = sites.Count,
|
||||
VectorStoreCount = vectorStoreCount,
|
||||
UnrelocatableSites = unrelocatableSites,
|
||||
ControlFlowRefusals = refusalCounts[(int)SpanRefusal.ControlFlow],
|
||||
BranchTargetRefusals = refusalCounts[(int)SpanRefusal.BranchTargetAfter],
|
||||
StackRefusals = refusalCounts[(int)SpanRefusal.StackAfter],
|
||||
TooShortRefusals = refusalCounts[(int)SpanRefusal.TooShort],
|
||||
ShaInstructionCount = shaInstructionCount,
|
||||
};
|
||||
return sites;
|
||||
|
|
@ -274,20 +344,199 @@ internal static class GuestRedZonePatcher
|
|||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the enclosing-span search over a raw instruction range, so the
|
||||
/// behaviour can be pinned on the byte sequences that occur in real guest
|
||||
/// code rather than on a mocked decode.
|
||||
/// </summary>
|
||||
internal static bool TryBuildEnclosingSpan(
|
||||
byte[] code,
|
||||
ulong baseAddress,
|
||||
ulong faultAddress,
|
||||
out ulong spanAddress,
|
||||
out int spanLength,
|
||||
out int coreStart,
|
||||
out int coreCount)
|
||||
{
|
||||
spanAddress = 0;
|
||||
spanLength = 0;
|
||||
coreStart = 0;
|
||||
coreCount = 0;
|
||||
|
||||
var decoded = DecodeFunction(code, baseAddress, baseAddress, baseAddress + (ulong)code.Length);
|
||||
var faultIndex = -1;
|
||||
for (var index = 0; index < decoded.Count; index++)
|
||||
{
|
||||
if (decoded[index].Instruction.IP == faultAddress)
|
||||
{
|
||||
faultIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (faultIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
spanAddress = site.Address;
|
||||
spanLength = site.ByteLength;
|
||||
coreStart = site.CoreStart;
|
||||
coreCount = site.CoreCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the faultable instructions of a span. The RSP shift has to bracket
|
||||
/// all of them and nothing that reads or writes through RSP, because the
|
||||
/// shift would move such an access by 128 bytes.
|
||||
/// </summary>
|
||||
private static bool TryComputeShiftedCore(IList<Instruction> instructions, out int coreStart, out int coreCount)
|
||||
{
|
||||
coreStart = -1;
|
||||
var coreEnd = -1;
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
if (!IsFaultableGuestMemoryInstruction(instructions[index]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (coreStart < 0)
|
||||
{
|
||||
coreStart = index;
|
||||
}
|
||||
|
||||
coreEnd = index;
|
||||
}
|
||||
|
||||
if (coreStart < 0)
|
||||
{
|
||||
coreCount = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = coreStart; index <= coreEnd; index++)
|
||||
{
|
||||
if (TouchesStackPointer(instructions[index]))
|
||||
{
|
||||
coreCount = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
coreCount = coreEnd - coreStart + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a span that <em>ends</em> at the faulting instruction, taking the
|
||||
/// bytes it needs from the instructions before it. The instruction after the
|
||||
/// site is never overwritten, so branches into it keep working - which is
|
||||
/// what the forward search cannot offer when that instruction is a branch
|
||||
/// target or touches RSP.
|
||||
/// </summary>
|
||||
private static bool TryBuildEnclosingPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int faultIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site)
|
||||
{
|
||||
site = default;
|
||||
var faulting = decoded[faultIndex].Instruction;
|
||||
if (faulting.FlowControl != FlowControl.Next || TouchesStackPointer(faulting))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Starting before the faulting instruction only works when nothing
|
||||
// branches directly to it: such a branch would land inside the new jump.
|
||||
if (branchTargets.Contains(faulting.IP))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var byteLength = faulting.Length;
|
||||
for (var startIndex = faultIndex - 1; startIndex >= 0; startIndex--)
|
||||
{
|
||||
var candidate = decoded[startIndex].Instruction;
|
||||
if (candidate.FlowControl != FlowControl.Next)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byteLength += candidate.Length;
|
||||
|
||||
// A branch target on the first instruction is fine: it lands on the
|
||||
// jump that replaces the span. One in the middle is not, so stop as
|
||||
// soon as the span is long enough rather than growing past it.
|
||||
if (byteLength < MinimumJumpBytes)
|
||||
{
|
||||
if (branchTargets.Contains(candidate.IP))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var instructions = new List<Instruction>(faultIndex - startIndex + 1);
|
||||
for (var index = startIndex; index <= faultIndex; index++)
|
||||
{
|
||||
instructions.Add(decoded[index].Instruction);
|
||||
}
|
||||
|
||||
if (!TryComputeShiftedCore(instructions, out var coreStart, out var coreCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
site = new PatchSite(candidate.IP, byteLength, instructions, coreStart, coreCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryBuildPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site)
|
||||
out PatchSite site) =>
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
|
||||
|
||||
private static bool TryBuildPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site,
|
||||
out SpanRefusal refusal)
|
||||
{
|
||||
refusal = SpanRefusal.None;
|
||||
var instructions = new List<Instruction>(3);
|
||||
var byteLength = 0;
|
||||
for (var index = startIndex; index < decoded.Count && byteLength < MinimumJumpBytes; index++)
|
||||
{
|
||||
var instruction = decoded[index].Instruction;
|
||||
if (instruction.FlowControl != FlowControl.Next ||
|
||||
(index != startIndex && branchTargets.Contains(instruction.IP)) ||
|
||||
TouchesStackPointer(instruction))
|
||||
if (instruction.FlowControl != FlowControl.Next)
|
||||
{
|
||||
refusal = SpanRefusal.ControlFlow;
|
||||
}
|
||||
else if (index != startIndex && branchTargets.Contains(instruction.IP))
|
||||
{
|
||||
refusal = SpanRefusal.BranchTargetAfter;
|
||||
}
|
||||
else if (TouchesStackPointer(instruction))
|
||||
{
|
||||
refusal = SpanRefusal.StackAfter;
|
||||
}
|
||||
|
||||
if (refusal != SpanRefusal.None)
|
||||
{
|
||||
site = default;
|
||||
return false;
|
||||
|
|
@ -299,11 +548,19 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
if (byteLength < MinimumJumpBytes)
|
||||
{
|
||||
refusal = SpanRefusal.TooShort;
|
||||
site = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
site = new PatchSite(decoded[startIndex].Instruction.IP, byteLength, instructions);
|
||||
if (!TryComputeShiftedCore(instructions, out var coreStart, out var coreCount))
|
||||
{
|
||||
refusal = SpanRefusal.TooShort;
|
||||
site = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
site = new PatchSite(decoded[startIndex].Instruction.IP, byteLength, instructions, coreStart, coreCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -385,6 +642,30 @@ internal static class GuestRedZonePatcher
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool TryEncodeSegment(
|
||||
IList<Instruction> instructions,
|
||||
int start,
|
||||
int count,
|
||||
ulong address,
|
||||
out byte[]? encoded)
|
||||
{
|
||||
encoded = null;
|
||||
if (count <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var writer = new ListCodeWriter();
|
||||
var segment = instructions.Skip(start).Take(count).ToList();
|
||||
if (!BlockEncoder.TryEncode(64, new InstructionBlock(writer, segment, address), out _, out _, BlockEncoderOptions.None))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
encoded = writer.ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryPatchSite(
|
||||
IVirtualMemory memory,
|
||||
PatchSite site,
|
||||
|
|
@ -393,9 +674,25 @@ internal static class GuestRedZonePatcher
|
|||
bool splitVectorStores,
|
||||
bool rewriteSha)
|
||||
{
|
||||
// A span that starts before the faulting instruction may relocate an
|
||||
// RSP-relative access, which must run before the shift or it would read
|
||||
// 128 bytes away. Emit such a span as prefix / shifted core / suffix.
|
||||
// A forward span starts at the faulting instruction and never steals a
|
||||
// stack-dependent one, so it keeps the original single-block shape.
|
||||
byte[]? prefix = null;
|
||||
if (site.CoreStart > 0 &&
|
||||
!TryEncodeSegment(site.Instructions, 0, site.CoreStart, trampolineCursor, out prefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prefixLength = prefix?.Length ?? 0;
|
||||
var writer = new ListCodeWriter();
|
||||
var relocatedAddress = trampolineCursor + 5;
|
||||
var instructions = splitVectorStores ? RosettaVectorStorePatch.SplitVectorStores(site.Instructions) : site.Instructions;
|
||||
var relocatedAddress = trampolineCursor + (ulong)prefixLength + 5;
|
||||
var core = prefix is null
|
||||
? site.Instructions
|
||||
: site.Instructions.Skip(site.CoreStart).Take(site.CoreCount).ToList();
|
||||
var instructions = splitVectorStores ? RosettaVectorStorePatch.SplitVectorStores(core) : core;
|
||||
if (rewriteSha)
|
||||
{
|
||||
instructions = ShaInstructionRewrite.Expand(instructions);
|
||||
|
|
@ -407,21 +704,22 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
|
||||
var relocated = writer.ToArray();
|
||||
var trampolineLength = checked(5 + relocated.Length + 8 + 5);
|
||||
var trampolineLength = checked(prefixLength + 5 + relocated.Length + 8 + 5);
|
||||
if (trampolineCursor > trampolineEnd || (ulong)trampolineLength > trampolineEnd - trampolineCursor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trampoline = new byte[trampolineLength];
|
||||
prefix?.CopyTo(trampoline, 0);
|
||||
// lea rsp,[rsp-128] -- LEA preserves guest flags.
|
||||
trampoline[0] = 0x48;
|
||||
trampoline[1] = 0x8D;
|
||||
trampoline[2] = 0x64;
|
||||
trampoline[3] = 0x24;
|
||||
trampoline[4] = 0x80;
|
||||
relocated.CopyTo(trampoline, 5);
|
||||
var restoreOffset = 5 + relocated.Length;
|
||||
trampoline[prefixLength] = 0x48;
|
||||
trampoline[prefixLength + 1] = 0x8D;
|
||||
trampoline[prefixLength + 2] = 0x64;
|
||||
trampoline[prefixLength + 3] = 0x24;
|
||||
trampoline[prefixLength + 4] = 0x80;
|
||||
relocated.CopyTo(trampoline, prefixLength + 5);
|
||||
var restoreOffset = prefixLength + 5 + relocated.Length;
|
||||
// lea rsp,[rsp+128]
|
||||
trampoline[restoreOffset] = 0x48;
|
||||
trampoline[restoreOffset + 1] = 0x8D;
|
||||
|
|
@ -560,7 +858,19 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
private readonly record struct DecodedInstruction(Instruction Instruction);
|
||||
|
||||
private readonly record struct PatchSite(ulong Address, int ByteLength, IList<Instruction> Instructions);
|
||||
/// <summary>
|
||||
/// A run of guest instructions relocated into one trampoline.
|
||||
/// <paramref name="CoreStart"/> and <paramref name="CoreCount"/> delimit the
|
||||
/// faultable instructions the RSP shift must bracket. Instructions outside
|
||||
/// that core run unshifted, which is what lets a span start before the
|
||||
/// faulting instruction and still relocate an RSP-relative one correctly.
|
||||
/// </summary>
|
||||
private readonly record struct PatchSite(
|
||||
ulong Address,
|
||||
int ByteLength,
|
||||
IList<Instruction> Instructions,
|
||||
int CoreStart,
|
||||
int CoreCount);
|
||||
|
||||
internal readonly record struct PatchResult
|
||||
{
|
||||
|
|
@ -580,6 +890,22 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
public int FailedSites { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Faultable instructions inside a red-zone function that no trampoline
|
||||
/// span could cover. These stay unprotected and are not counted by
|
||||
/// <see cref="FailedSites"/>, which only tracks sites that were selected
|
||||
/// and then failed to patch.
|
||||
/// </summary>
|
||||
public int UnrelocatableSites { get; init; }
|
||||
|
||||
public int ControlFlowRefusals { get; init; }
|
||||
|
||||
public int BranchTargetRefusals { get; init; }
|
||||
|
||||
public int StackRefusals { get; init; }
|
||||
|
||||
public int TooShortRefusals { get; init; }
|
||||
|
||||
public ulong TrampolineBytes { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE.GpuMemory;
|
||||
|
||||
// One bit per tracked block: set while the block has at least one CPU-dirty page.
|
||||
// Regions update their bit under their own lock whenever their CPU-dirty mask
|
||||
// changes, so a clear bit lets a reader skip the block without taking any lock.
|
||||
// A reader that must observe a CPU write is ordered after it by the guest's own
|
||||
// submission, which happens after the write fault published the bit.
|
||||
public sealed class CpuDirtySummary
|
||||
{
|
||||
private const int WordBits = 64;
|
||||
private readonly long[] _words = new long[(TrackerLayout.BlockCount + WordBits - 1) / WordBits];
|
||||
|
||||
public void Set(ulong block, bool dirty)
|
||||
{
|
||||
var bit = 1L << (int)(block % WordBits);
|
||||
ref var word = ref _words[block / WordBits];
|
||||
if (dirty)
|
||||
{
|
||||
Interlocked.Or(ref word, bit);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.And(ref word, ~bit);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDirty(ulong block) => (Volatile.Read(ref _words[block / WordBits]) & (1L << (int)(block % WordBits))) != 0;
|
||||
}
|
||||
|
|
@ -16,12 +16,18 @@ public sealed class GuestPageTracker
|
|||
private readonly PageGuard _pages;
|
||||
private readonly TrackedRegion?[] _regions = new TrackedRegion?[TrackerLayout.BlockCount];
|
||||
private readonly object _regionGate = new();
|
||||
private readonly CpuDirtySummary _cpuDirtySummary = new();
|
||||
|
||||
public GuestPageTracker(PageGuard pages) => _pages = pages;
|
||||
|
||||
public bool HasCpuDirtyPages(ulong vaddr, ulong size)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
if (IsKnownCpuClean(vaddr, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return VisitRegions(vaddr, size, create: true, (region, offset, bytes) =>
|
||||
{
|
||||
using var _ = region.Lock.Hold();
|
||||
|
|
@ -341,6 +347,23 @@ public sealed class GuestPageTracker
|
|||
}
|
||||
}
|
||||
|
||||
// True when every block of the range is tracked and has no CPU-dirty page. A missing
|
||||
// region is not known clean: the precise path creates it, fully dirty.
|
||||
private bool IsKnownCpuClean(ulong vaddr, ulong size)
|
||||
{
|
||||
ValidateRange(vaddr, size);
|
||||
var last = (vaddr + size - 1) / BlockBytes;
|
||||
for (var index = vaddr / BlockBytes; index <= last; index++)
|
||||
{
|
||||
if (Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Visits (region, offset, bytes) per 4 MiB chunk; a true result stops the walk early.
|
||||
private bool VisitRegions(ulong vaddr, ulong size, bool create, Func<TrackedRegion, ulong, ulong, bool> visit)
|
||||
{
|
||||
|
|
@ -379,7 +402,7 @@ public sealed class GuestPageTracker
|
|||
return existing;
|
||||
}
|
||||
|
||||
var created = new TrackedRegion(_pages, index * BlockBytes);
|
||||
var created = new TrackedRegion(_pages, index * BlockBytes, _cpuDirtySummary);
|
||||
Volatile.Write(ref _regions[index], created);
|
||||
return created;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ public sealed class TrackedRegion
|
|||
private PageMask _recentCpuUploads;
|
||||
private PageMask _repeatedCpuWrites;
|
||||
private PageMask _hotCpuWrites;
|
||||
private readonly CpuDirtySummary? _summary;
|
||||
|
||||
public TrackedRegion(PageGuard pages, ulong baseAddress)
|
||||
public TrackedRegion(PageGuard pages, ulong baseAddress, CpuDirtySummary? summary = null)
|
||||
{
|
||||
if (baseAddress % BlockBytes != 0)
|
||||
{
|
||||
|
|
@ -26,10 +27,12 @@ public sealed class TrackedRegion
|
|||
}
|
||||
|
||||
_pages = pages;
|
||||
_summary = summary;
|
||||
BaseAddress = baseAddress;
|
||||
_cpuDirty.Fill();
|
||||
_writable.Fill();
|
||||
_readable.Fill();
|
||||
_summary?.Set(BaseAddress / BlockBytes, dirty: true);
|
||||
}
|
||||
|
||||
public RegionLock Lock { get; } = new();
|
||||
|
|
@ -205,8 +208,10 @@ public sealed class TrackedRegion
|
|||
|
||||
private ref PageMask GetDirtyMask(WriteOrigin side) => ref side == WriteOrigin.Cpu ? ref _cpuDirty : ref _gpuDirty;
|
||||
|
||||
// Every change to the CPU-dirty mask ends here, so it also publishes the block summary.
|
||||
private void UpdateCpuProtection(bool track)
|
||||
{
|
||||
_summary?.Set(BaseAddress / BlockBytes, _cpuDirty.Any);
|
||||
var mask = _cpuDirty ^ _writable;
|
||||
_writable = _cpuDirty;
|
||||
if (mask.None)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ public readonly record struct GuestThreadStartRequest(
|
|||
ulong AttributeAddress,
|
||||
string Name,
|
||||
int Priority,
|
||||
ulong AffinityMask);
|
||||
ulong AffinityMask,
|
||||
ulong StackSize = 0);
|
||||
|
||||
public readonly record struct GuestThreadSnapshot(
|
||||
ulong ThreadHandle,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ public static partial class AgcExports
|
|||
private const ulong ShaderNumShRegistersOffset = 0x5C;
|
||||
private const int ShaderStructBytes = 0x60;
|
||||
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
|
||||
private const int MaximumEmbeddedFusedScanBytes = 64 * 1024;
|
||||
// Embedded GS/HS front/back descriptors can sit beyond the first 64 KiB of
|
||||
// the upload. Scan the full shader object size accepted by AGC so the
|
||||
// continuation is registered before resource planning sees the front half.
|
||||
private const int MaximumEmbeddedFusedScanBytes = 1024 * 1024;
|
||||
private const ulong FusedShaderImageAlignment = 4;
|
||||
private const byte ComputeShaderType = 0;
|
||||
private const byte PsShaderType = 1;
|
||||
|
|
@ -166,27 +169,6 @@ public static partial class AgcExports
|
|||
return false;
|
||||
}
|
||||
|
||||
var upload = new byte[MaximumEmbeddedFusedScanBytes];
|
||||
var bytesRead = 0;
|
||||
const int readChunkBytes = 4 * 1024;
|
||||
while (bytesRead < upload.Length)
|
||||
{
|
||||
var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
|
||||
if (!ctx.Memory.TryRead(
|
||||
entryCodeAddress + (ulong)bytesRead,
|
||||
upload.AsSpan(bytesRead, chunkLength)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += chunkLength;
|
||||
}
|
||||
|
||||
if (bytesRead < ShaderStructBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requiredContinuationType = entryType == GsFrontShaderType
|
||||
? GsBackShaderType
|
||||
: HsBackShaderType;
|
||||
|
|
@ -201,57 +183,95 @@ public static partial class AgcExports
|
|||
ulong bestCodeAddress = 0;
|
||||
ulong bestHeaderAddress = 0;
|
||||
var bestDistance = ulong.MaxValue;
|
||||
for (var offset = 0;
|
||||
offset <= bytesRead - ShaderStructBytes;
|
||||
offset += sizeof(uint))
|
||||
|
||||
bool ScanForContinuation(ulong scanAddress)
|
||||
{
|
||||
var descriptor = upload.AsSpan(offset, ShaderStructBytes);
|
||||
if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
|
||||
descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
|
||||
var upload = new byte[MaximumEmbeddedFusedScanBytes];
|
||||
var bytesRead = 0;
|
||||
const int readChunkBytes = 4 * 1024;
|
||||
while (bytesRead < upload.Length)
|
||||
{
|
||||
continue;
|
||||
var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
|
||||
if (!ctx.Memory.TryRead(
|
||||
scanAddress + (ulong)bytesRead,
|
||||
upload.AsSpan(bytesRead, chunkLength)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += chunkLength;
|
||||
}
|
||||
|
||||
var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderCodeOffset..]);
|
||||
var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
descriptor[(int)ShaderSizeOffset..]);
|
||||
if (continuationCodeAddress <= entryCodeAddress ||
|
||||
continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
|
||||
!IsValidDeclaredShaderSize(continuationSize) ||
|
||||
!CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
|
||||
if (bytesRead < ShaderStructBytes)
|
||||
{
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderSpecialsOffset..]);
|
||||
if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
|
||||
var found = false;
|
||||
for (var offset = 0;
|
||||
offset <= bytesRead - ShaderStructBytes;
|
||||
offset += sizeof(uint))
|
||||
{
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var entryStages) ||
|
||||
!TryReadUInt32(
|
||||
ctx,
|
||||
continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var continuationStages) ||
|
||||
((entryStages ^ continuationStages) & waveSizeBit) != 0)
|
||||
var descriptor = upload.AsSpan(offset, ShaderStructBytes);
|
||||
if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
|
||||
descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderCodeOffset..]);
|
||||
var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
descriptor[(int)ShaderSizeOffset..]);
|
||||
if (continuationCodeAddress <= entryCodeAddress ||
|
||||
continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
|
||||
!IsValidDeclaredShaderSize(continuationSize) ||
|
||||
!CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderSpecialsOffset..]);
|
||||
if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
|
||||
{
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var entryStages) ||
|
||||
!TryReadUInt32(
|
||||
ctx,
|
||||
continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var continuationStages) ||
|
||||
((entryStages ^ continuationStages) & waveSizeBit) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var distance = continuationCodeAddress - entryCodeAddress;
|
||||
if (distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistance = distance;
|
||||
bestCodeAddress = continuationCodeAddress;
|
||||
bestHeaderAddress = scanAddress + (ulong)offset;
|
||||
found = true;
|
||||
}
|
||||
|
||||
var distance = continuationCodeAddress - entryCodeAddress;
|
||||
if (distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bestDistance = distance;
|
||||
bestCodeAddress = continuationCodeAddress;
|
||||
bestHeaderAddress = entryCodeAddress + (ulong)offset;
|
||||
// Most uploads place the second descriptor beside the code. Some
|
||||
// titles keep both descriptors in the header allocation instead, so
|
||||
// inspect that region as a fallback.
|
||||
ScanForContinuation(entryCodeAddress);
|
||||
if (bestHeaderAddress == 0)
|
||||
{
|
||||
ScanForContinuation(entryHeaderAddress);
|
||||
}
|
||||
|
||||
if (bestHeaderAddress == 0)
|
||||
|
|
@ -422,6 +442,13 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
// Both halves share one user-data block, so the fused USER_SGPR count is
|
||||
// the larger of the two.
|
||||
if (!MergeFusedUserScalarCount(ctx, frontRegistersAddress, frontRegisterCount, fusedRegistersAddress, registerCount))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
for (var occurrence = 0; occurrence < 2; occurrence++)
|
||||
{
|
||||
if (!TryFindShaderRegister(ctx, fusedRegistersAddress, registerCount, SpiShaderPgmChksumGs, occurrence, out var fusedEntry) ||
|
||||
|
|
@ -1216,6 +1243,27 @@ public static partial class AgcExports
|
|||
(frontType == GsFrontShaderType && backType == GsBackShaderType) ||
|
||||
(frontType == HsFrontShaderType && backType == HsBackShaderType);
|
||||
|
||||
private static uint UserScalarCount(uint rsrc2) => ((rsrc2 >> 1) & 0x1Fu) | (((rsrc2 >> 27) & 1u) << 5);
|
||||
|
||||
private static bool MergeFusedUserScalarCount(CpuContext ctx, ulong frontRegisters, int frontCount, ulong fusedRegisters, int fusedCount)
|
||||
{
|
||||
if (!TryFindShaderRegister(ctx, frontRegisters, frontCount, SpiShaderPgmRsrc2Gs, 0, out var frontEntry) ||
|
||||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, SpiShaderPgmRsrc2Gs, 0, out var fusedEntry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, frontEntry + sizeof(uint), out var front) ||
|
||||
!TryReadUInt32(ctx, fusedEntry + sizeof(uint), out var fused))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var count = Math.Max(UserScalarCount(front), UserScalarCount(fused));
|
||||
var merged = (fused & ~((0x1Fu << 1) | (1u << 27))) | ((count & 0x1Fu) << 1) | ((count >> 5) << 27);
|
||||
return merged == fused || TryWriteUInt32(ctx, fusedEntry + sizeof(uint), merged);
|
||||
}
|
||||
|
||||
private static bool TryFindShaderRegister(
|
||||
CpuContext ctx,
|
||||
ulong registersAddress,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ public static partial class AgcExports
|
|||
private const uint SpiShaderPgmLoGs = 0x88;
|
||||
private const uint SpiShaderPgmHiGs = 0x89;
|
||||
private const uint SpiShaderPgmRsrc1Gs = 0x8A;
|
||||
private const uint SpiShaderPgmRsrc2Gs = 0x8B;
|
||||
private const uint SpiShaderPgmChksumGs = 0x80;
|
||||
private const uint SpiShaderPgmChksumPs = 0x06;
|
||||
private const uint SpiPsInputEna = 0x1B3;
|
||||
|
|
|
|||
|
|
@ -965,6 +965,8 @@ public static class AmprExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int _unknownReadFileIdWarnings;
|
||||
|
||||
private static int CompleteReadFileRecord(CpuContext ctx, ulong commandBuffer, ReadFileCommand command)
|
||||
{
|
||||
var fileId = command.FileId;
|
||||
|
|
@ -982,6 +984,13 @@ public static class AmprExports
|
|||
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out hostPath))
|
||||
{
|
||||
// An unknown id stalls the guest's stream silently, so warn even without tracing.
|
||||
if (Interlocked.Increment(ref _unknownReadFileIdWarnings) <= 16)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.read_unknown_file_id id=0x{fileId:X8} size=0x{size:X} offset=0x{fileOffset:X}");
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ internal static class AmprFileRegistry
|
|||
private static readonly object _resolvedFileGate = new();
|
||||
private static readonly Dictionary<string, uint> _resolvedIdsByPath = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<uint, string> _resolvedPathsById = new();
|
||||
// Ids already handed to the guest; alias publishes must not re-poison them.
|
||||
private static readonly ConcurrentDictionary<uint, string> _aprResolvedPathsById = new();
|
||||
private static readonly ConcurrentDictionary<uint, byte> _loggedAprCollisionIds = new();
|
||||
// Resolved handles use a separate range from the 31-bit compatibility hashes.
|
||||
// Never reuse a handle while queued reads can still refer to it.
|
||||
private static uint _nextResolvedId = 0x80000000;
|
||||
|
|
@ -67,24 +70,52 @@ internal static class AmprFileRegistry
|
|||
|
||||
public static uint RegisterAprResolvedPath(string guestPath, string hostPath)
|
||||
{
|
||||
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
|
||||
{
|
||||
RegisterApp0Relative(relative, hostPath);
|
||||
}
|
||||
|
||||
// APR file ids are part of the guest ABI: ResolveFilepathsToIds returns
|
||||
// the 31-bit FNV-1a hash of the guest path. Keep the collision-safe
|
||||
// process-local handles used by Register() separate from this path so a
|
||||
// title can compare resolved ids with ids baked into its asset tables.
|
||||
// A hash shared by two files (13 /app0/ pairs in Demon's Souls) falls back to a handle.
|
||||
var fileId = ComputeFileId(guestPath);
|
||||
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
|
||||
return fileId;
|
||||
lock (_resolvedFileGate)
|
||||
{
|
||||
if (_aprResolvedPathsById.TryGetValue(fileId, out var claimedPath))
|
||||
{
|
||||
if (IsSameHostFile(claimedPath, hostPath))
|
||||
return fileId;
|
||||
}
|
||||
else if (!_hostPathsById.TryGetValue(fileId, out var indexed) ||
|
||||
(!indexed.Ambiguous && IsSameHostFile(indexed.HostPath, hostPath)))
|
||||
{
|
||||
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
|
||||
{
|
||||
RegisterApp0Relative(relative, hostPath);
|
||||
}
|
||||
|
||||
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
|
||||
_aprResolvedPathsById[fileId] = hostPath;
|
||||
return fileId;
|
||||
}
|
||||
}
|
||||
|
||||
var handle = Register(guestPath, hostPath);
|
||||
if (_loggedAprCollisionIds.TryAdd(fileId, 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.apr_id_collision id=0x{fileId:X8} path='{guestPath}' handle=0x{handle:X8}");
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static bool IsSameHostFile(string left, string right) =>
|
||||
HostFsPath.Comparer.Equals(Path.GetFullPath(left), Path.GetFullPath(right));
|
||||
|
||||
public static bool TryGetHostPath(uint id, out string hostPath)
|
||||
{
|
||||
if ((id & 0x80000000) != 0)
|
||||
return _resolvedPathsById.TryGetValue(id, out hostPath!);
|
||||
if (_aprResolvedPathsById.TryGetValue(id, out hostPath!))
|
||||
return true;
|
||||
if (_hostPathsById.TryGetValue(id, out var entry) && !entry.Ambiguous)
|
||||
{
|
||||
hostPath = entry.HostPath;
|
||||
|
|
@ -101,8 +132,10 @@ internal static class AmprFileRegistry
|
|||
lock (_indexGate)
|
||||
{
|
||||
_hostPathsById.Clear();
|
||||
_loggedAprCollisionIds.Clear();
|
||||
lock (_resolvedFileGate)
|
||||
{
|
||||
_aprResolvedPathsById.Clear();
|
||||
_resolvedIdsByPath.Clear();
|
||||
_resolvedPathsById.Clear();
|
||||
_nextResolvedId = 0x80000000;
|
||||
|
|
@ -417,7 +450,23 @@ internal static class AmprFileRegistry
|
|||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"SharpEmu",
|
||||
"ampr-index");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
var probePath = Path.Combine(cacheDir, $".write-test-{Environment.ProcessId}");
|
||||
using (File.Create(probePath)) { }
|
||||
File.Delete(probePath);
|
||||
}
|
||||
catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
|
||||
{
|
||||
// Sandboxed launches and mitigated child processes can lose access
|
||||
// to the profile AppData directory. Keep the index beside the
|
||||
// executable so the next launch can still reuse it.
|
||||
cacheDir = Path.Combine(AppContext.BaseDirectory, "user", "ampr-index");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.app0_index_cache_fallback path={cacheDir}: {exception.Message}");
|
||||
}
|
||||
|
||||
// Distinct roots must not share a cache file. Folding case is only
|
||||
// correct where the host filesystem folds it too.
|
||||
|
|
|
|||
|
|
@ -497,18 +497,23 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var index = _registry.FindFirstOverlappingIndex(guestAddress);
|
||||
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
|
||||
{
|
||||
var buffer = _registry.GetBuffer(_registry.GetRegisteredIdentifier(index));
|
||||
var identifier = _registry.GetRegisteredIdentifier(index);
|
||||
var buffer = _registry.GetBuffer(identifier);
|
||||
var start = Math.Max(buffer.CpuAddress, guestAddress);
|
||||
var finish = Math.Min(buffer.CpuAddress + buffer.Size, end);
|
||||
if (start < finish)
|
||||
{
|
||||
if (GuestGpuMemoryHook.Traces(start, finish - start))
|
||||
GuestGpuMemoryHook.Trace(start, finish - start,
|
||||
$"device-address-touch buffer={_registry.GetRegisteredIdentifier(index)} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
|
||||
$"device-address-touch buffer={identifier} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
|
||||
// Clean buffers remain in use through their device addresses.
|
||||
TouchBuffer(buffer);
|
||||
TouchBuffer(identifier);
|
||||
// Device-address reads reuse persistent buffers; track writes after each upload.
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
// A range without CPU-dirty pages has nothing to upload (the same early exit
|
||||
// SynchronizeBuffer takes for this read-only call), and the block summary
|
||||
// answers that without a lock for the common all-clean case.
|
||||
if (_tracker.HasCpuDirtyPages(start, finish - start))
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1052,7 +1057,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
|
||||
}
|
||||
|
||||
if (isTexelBuffer && !isWritten)
|
||||
if (isTexelBuffer)
|
||||
{
|
||||
return RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,12 +190,27 @@ public sealed partial class GpuCommandInterpreter
|
|||
SubmitFlipWithInterrupt(payload[4], payload[5], Address(payload[1], payload[2]), payload[3]);
|
||||
break;
|
||||
default:
|
||||
throw _host.Fatal($"The marker is unknown: offset=0x{packet.Offset:X5} marker=0x{markerId:X} address=0x{packet.PacketAddress:X16}.");
|
||||
// A marker id this interpreter has no side effect for (profiling/debug annotations such as
|
||||
// 0x6EC and 0x80C in Demon's Souls). The packet carries no work, so it is consumed; the id is
|
||||
// reported once so a marker that later turns out to matter is still visible.
|
||||
NoteUnknownMarker(markerId, packet);
|
||||
break;
|
||||
}
|
||||
|
||||
return packet.Length - 1;
|
||||
}
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<uint, bool> _reportedUnknownMarkers = new();
|
||||
|
||||
private static void NoteUnknownMarker(uint markerId, in PacketContext packet)
|
||||
{
|
||||
if (_reportedUnknownMarkers.TryAdd(markerId, true))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] command_stream.marker_ignored marker=0x{markerId:X} offset=0x{packet.Offset:X5} address=0x{packet.PacketAddress:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
internal uint PushMarkerPacket(in PacketContext packet, ReadOnlySpan<uint> payload) => packet.Length - 1;
|
||||
|
||||
internal uint PopMarkerPacket(in PacketContext packet, ReadOnlySpan<uint> payload) => packet.Length - 1;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
private bool _maybeHashValid;
|
||||
private bool _gpuModified;
|
||||
private bool _bufferModified;
|
||||
private bool _bufferHoldsGpuContents;
|
||||
|
||||
public ImageDescription Description;
|
||||
public readonly ImageBacking Backing = new();
|
||||
|
|
@ -130,9 +131,10 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
Backing.Usage = create.Usage;
|
||||
|
||||
var vk = device.Vk;
|
||||
if (vk.CreateImage(device.Device, &create, null, out Backing.Handle) != Result.Success)
|
||||
var createResult = vk.CreateImage(device.Device, &create, null, out Backing.Handle);
|
||||
if (createResult != Result.Success)
|
||||
{
|
||||
throw CreateFailure(create);
|
||||
throw CreateFailure(create, "vkCreateImage", createResult, 0);
|
||||
}
|
||||
|
||||
vk.GetImageMemoryRequirements(device.Device, Backing.Handle, out var requirements);
|
||||
|
|
@ -158,21 +160,33 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
if (allocated != Result.Success || vk.BindImageMemory(device.Device, Backing.Handle, Backing.Memory, 0) != Result.Success)
|
||||
if (allocated != Result.Success)
|
||||
{
|
||||
vk.DestroyImage(device.Device, Backing.Handle, null);
|
||||
device.FreeMemory(Backing.Memory);
|
||||
Backing.Handle = default;
|
||||
Backing.Memory = default;
|
||||
throw CreateFailure(create);
|
||||
throw CreateFailure(create, "vkAllocateMemory", allocated, requirements.Size);
|
||||
}
|
||||
|
||||
var bindResult = vk.BindImageMemory(device.Device, Backing.Handle, Backing.Memory, 0);
|
||||
if (bindResult != Result.Success)
|
||||
{
|
||||
vk.DestroyImage(device.Device, Backing.Handle, null);
|
||||
device.FreeMemory(Backing.Memory);
|
||||
Backing.Handle = default;
|
||||
Backing.Memory = default;
|
||||
throw CreateFailure(create, "vkBindImageMemory", bindResult, requirements.Size);
|
||||
}
|
||||
|
||||
Backing.AllocationSize = requirements.Size;
|
||||
}
|
||||
|
||||
private static Exception CreateFailure(in ImageCreateInfo create) =>
|
||||
private static Exception CreateFailure(in ImageCreateInfo create, string operation, Result result, ulong requiredBytes) =>
|
||||
SubmissionScheduler.Fatal(
|
||||
$"The image could not be created: extent={create.Extent.Width}x{create.Extent.Height}x{create.Extent.Depth} format={(int)create.Format} layers={create.ArrayLayers} levels={create.MipLevels}.");
|
||||
$"The image could not be created: operation={operation} result={result} required_bytes={requiredBytes} " +
|
||||
$"extent={create.Extent.Width}x{create.Extent.Height}x{create.Extent.Depth} format={create.Format}({(int)create.Format}) " +
|
||||
$"layers={create.ArrayLayers} levels={create.MipLevels} usage=0x{(uint)create.Usage:X} flags=0x{(uint)create.Flags:X}.");
|
||||
|
||||
internal static bool TrySelectSupportedImageConfiguration(IImageFormatSupport device, ref ImageCreateInfo configuration, bool allowCompressedImageFallback)
|
||||
{
|
||||
|
|
@ -351,12 +365,21 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
|
||||
public bool IsGpuModified => _gpuModified;
|
||||
|
||||
public void MarkGpuModified() => _gpuModified = true;
|
||||
public void MarkGpuModified()
|
||||
{
|
||||
_gpuModified = true;
|
||||
_bufferHoldsGpuContents = false;
|
||||
}
|
||||
|
||||
public void ClearGpuModified() => _gpuModified = false;
|
||||
|
||||
public bool IsBufferModified => _bufferModified;
|
||||
|
||||
// The guest buffer cache holds this image's current GPU contents.
|
||||
public bool BufferHoldsGpuContents => _bufferHoldsGpuContents;
|
||||
|
||||
public void MarkBufferHoldsGpuContents() => _bufferHoldsGpuContents = true;
|
||||
|
||||
public void MarkBufferModified() => _bufferModified = true;
|
||||
|
||||
public void ClearBufferModified() => _bufferModified = false;
|
||||
|
|
|
|||
|
|
@ -349,6 +349,11 @@ public sealed partial class GuestImageCache
|
|||
case ImageRole.Texture:
|
||||
recreate |= requested.IsDepth && !cachedInfo.IsDepth;
|
||||
recreate |= rawD16Texture;
|
||||
// A color texture that reads neither the depth nor the stencil plane
|
||||
// reinterprets the memory as color, so its bytes move to a color image.
|
||||
recreate |= cachedInfo.IsDepth && !requested.IsDepth &&
|
||||
!ViewFormatRules.IsDepthCompatible(requested.PixelFormat) &&
|
||||
!ViewFormatRules.IsStencilViewFormat(requested.PixelFormat);
|
||||
break;
|
||||
case ImageRole.StorageImage:
|
||||
case ImageRole.ColorTarget:
|
||||
|
|
@ -440,23 +445,15 @@ public sealed partial class GuestImageCache
|
|||
ref var cachedInfo = ref cached.Description;
|
||||
var currentTick = _scheduler.CurrentTick;
|
||||
var safeToDelete = currentTick - Math.Min(currentTick, cached.LastAccessTick) > TicksBeforeRemoval;
|
||||
var requestedBlock = requested.BytesPerBlock * requested.Samples;
|
||||
var cachedBlock = cachedInfo.BytesPerBlock * cachedInfo.Samples;
|
||||
var requestedBlockExtent = requested.BlockExtent;
|
||||
var cachedBlockExtent = cachedInfo.BlockExtent;
|
||||
|
||||
if (requested.Data.Address == cachedInfo.Data.Address)
|
||||
if (requested.Data.Address == cachedInfo.Data.Address &&
|
||||
requestedBlockExtent.Width == cachedBlockExtent.Width && requestedBlockExtent.Height == cachedBlockExtent.Height &&
|
||||
requestedBlock == cachedBlock)
|
||||
{
|
||||
var requestedBlock = requested.BytesPerBlock * requested.Samples;
|
||||
var cachedBlock = cachedInfo.BytesPerBlock * cachedInfo.Samples;
|
||||
var requestedBlockExtent = requested.BlockExtent;
|
||||
var cachedBlockExtent = cachedInfo.BlockExtent;
|
||||
if (requestedBlockExtent.Width != cachedBlockExtent.Width || requestedBlockExtent.Height != cachedBlockExtent.Height || requestedBlock != cachedBlock)
|
||||
{
|
||||
if (safeToDelete)
|
||||
{
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
}
|
||||
|
||||
return new OverlapResolution(mergedImageIdentifier);
|
||||
}
|
||||
|
||||
var depthImageIdentifier = ResolveDepthOverlap(requested, role, cachedImageIdentifier);
|
||||
if (depthImageIdentifier.IsValid)
|
||||
{
|
||||
|
|
@ -509,50 +506,32 @@ public sealed partial class GuestImageCache
|
|||
$"type={(uint)requested.Type}/{(uint)cachedInfo.Type} tile={(uint)requested.TileMode}/{(uint)cachedInfo.TileMode}.");
|
||||
}
|
||||
|
||||
if (requested.Data.Address > cachedInfo.Data.Address)
|
||||
var requestedMip = requested.FindMatchingMipLevel(cachedInfo);
|
||||
if (requestedMip >= 0)
|
||||
{
|
||||
var mip = requested.FindMatchingMipLevel(cachedInfo);
|
||||
if (mip >= 0)
|
||||
{
|
||||
var layer = requested.FindMatchingArraySlice(cachedInfo, mip);
|
||||
if (layer >= 0)
|
||||
{
|
||||
return new OverlapResolution(cachedImageIdentifier, mip, layer);
|
||||
}
|
||||
}
|
||||
|
||||
if (safeToDelete)
|
||||
{
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
}
|
||||
|
||||
return new OverlapResolution(ResourceSlotIdentifier.Invalid);
|
||||
var layer = requested.FindMatchingArraySlice(cachedInfo, requestedMip);
|
||||
return new OverlapResolution(cachedImageIdentifier, requestedMip, layer);
|
||||
}
|
||||
|
||||
var containedMip = cachedInfo.FindMatchingMipLevel(requested);
|
||||
if (containedMip >= 0)
|
||||
{
|
||||
var containedLayer = cachedInfo.FindMatchingArraySlice(requested, containedMip);
|
||||
if (containedLayer >= 0)
|
||||
if (!mergedImageIdentifier.IsValid)
|
||||
{
|
||||
if (cached.Binding.IsTarget)
|
||||
{
|
||||
cached.Binding.NeedsRebind = true;
|
||||
if (mergedImageIdentifier.IsValid)
|
||||
{
|
||||
_slots[mergedImageIdentifier].Binding.IsTarget = true;
|
||||
}
|
||||
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
return new OverlapResolution(mergedImageIdentifier);
|
||||
}
|
||||
|
||||
if (mergedImageIdentifier.IsValid)
|
||||
{
|
||||
CopyIntoMip(mergedImageIdentifier, cachedImageIdentifier, (uint)containedMip, (uint)containedLayer);
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
}
|
||||
return new OverlapResolution(GrowImage(requested, cachedImageIdentifier));
|
||||
}
|
||||
|
||||
cached.Binding.NeedsRebind |= cached.Binding.IsBound || cached.Binding.IsTarget;
|
||||
_slots[mergedImageIdentifier].Binding.IsTarget |= cached.Binding.IsTarget;
|
||||
CopyIntoMip(mergedImageIdentifier, cachedImageIdentifier, (uint)containedMip, (uint)containedLayer);
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
return new OverlapResolution(mergedImageIdentifier);
|
||||
}
|
||||
|
||||
if (requested.Data.Address >= cachedInfo.Data.Address && safeToDelete)
|
||||
{
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
}
|
||||
|
||||
return new OverlapResolution(mergedImageIdentifier);
|
||||
|
|
@ -572,7 +551,17 @@ public sealed partial class GuestImageCache
|
|||
}
|
||||
|
||||
PopulateFromGuest(expandedImageIdentifier, ImageRequest.Refresh(description, UploadRole(source)), "expand");
|
||||
CopyWholeImage(expandedImageIdentifier, sourceImageIdentifier);
|
||||
var mip = source.Description.FindMatchingMipLevel(description);
|
||||
var layer = source.Description.FindMatchingArraySlice(description, mip);
|
||||
if (layer >= 0)
|
||||
{
|
||||
CopyIntoMip(expandedImageIdentifier, sourceImageIdentifier, (uint)mip, (uint)layer);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyWholeImage(expandedImageIdentifier, sourceImageIdentifier);
|
||||
}
|
||||
|
||||
ReleaseImage(sourceImageIdentifier);
|
||||
return expandedImageIdentifier;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@ public sealed unsafe partial class GuestImageCache
|
|||
}
|
||||
|
||||
DownloadToBuffer(image, buffer, bufferOffset, copySize, plan);
|
||||
image.MarkBufferHoldsGpuContents();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ public static class GuestPixelFormats
|
|||
new(GuestPixelFormat.Bits16Float, 2, 0, 2, true, false),
|
||||
new(GuestPixelFormat.Bits8_8UNorm, 2, 0, 2, true, false),
|
||||
new(GuestPixelFormat.Bits8_8SNorm, 2, 0, 2, true, false),
|
||||
new(GuestPixelFormat.Bits8_8UScaled, 2, 0, 2, true, false),
|
||||
new(GuestPixelFormat.Bits8_8UInt, 2, 0, 2, true, true),
|
||||
new(GuestPixelFormat.Bits8_8SInt, 2, 0, 2, true, false, true),
|
||||
new(GuestPixelFormat.Bits32UInt, 4, 0, 4, true, true),
|
||||
|
|
@ -325,6 +326,9 @@ public static class GuestPixelFormats
|
|||
new(GuestPixelFormat.Bits16_16UInt, 4, 0, 4, true, true),
|
||||
new(GuestPixelFormat.Bits16_16SInt, 4, 0, 4, true, false, true),
|
||||
new(GuestPixelFormat.Bits16_16Float, 4, 0, 4, true, false),
|
||||
// Vulkan has no normalized 11:11:10 format. Keep the guest packing sampled
|
||||
// through the native 32-bit packed-float view, matching the host backend.
|
||||
new(GuestPixelFormat.Bits11_11_10UNorm, 4, 0, 4, true, false),
|
||||
new(GuestPixelFormat.Bits11_11_10UInt, 4, 0, 4, true, true),
|
||||
new(GuestPixelFormat.Bits11_11_10Float, 4, 0, 4, true, false),
|
||||
new(GuestPixelFormat.Bits10_10_10_2UNorm, 4, 0, 4, true, false),
|
||||
|
|
@ -347,8 +351,8 @@ public static class GuestPixelFormats
|
|||
new(GuestPixelFormat.Bits32_32_32_32UInt, 16, 0, 16, true, true),
|
||||
new(GuestPixelFormat.Bits32_32_32_32SInt, 16, 0, 16, true, false, true),
|
||||
new(GuestPixelFormat.Bits32_32_32_32Float, 16, 0, 16, true, false),
|
||||
new(GuestPixelFormat.Bits8Srgb, 1, 0, 0, true, false),
|
||||
new(GuestPixelFormat.Bits8_8Srgb, 2, 0, 0, true, false),
|
||||
new(GuestPixelFormat.Bits8Srgb, 1, 0, 1, true, false),
|
||||
new(GuestPixelFormat.Bits8_8Srgb, 2, 0, 2, true, false),
|
||||
new(GuestPixelFormat.Bits8_8_8_8Srgb, 4, 0, 4, true, false),
|
||||
new(GuestPixelFormat.Bits9_9_9_5Float, 4, 0, 0, true, false),
|
||||
new(GuestPixelFormat.Bits5_6_5UNorm, 2, 0, 2, true, false),
|
||||
|
|
@ -432,6 +436,7 @@ public static class GuestPixelFormats
|
|||
(GuestPixelFormat.Bits16Float, Format.R16Sfloat),
|
||||
(GuestPixelFormat.Bits8_8UNorm, Format.R8G8Unorm),
|
||||
(GuestPixelFormat.Bits8_8SNorm, Format.R8G8SNorm),
|
||||
(GuestPixelFormat.Bits8_8UScaled, Format.R8G8Uscaled),
|
||||
(GuestPixelFormat.Bits8_8UInt, Format.R8G8Uint),
|
||||
(GuestPixelFormat.Bits8_8SInt, Format.R8G8Sint),
|
||||
(GuestPixelFormat.Bits32UInt, Format.R32Uint),
|
||||
|
|
@ -442,6 +447,7 @@ public static class GuestPixelFormats
|
|||
(GuestPixelFormat.Bits16_16UInt, Format.R16G16Uint),
|
||||
(GuestPixelFormat.Bits16_16SInt, Format.R16G16Sint),
|
||||
(GuestPixelFormat.Bits16_16Float, Format.R16G16Sfloat),
|
||||
(GuestPixelFormat.Bits11_11_10UNorm, Format.B10G11R11UfloatPack32),
|
||||
(GuestPixelFormat.Bits11_11_10Float, Format.B10G11R11UfloatPack32),
|
||||
(GuestPixelFormat.Bits10_10_10_2UNorm, Format.A2B10G10R10UnormPack32),
|
||||
(GuestPixelFormat.Bits10_10_10_2UInt, Format.A2B10G10R10UintPack32),
|
||||
|
|
|
|||
|
|
@ -131,8 +131,29 @@ public struct ImageDescription
|
|||
// Return the matching mip level in the containing image, or -1 if none matches.
|
||||
public readonly int FindMatchingMipLevel(in ImageDescription container)
|
||||
{
|
||||
if (!IsCompatible(container) || TileMode != container.TileMode || Resources.Levels != 1 ||
|
||||
container.Resources.Layers == 0 || container.Resources.Levels > MaxLevels)
|
||||
if (container.Resources.Levels > MaxLevels)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (uint level = 0; level < container.Resources.Levels; level++)
|
||||
{
|
||||
if (FindMatchingArraySlice(container, (int)level) >= 0)
|
||||
{
|
||||
return (int)level;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Return the matching slice in the containing mip level, or -1 if none matches.
|
||||
public readonly int FindMatchingArraySlice(in ImageDescription container, int mip)
|
||||
{
|
||||
if (!IsCompatible(container) || TileMode != container.TileMode || Type != container.Type || IsVolume ||
|
||||
Resources.Levels != 1 || Resources.Layers == 0 || container.Resources.Layers == 0 || mip < 0 ||
|
||||
(uint)mip >= container.Resources.Levels || container.Resources.Levels > MaxLevels ||
|
||||
!IsValidRange(Data) || !IsValidRange(container.Data) || Data.Address < container.Data.Address || Data.End > container.Data.End)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -143,95 +164,38 @@ public struct ImageDescription
|
|||
return -1;
|
||||
}
|
||||
|
||||
var mip = -1;
|
||||
for (uint level = 0; level < container.Resources.Levels; level++)
|
||||
{
|
||||
var layout = container.MipLayout[(int)level];
|
||||
if (layout.Size == 0 || layout.Size % container.Resources.Layers != 0 || container.Data.Address > ulong.MaxValue - layout.Offset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mipBase = container.Data.Address + layout.Offset;
|
||||
var sliceSize = layout.Size / container.Resources.Layers;
|
||||
if (sliceSize == 0 || mipBase > ulong.MaxValue - layout.Size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mipEnd = mipBase + layout.Size;
|
||||
if (Data.Address >= mipBase && Data.Address < mipEnd && (Data.Address - mipBase) % sliceSize == 0)
|
||||
{
|
||||
mip = (int)level;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mip < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Extent.Width != Math.Max(container.Extent.Width >> mip, 1) || Extent.Height != Math.Max(container.Extent.Height >> mip, 1))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var mipDepth = Math.Max(container.Extent.Depth >> mip, 1);
|
||||
if (container.Type == GuestImageType.Color3D && Type == GuestImageType.Color2D)
|
||||
{
|
||||
if (Resources.Layers != mipDepth)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (Type != container.Type)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return mip;
|
||||
}
|
||||
|
||||
// Return the matching slice in the containing mip level, or -1 if none matches.
|
||||
public readonly int FindMatchingArraySlice(in ImageDescription container, int mip)
|
||||
{
|
||||
if (!IsCompatible(container) || Type != container.Type || mip < 0 || (uint)mip >= container.Resources.Levels ||
|
||||
container.Resources.Levels > MaxLevels || container.Resources.Layers == 0 || Data.Size == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Extent.Width != Math.Max(container.Extent.Width >> mip, 1) || Extent.Height != Math.Max(container.Extent.Height >> mip, 1))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var layout = container.MipLayout[mip];
|
||||
if (layout.Size == 0 || layout.Size % container.Resources.Layers != 0 || container.Data.Address > ulong.MaxValue - layout.Offset)
|
||||
if (Extent.Width != Math.Max(container.Extent.Width >> mip, 1) || Extent.Height != Math.Max(container.Extent.Height >> mip, 1) ||
|
||||
Extent.Depth != container.Extent.Depth || MipLayout[0].Pitch != layout.Pitch || MipLayout[0].Height != layout.Height ||
|
||||
layout.Size == 0 || layout.Size % container.Resources.Layers != 0 ||
|
||||
container.Data.Size % container.Resources.Layers != 0 || Data.Size % Resources.Layers != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var sliceSize = layout.Size / container.Resources.Layers;
|
||||
if (sliceSize == 0 || Data.Size % sliceSize != 0)
|
||||
// PS5 array slices contain the whole reversed mip chain, rather than
|
||||
// storing all slices of one mip contiguously.
|
||||
var sliceStride = container.Data.Size / container.Resources.Layers;
|
||||
var childStride = Data.Size / Resources.Layers;
|
||||
if (childStride != layout.Size / container.Resources.Layers || layout.Offset >= sliceStride ||
|
||||
childStride > sliceStride - layout.Offset || (Resources.Layers > 1 && childStride != sliceStride))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var mipBase = container.Data.Address + layout.Offset;
|
||||
if (Data.Address < mipBase)
|
||||
var addressDelta = Data.Address - container.Data.Address;
|
||||
if (addressDelta < layout.Offset || (addressDelta - layout.Offset) % sliceStride != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var delta = Data.Address - mipBase;
|
||||
if (delta % Data.Size != 0 || delta / Data.Size > int.MaxValue)
|
||||
var layer = (addressDelta - layout.Offset) / sliceStride;
|
||||
if (layer > int.MaxValue || layer >= container.Resources.Layers || Resources.Layers > container.Resources.Layers - layer)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int)(delta / Data.Size);
|
||||
return (int)layer;
|
||||
}
|
||||
|
||||
public readonly bool IsSupportedDepthTarget
|
||||
|
|
|
|||
|
|
@ -122,6 +122,17 @@ public static partial class ImageRequestBuilders
|
|||
}
|
||||
|
||||
var depth = volume ? words.Depth + 1 : 1;
|
||||
// Some Prospero titles encode a full 3D attachment with SliceMax as
|
||||
// the exclusive depth (one past the last slice). The hardware accepts
|
||||
// that form, while Vulkan needs the actual slice count for a 2D-array
|
||||
// view. Normalize only this exact one-past range; other invalid views
|
||||
// remain rejected below.
|
||||
if (volume && view.BaseLayer == 0 && view.LayerCount == depth + 1)
|
||||
{
|
||||
view = new TargetViewRange(0, depth, depth);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] normalized an exclusive 3D color-target slice range to depth={depth}");
|
||||
}
|
||||
var tileMode = words.TileMode;
|
||||
var standard4 = tileMode == GuestTileMode.Standard4KB;
|
||||
var standard64 = tileMode == GuestTileMode.Standard64KB;
|
||||
|
|
|
|||
|
|
@ -240,7 +240,8 @@ public static partial class ImageRequestBuilders
|
|||
{
|
||||
pixelFormat = depthFormat.DepthAttachmentFormat;
|
||||
}
|
||||
var storageViewFormat = storage && format == GuestPixelFormat.Bits32SInt ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat);
|
||||
// Atomic storage images are declared as UINT in SPIR-V, including float atomics.
|
||||
var storageViewFormat = storage && (shape.Atomic || format == GuestPixelFormat.Bits32SInt) ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat);
|
||||
var viewFormat = storage && storageViewFormat != Format.Undefined ? storageViewFormat : pixelFormat;
|
||||
var blockBytes = GuestPixelFormats.BlockCompressedBytes(format);
|
||||
var description = ImageDescription.Create();
|
||||
|
|
|
|||
|
|
@ -42,28 +42,30 @@ public enum SamplerBorderColor : uint
|
|||
FromTable = 3,
|
||||
}
|
||||
|
||||
// One Vulkan sampler per distinct guest sampler descriptor, created on first use.
|
||||
// One Vulkan sampler per distinct guest sampler descriptor and view numeric class,
|
||||
// created on first use. The guest border color has no numeric class; Vulkan's has to
|
||||
// match the sampled view, and integer views cannot be filtered.
|
||||
public sealed unsafe class SamplerStore : IDisposable
|
||||
{
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly Dictionary<(uint, uint, uint, uint), Sampler> _samplers = new();
|
||||
private readonly Dictionary<(uint, uint, uint, uint, bool), Sampler> _samplers = new();
|
||||
private readonly object _gate = new();
|
||||
|
||||
public SamplerStore(GpuDeviceInfo device) => _device = device;
|
||||
|
||||
public int Count => _samplers.Count;
|
||||
|
||||
public Sampler GetSampler(in SamplerDescriptorWords words)
|
||||
public Sampler GetSampler(in SamplerDescriptorWords words, bool integerView)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var key = (words[0], words[1], words[2], words[3]);
|
||||
var key = (words[0], words[1], words[2], words[3], integerView);
|
||||
if (_samplers.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var sampler = CreateSampler(words);
|
||||
var sampler = CreateSampler(words, integerView);
|
||||
_samplers.Add(key, sampler);
|
||||
return sampler;
|
||||
}
|
||||
|
|
@ -96,10 +98,10 @@ public sealed unsafe class SamplerStore : IDisposable
|
|||
_ => throw SubmissionScheduler.Fatal($"The sampler clamp mode is unknown: clamp={clamp}."),
|
||||
};
|
||||
|
||||
private Sampler CreateSampler(in SamplerDescriptorWords words)
|
||||
private Sampler CreateSampler(in SamplerDescriptorWords words, bool integerView)
|
||||
{
|
||||
var magnify = words.MagnifyFilter;
|
||||
var minify = words.MinifyFilter;
|
||||
var magnify = integerView ? (uint)SamplerFilter.Point : words.MagnifyFilter;
|
||||
var minify = integerView ? (uint)SamplerFilter.Point : words.MinifyFilter;
|
||||
var anisotropic = IsAnisotropic(magnify) || IsAnisotropic(minify);
|
||||
var anisotropyRatio = 1.0f;
|
||||
if (anisotropic)
|
||||
|
|
@ -117,6 +119,11 @@ public sealed unsafe class SamplerStore : IDisposable
|
|||
}
|
||||
|
||||
var mipFilter = (SamplerMipFilter)words.MipFilter;
|
||||
if (integerView && mipFilter == SamplerMipFilter.Linear)
|
||||
{
|
||||
mipFilter = SamplerMipFilter.Point;
|
||||
}
|
||||
|
||||
var minLod = 0.0f;
|
||||
var maxLod = 0.0f;
|
||||
if (mipFilter != SamplerMipFilter.None)
|
||||
|
|
@ -125,26 +132,21 @@ public sealed unsafe class SamplerStore : IDisposable
|
|||
maxLod = words.MaxLod / 256.0f;
|
||||
}
|
||||
|
||||
BorderColor border;
|
||||
switch ((SamplerBorderColor)words.BorderColorType)
|
||||
var borderType = (SamplerBorderColor)words.BorderColorType;
|
||||
if (borderType == SamplerBorderColor.FromTable)
|
||||
{
|
||||
case SamplerBorderColor.TransparentBlack:
|
||||
border = BorderColor.IntTransparentBlack;
|
||||
break;
|
||||
case SamplerBorderColor.OpaqueBlack:
|
||||
border = BorderColor.IntOpaqueBlack;
|
||||
break;
|
||||
case SamplerBorderColor.OpaqueWhite:
|
||||
border = BorderColor.IntOpaqueWhite;
|
||||
break;
|
||||
case SamplerBorderColor.FromTable:
|
||||
Console.Error.WriteLine($"[LOADER][WARN] A table border color is approximated as transparent black: index={words.BorderColorIndex}.");
|
||||
border = BorderColor.IntTransparentBlack;
|
||||
break;
|
||||
default:
|
||||
throw SubmissionScheduler.Fatal($"The border color type is unknown: type={words.BorderColorType}.");
|
||||
Console.Error.WriteLine($"[LOADER][WARN] A table border color is approximated as transparent black: index={words.BorderColorIndex}.");
|
||||
borderType = SamplerBorderColor.TransparentBlack;
|
||||
}
|
||||
|
||||
var border = borderType switch
|
||||
{
|
||||
SamplerBorderColor.TransparentBlack => integerView ? BorderColor.IntTransparentBlack : BorderColor.FloatTransparentBlack,
|
||||
SamplerBorderColor.OpaqueBlack => integerView ? BorderColor.IntOpaqueBlack : BorderColor.FloatOpaqueBlack,
|
||||
SamplerBorderColor.OpaqueWhite => integerView ? BorderColor.IntOpaqueWhite : BorderColor.FloatOpaqueWhite,
|
||||
_ => throw SubmissionScheduler.Fatal($"The border color type is unknown: type={words.BorderColorType}."),
|
||||
};
|
||||
|
||||
var info = new SamplerCreateInfo
|
||||
{
|
||||
SType = StructureType.SamplerCreateInfo,
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@ public readonly record struct DisplaySurfaceWords(
|
|||
bool Compressed);
|
||||
|
||||
// The shape the shader module was compiled with; the view type must match it.
|
||||
public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Cube, bool Storage, bool DynamicMip, TextureNumericClass NumericClass, bool OneDimensional = false, bool R128 = false, bool Multisampled = false, bool DepthCompare = false)
|
||||
public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Cube, bool Storage, bool DynamicMip, TextureNumericClass NumericClass, bool OneDimensional = false, bool R128 = false, bool Multisampled = false, bool DepthCompare = false, bool Atomic = false)
|
||||
{
|
||||
public ShaderImageShape(bool volume, bool arrayed, bool storage, bool dynamicMip, TextureNumericClass numericClass)
|
||||
: this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false)
|
||||
: this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false, false, false)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,63 +173,57 @@ public static partial class TileGeometry
|
|||
switch (bytesPerElement)
|
||||
{
|
||||
case 1:
|
||||
offset ^= (y << 2) & 0x0008;
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 3) & 0x00a0;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 6) & 0x1000;
|
||||
offset ^= (y << 7) & 0x4000;
|
||||
offset ^= x & 0x0007;
|
||||
offset ^= (x << 3) & 0x0040;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
offset ^= Bit(x, 0, 0) ^ Bit(x, 1, 1) ^ Bit(x, 2, 2) ^ Bit(x, 3, 3);
|
||||
offset ^= Bit(y, 0, 4) ^ Bit(y, 1, 5) ^ Bit(y, 2, 6) ^ Bit(y, 3, 7);
|
||||
offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
|
||||
offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
|
||||
offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
|
||||
offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
|
||||
offset ^= Bit(x, 6, 12) ^ Bit(y, 6, 13);
|
||||
offset ^= Bit(x, 7, 14) ^ Bit(y, 8, 14);
|
||||
offset ^= Bit(x, 8, 15) ^ Bit(y, 7, 15);
|
||||
return offset;
|
||||
case 2:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 8) & 0x5000;
|
||||
offset ^= (x << 1) & 0x000e;
|
||||
offset ^= (x << 4) & 0x0480;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
offset ^= Bit(x, 0, 1) ^ Bit(x, 1, 2) ^ Bit(x, 2, 3);
|
||||
offset ^= Bit(y, 0, 4) ^ Bit(y, 1, 5) ^ Bit(y, 2, 6) ^ Bit(x, 3, 7);
|
||||
offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
|
||||
offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
|
||||
offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
|
||||
offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
|
||||
offset ^= Bit(y, 3, 12) ^ Bit(x, 6, 13);
|
||||
offset ^= Bit(x, 7, 14) ^ Bit(y, 7, 14);
|
||||
offset ^= Bit(x, 8, 15) ^ Bit(y, 6, 15);
|
||||
return offset;
|
||||
case 4:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 9) & 0x1000;
|
||||
offset ^= (y << 8) & 0x4000;
|
||||
offset ^= (x << 2) & 0x000c;
|
||||
offset ^= (x << 5) & 0x0380;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 9) & 0xa000;
|
||||
offset ^= Bit(x, 0, 2) ^ Bit(x, 1, 3);
|
||||
offset ^= Bit(y, 0, 4) ^ Bit(y, 1, 5) ^ Bit(x, 2, 6) ^ Bit(y, 2, 7);
|
||||
offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
|
||||
offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
|
||||
offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
|
||||
offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
|
||||
offset ^= Bit(x, 3, 12) ^ Bit(y, 3, 13);
|
||||
offset ^= Bit(x, 6, 14) ^ Bit(y, 7, 14);
|
||||
offset ^= Bit(x, 7, 15) ^ Bit(y, 6, 15);
|
||||
return offset;
|
||||
case 8:
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 10) & 0x5000;
|
||||
offset ^= (x << 3) & 0x0008;
|
||||
offset ^= (x << 4) & 0x0460;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 10) & 0x2000;
|
||||
offset ^= (x << 9) & 0x8000;
|
||||
offset ^= Bit(x, 0, 3) ^ Bit(y, 0, 4) ^ Bit(x, 1, 5) ^ Bit(x, 2, 6) ^ Bit(y, 1, 7);
|
||||
offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
|
||||
offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
|
||||
offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
|
||||
offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
|
||||
offset ^= Bit(y, 2, 12) ^ Bit(x, 3, 13);
|
||||
offset ^= Bit(x, 7, 14) ^ Bit(y, 3, 14);
|
||||
offset ^= Bit(x, 6, 15) ^ Bit(y, 6, 15);
|
||||
return offset;
|
||||
case 16:
|
||||
offset ^= (x << 4) & 0x0410;
|
||||
offset ^= (x << 5) & 0x0340;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 11) & 0xa000;
|
||||
offset ^= (y << 5) & 0x0f20;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 10) & 0x1000;
|
||||
offset ^= (y << 11) & 0x4000;
|
||||
offset ^= Bit(x, 0, 4) ^ Bit(y, 0, 5) ^ Bit(x, 1, 6) ^ Bit(y, 1, 7);
|
||||
offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
|
||||
offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
|
||||
offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
|
||||
offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
|
||||
offset ^= Bit(x, 2, 12) ^ Bit(y, 2, 13);
|
||||
offset ^= Bit(x, 6, 14) ^ Bit(y, 3, 14);
|
||||
offset ^= Bit(x, 3, 15) ^ Bit(y, 6, 15);
|
||||
return offset;
|
||||
default:
|
||||
throw SubmissionScheduler.Fatal($"The render-target tile block does not support this element size: bytes={bytesPerElement}.");
|
||||
|
|
|
|||
|
|
@ -130,6 +130,19 @@ public static class ViewFormatRules
|
|||
_ => None,
|
||||
};
|
||||
|
||||
// Integer views sample raw integers: no filtering, integer border colors.
|
||||
public static bool IsIntegerFormat(Format format) => format is
|
||||
Format.R8Uint or Format.R8Sint or Format.R8G8Uint or Format.R8G8Sint or
|
||||
Format.R8G8B8A8Uint or Format.R8G8B8A8Sint or Format.B8G8R8A8Uint or Format.B8G8R8A8Sint or
|
||||
Format.A8B8G8R8UintPack32 or Format.A8B8G8R8SintPack32 or
|
||||
Format.A2B10G10R10UintPack32 or Format.A2B10G10R10SintPack32 or
|
||||
Format.A2R10G10B10UintPack32 or Format.A2R10G10B10SintPack32 or
|
||||
Format.R16Uint or Format.R16Sint or Format.R16G16Uint or Format.R16G16Sint or
|
||||
Format.R16G16B16A16Uint or Format.R16G16B16A16Sint or
|
||||
Format.R32Uint or Format.R32Sint or Format.R32G32Uint or Format.R32G32Sint or
|
||||
Format.R32G32B32Uint or Format.R32G32B32Sint or Format.R32G32B32A32Uint or Format.R32G32B32A32Sint or
|
||||
Format.S8Uint;
|
||||
|
||||
// The bytes of one texel block of a host format; 0 for a format outside the compatibility classes.
|
||||
public static uint BlockBytes(Format format)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -164,6 +164,9 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
|
||||
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => true;
|
||||
|
||||
// Metal has no 64-bit workgroup atomics here; keep the non-atomic 32-bit pair.
|
||||
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => false;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => new(MaxDimension, MaxDimension, MaxDimension, MaxDimension);
|
||||
|
||||
SampleCountFlags IShaderPipelineHost.NoAttachmentSampleCounts =>
|
||||
|
|
@ -174,6 +177,9 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
|
||||
bool IShaderPipelineHost.TryReadCleanGuestWord(ulong address, out uint word) => TryReadWord(address, out word);
|
||||
|
||||
bool IShaderPipelineHost.TryReadResidentGuestBytes(ulong address, Span<byte> destination, bool clean) =>
|
||||
Memory.TryRead(address, destination);
|
||||
|
||||
private bool TryReadWord(ulong address, out uint word)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Pipelines;
|
||||
|
||||
// A compute program that stores one dword, read from a constant buffer, into every
|
||||
// record of a buffer: the element index is group * 64 + thread.
|
||||
// v_lshl_add_u32 v0, s<group>, 6, v0
|
||||
// s_buffer_load_dword s<value>, s[<source>:+3], 0
|
||||
// s_waitcnt
|
||||
// v_mov_b32 v1, s<value>
|
||||
// buffer_store_format_x v1, v0, s[<destination>:+3], 0 idxen
|
||||
// s_endpgm
|
||||
// Unreal clears DCC metadata this way, so the dispatch must reach the image cache as a
|
||||
// clear; the metadata is not emulated and the stores alone would leave the image stale.
|
||||
public sealed record ConstantFill(uint GroupScalarRegister, uint DestinationScalarResource, uint SourceScalarResource);
|
||||
|
||||
public static class ConstantFillDetector
|
||||
{
|
||||
private const uint Shift64 = 134; // inline constant 6
|
||||
private const uint ZeroConstant = 128;
|
||||
private const uint NullScalar = 125;
|
||||
|
||||
public static ConstantFill? Detect(Gen5ShaderProgram program)
|
||||
{
|
||||
var instructions = program.Instructions.Where(instruction => instruction.Opcode != "SWaitcnt").ToArray();
|
||||
if (instructions.Length != 5)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var index = instructions[0];
|
||||
if (index.Opcode != "VLshlAddU32" || index.Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } indexRegister] ||
|
||||
index.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } group, { Kind: Gen5OperandKind.EncodedConstant, Value: Shift64 },
|
||||
{ Kind: Gen5OperandKind.VectorRegister } thread] || thread.Value != indexRegister.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var load = instructions[1];
|
||||
if (load.Opcode != "SBufferLoadDword" || load.Control is not Gen5ScalarMemoryControl { ImmediateOffsetBytes: 0, DynamicOffsetRegister: null } ||
|
||||
load.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } value] ||
|
||||
load.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } source, { Kind: Gen5OperandKind.EncodedConstant, Value: NullScalar or ZeroConstant }])
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var move = instructions[2];
|
||||
if (move.Opcode != "VMovB32" || move.Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } data] ||
|
||||
move.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } moved] || moved.Value != value.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var store = instructions[3];
|
||||
if (store.Opcode != "BufferStoreFormatX" ||
|
||||
store.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } control ||
|
||||
control.VectorAddress != indexRegister.Value || control.VectorData != data.Value ||
|
||||
store.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }])
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return instructions[4].Opcode == "SEndpgm" ? new ConstantFill(group.Value, control.ScalarResource, source.Value) : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,9 @@ internal interface IShaderPipelineHost
|
|||
|
||||
bool GraphicsSubgroupOperationsEnabled { get; }
|
||||
|
||||
// The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be
|
||||
// emitted as real 64-bit atomics instead of a non-atomic 32-bit pair.
|
||||
bool SharedInt64AtomicsEnabled { get; }
|
||||
bool PerVertexPixelInputsSupported => true;
|
||||
|
||||
RenderHostLimits Limits { get; }
|
||||
|
|
@ -53,6 +56,11 @@ internal interface IShaderPipelineHost
|
|||
// Reads one guest dword only when no GPU work may still own the range.
|
||||
bool TryReadCleanGuestWord(ulong address, out uint word);
|
||||
|
||||
// Copies guest bytes the CPU already holds, without synchronizing. False when the GPU
|
||||
// may own the range (or, for a clean read, when a clean word read would be refused);
|
||||
// the resource cache then re-materializes instead of trusting a stale copy.
|
||||
bool TryReadResidentGuestBytes(ulong address, Span<byte> destination, bool clean) => false;
|
||||
|
||||
// Creates the host module of one compiled permutation and returns its handle.
|
||||
ulong CreateShaderModule(IGuestCompiledShader shader, ShaderStage stage, ulong hash, ulong programId);
|
||||
|
||||
|
|
|
|||
|
|
@ -43,19 +43,44 @@ public static class PipelineCacheSignature
|
|||
public static bool TryUnwrap(string signature, ReadOnlySpan<byte> file, out byte[] payload)
|
||||
{
|
||||
payload = [];
|
||||
var prefix = Encoding.ASCII.GetBytes(signature);
|
||||
if (file.Length < prefix.Length + sizeof(ulong) || file.Length > int.MaxValue)
|
||||
if (file.Length > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file[..prefix.Length].SequenceEqual(prefix))
|
||||
var newline = file.IndexOf((byte)'\n');
|
||||
if (newline < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedHash = BinaryPrimitives.ReadUInt64LittleEndian(file[prefix.Length..]);
|
||||
var data = file[(prefix.Length + sizeof(ulong))..];
|
||||
var actualSignature = Encoding.ASCII.GetString(file[..(newline + 1)]);
|
||||
if (!string.Equals(actualSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
// Pipeline binaries are keyed by the Vulkan device and driver;
|
||||
// the SharpEmu build version only controls which shader records
|
||||
// may be present. A newer build can safely reuse an older driver
|
||||
// cache because Vulkan validates each pipeline key and ignores
|
||||
// stale records. Keep the cache when the hardware suffix matches.
|
||||
var expectedParts = signature.TrimEnd('\n').Split(':');
|
||||
var actualParts = actualSignature.TrimEnd('\n').Split(':');
|
||||
if (expectedParts.Length != actualParts.Length ||
|
||||
expectedParts.Length < 6 ||
|
||||
!string.Equals(expectedParts[0], actualParts[0], StringComparison.Ordinal) ||
|
||||
!expectedParts[2..].SequenceEqual(actualParts[2..], StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var hashOffset = newline + 1;
|
||||
if (file.Length < hashOffset + sizeof(ulong))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedHash = BinaryPrimitives.ReadUInt64LittleEndian(file[hashOffset..]);
|
||||
var data = file[(hashOffset + sizeof(ulong))..];
|
||||
if (XxHash3.HashToUInt64(data) != expectedHash)
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal static class ShaderPermutationDump
|
|||
private static long _sequence;
|
||||
|
||||
public static Action<IndirectImageFailure>? CreateFailureCapture(ShaderSource source) =>
|
||||
CompiledShaderDump.ShouldWrite(source.Address) ? CreateEnabledFailureCapture(source) : null;
|
||||
CompiledShaderDump.ShouldWrite(source.Address, source.Hash) ? CreateEnabledFailureCapture(source) : null;
|
||||
|
||||
// Keep captured shader state out of the disabled per-draw path.
|
||||
private static Action<IndirectImageFailure> CreateEnabledFailureCapture(ShaderSource source) =>
|
||||
|
|
@ -32,7 +32,7 @@ internal static class ShaderPermutationDump
|
|||
uint pushDataCursor,
|
||||
ulong programId)
|
||||
{
|
||||
if (!CompiledShaderDump.ShouldWrite(source.Address)) return null;
|
||||
if (!CompiledShaderDump.ShouldWrite(source.Address, source.Hash)) return null;
|
||||
try
|
||||
{
|
||||
var sequence = Interlocked.Increment(ref _sequence);
|
||||
|
|
@ -90,7 +90,7 @@ internal static class ShaderPermutationDump
|
|||
|
||||
public static void WriteIndirectImageFailure(ShaderSource source, IndirectImageFailure failure)
|
||||
{
|
||||
if (!CompiledShaderDump.ShouldWrite(source.Address)) return;
|
||||
if (!CompiledShaderDump.ShouldWrite(source.Address, source.Hash)) return;
|
||||
try
|
||||
{
|
||||
var sequence = Interlocked.Increment(ref _sequence);
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
RenderTrace.Write(
|
||||
$"PipelineCache create graphics vertex=0x{vertexProgram.Id:X16} pixel=0x{key.PixelProgramId:X16} colors={key.Rendering.ColorCount} " +
|
||||
$"depth={description.StaticParameters.WithDepth} samples={description.StaticParameters.Samples}");
|
||||
|
||||
}
|
||||
|
||||
var created = _host.CreateGraphicsPipeline(description);
|
||||
|
|
@ -443,7 +444,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
// host output would otherwise write an undefined value (e.g. depth-only passes that
|
||||
// leave a color target bound and export only to the null target).
|
||||
var exported = pixelStage is null || ((pixelStage.PixelColorExportMasks >> (int)(color.Slot * 4)) & 0xFu) != 0;
|
||||
parameters.SetColorMask(index, exported ? color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)) : 0);
|
||||
var colorMask = exported ? color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)) : 0;
|
||||
parameters.SetColorMask(index, colorMask);
|
||||
if (RenderTrace.Enabled && RenderTrace.Pipeline())
|
||||
{
|
||||
RenderTrace.Write(
|
||||
$"PipelineCache output slot={color.Slot} guestMask=0x{context.RenderTargetMaskForSlot(color.Slot):X} " +
|
||||
$"exported={(exported ? 1 : 0)} shaderMask=0x{pixelStage?.PixelColorExportMasks ?? 0:X8} " +
|
||||
$"mapping=0x{color.Resolution.ExportMapping.Packed:X2} hostMask=0x{colorMask:X} format={(int)format}");
|
||||
}
|
||||
}
|
||||
|
||||
var withDepth = depth.HasTarget;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ internal static class ShaderPlanningDump
|
|||
|
||||
private static void Write(ShaderSource source, string suffix, Action<TextWriter> write)
|
||||
{
|
||||
if (!CompiledShaderDump.ShouldWrite(source.Address)) return;
|
||||
if (!CompiledShaderDump.ShouldWrite(source.Address, source.Hash)) return;
|
||||
try
|
||||
{
|
||||
var path = $"{CompiledShaderDump.GetBasePath(source.Label, source.Address, source.Hash)}.{suffix}";
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ internal sealed class ProgramSourceEntry
|
|||
public required ShaderResourcePlan Plan { get; init; }
|
||||
public required Gen5ShaderProgram Program { get; init; }
|
||||
public required bool HasBitwiseExclusiveOr { get; init; }
|
||||
public ConstantFill? ConstantFill { get; init; }
|
||||
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
|
||||
public ShaderVertexInput[] VertexInputs { get; init; } = [];
|
||||
public List<ProgramPermutation> Permutations { get; } = new(8);
|
||||
|
|
@ -102,8 +103,14 @@ internal sealed class ShaderProgramCache
|
|||
private readonly Dictionary<ProgramKey, ProgramSourceEntry> _programs = new();
|
||||
private readonly Dictionary<(ulong Hash, uint CodeSize), Gen5ShaderProgram> _decoded = new();
|
||||
private readonly List<uint> _staticState = new(StageStaticKey.MaxWords);
|
||||
// Draws that re-bind unchanged resources reuse the last materialization.
|
||||
// SHARPEMU_RESOURCE_CACHE=0 materializes every draw, for A/B comparisons.
|
||||
private readonly ResourceMaterializationCache? _materializations =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_RESOURCE_CACHE") == "0" ? null : new();
|
||||
private ulong _nextProgramId;
|
||||
|
||||
public ResourceMaterializationCache? Materializations => _materializations;
|
||||
|
||||
public ShaderProgramCache(CpuContext context, IGuestGpuBackend compiler, IShaderPipelineHost host)
|
||||
{
|
||||
_context = context;
|
||||
|
|
@ -211,9 +218,15 @@ internal sealed class ShaderProgramCache
|
|||
var captureIndirectImageFailure = ShaderPermutationDump.CreateFailureCapture(source);
|
||||
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ResourceMaterialization))
|
||||
{
|
||||
if (!ResourceMaterializer.Materialize(entry.Plan, inputs, ref snapshot, ref specialization, out var materializationFailure, captureIndirectImageFailure))
|
||||
// Failure capture needs the full walk, so a dump run bypasses the cache.
|
||||
var materialized = _materializations is not null && captureIndirectImageFailure is null
|
||||
? _materializations.Materialize(entry.Plan, inputs, _host.TryReadResidentGuestBytes, ref snapshot, ref specialization,
|
||||
out var materializationFailure)
|
||||
: ResourceMaterializer.Materialize(entry.Plan, inputs, ref snapshot, ref specialization, out materializationFailure,
|
||||
captureIndirectImageFailure);
|
||||
if (!materialized)
|
||||
{
|
||||
var message = $"The shader resources could not be materialized: stage={source.Label} hash=0x{source.Hash:X16} shader=0x{source.Address:X16}.";
|
||||
var message = $"The shader resources could not be materialized: stage={source.Label} hash=0x{source.Hash:X16} shader=0x{source.Address:X16} reason={materializationFailure}.";
|
||||
if (materializationFailure is ResourceMaterializationFailure.IncompatibleImageCandidates or ResourceMaterializationFailure.ImageCapacityExceeded)
|
||||
throw new ShaderProgramRejectedException(message);
|
||||
throw SubmissionScheduler.Fatal(message);
|
||||
|
|
@ -283,7 +296,7 @@ internal sealed class ShaderProgramCache
|
|||
private ProgramSourceEntry CreateEntry(ShaderSource source, StageCompileOptions options)
|
||||
{
|
||||
var program = Decode(source);
|
||||
var dumpPlanning = CompiledShaderDump.ShouldWrite(source.Address);
|
||||
var dumpPlanning = CompiledShaderDump.ShouldWrite(source.Address, source.Hash);
|
||||
if (dumpPlanning) ShaderPlanningDump.WriteInput(source, program);
|
||||
EmbeddedVertexFetchPlan? fetch = null;
|
||||
ShaderVertexInput[] vertexInputs = [];
|
||||
|
|
@ -326,6 +339,7 @@ internal sealed class ShaderProgramCache
|
|||
Plan = plan,
|
||||
Program = program,
|
||||
HasBitwiseExclusiveOr = exclusiveOr,
|
||||
ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
|
||||
EmbeddedFetch = fetch,
|
||||
VertexInputs = vertexInputs,
|
||||
};
|
||||
|
|
@ -521,6 +535,7 @@ internal sealed class ShaderProgramCache
|
|||
BindingLayout layout)
|
||||
{
|
||||
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
|
||||
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
|
||||
switch (source.Stage)
|
||||
{
|
||||
case ShaderStage.Vertex:
|
||||
|
|
@ -532,6 +547,7 @@ internal sealed class ShaderProgramCache
|
|||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
|
|
@ -557,6 +573,7 @@ internal sealed class ShaderProgramCache
|
|||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
|
|
@ -574,6 +591,7 @@ internal sealed class ShaderProgramCache
|
|||
WaveSize = info.WaveSize,
|
||||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
ScratchDwords = info.ScratchDwords,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ComputeSystemRegisters = options.ComputeSystemRegisters,
|
||||
LocalSizeX = Math.Max(info.ThreadsX, 1),
|
||||
LocalSizeY = Math.Max(info.ThreadsY, 1),
|
||||
|
|
@ -637,6 +655,7 @@ internal sealed class ShaderProgramCache
|
|||
InstanceOffsetScalarRegister = entry.EmbeddedFetch?.InstanceOffsetScalarRegister ?? ShaderProgramInfo.NoScalarRegister,
|
||||
UsesDeviceAddresses = info.UsesDeviceAddresses,
|
||||
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
|
||||
ConstantFill = entry.ConstantFill,
|
||||
Buffers = buffers,
|
||||
Images = images,
|
||||
SamplerCount = info.Samplers.Count,
|
||||
|
|
@ -652,7 +671,7 @@ internal sealed class ShaderProgramCache
|
|||
// Writes each compiled module and its decoded listing when the dump switch is on.
|
||||
internal static class CompiledShaderDump
|
||||
{
|
||||
internal static bool ShouldWrite(ulong shaderAddress)
|
||||
internal static bool ShouldWrite(ulong shaderAddress, ulong shaderHash)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
|
|
@ -675,6 +694,27 @@ internal static class CompiledShaderDump
|
|||
}
|
||||
}
|
||||
|
||||
var hashFilter = Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV_HASH");
|
||||
if (!string.IsNullOrWhiteSpace(hashFilter))
|
||||
{
|
||||
foreach (var filter in hashFilter.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var span = filter.AsSpan();
|
||||
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
span = span[2..];
|
||||
}
|
||||
|
||||
if (ulong.TryParse(span, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var filteredHash) &&
|
||||
shaderHash == filteredHash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -692,7 +732,7 @@ internal static class CompiledShaderDump
|
|||
|
||||
public static void Write(string stage, ulong shaderAddress, ulong hash, IGuestCompiledShader shader, Gen5ShaderProgram program)
|
||||
{
|
||||
if (shader.Payload.Length == 0 || !ShouldWrite(shaderAddress)) return;
|
||||
if (shader.Payload.Length == 0 || !ShouldWrite(shaderAddress, hash)) return;
|
||||
var basePath = GetBasePath(stage, shaderAddress, hash);
|
||||
File.WriteAllBytes($"{basePath}.{shader.PayloadFileExtension}", shader.Payload);
|
||||
var lines = new List<string>(program.Instructions.Count + 2)
|
||||
|
|
|
|||
|
|
@ -41,13 +41,17 @@ public static class VertexInputResolver
|
|||
|
||||
var attributes = new List<VertexAttributeResource>();
|
||||
VertexInputBuffer[] buffers = [];
|
||||
if (metadata.VertexAttributeRegister >= 0)
|
||||
// A shader without input semantics never reads its vertex tables, which may be null.
|
||||
if (metadata.VertexAttributeRegister >= 0 && metadata.InputSemantics.Length != 0)
|
||||
{
|
||||
var attributeTable = ReadTablePointer(userData, metadata.VertexAttributeRegister);
|
||||
var bufferTable = ReadTablePointer(userData, metadata.VertexBufferRegister);
|
||||
if (attributeTable == 0 || bufferTable == 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"The vertex table pointer is null: shader=0x{shader.CodeAddress:X16} attributes=0x{attributeTable:X16} buffers=0x{bufferTable:X16}.");
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The vertex table pointer is null: shader=0x{shader.CodeAddress:X16} attributes=0x{attributeTable:X16} buffers=0x{bufferTable:X16} " +
|
||||
$"attributeRegister={metadata.VertexAttributeRegister} bufferRegister={metadata.VertexBufferRegister} " +
|
||||
$"user_data=[{string.Join(",", userData.ToArray().Select(word => $"{word:X8}"))}].");
|
||||
}
|
||||
|
||||
attributes = ApplySemantics(context, metadata.InputSemantics, attributeTable, bufferTable, shader.CodeAddress);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
using ResourceSnapshot = SharpEmu.ShaderCompiler.Resources.ResourceSnapshot;
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ public sealed partial class RenderExecutor
|
|||
private const uint DispatchInitiatorKnownMask = DispatchInitiatorBaseBits | DispatchInitiatorModifierBits;
|
||||
private const uint ImageClearDispatchInitiator = 0x61;
|
||||
private const uint ImageClearWaveSize = 64;
|
||||
private const uint Format32UInt = 20;
|
||||
private const uint ImageClearStride = 16;
|
||||
private const uint ImageClearUserDataCount = 8;
|
||||
|
||||
|
|
@ -82,6 +84,12 @@ public sealed partial class RenderExecutor
|
|||
return;
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 && TryConsumeConstantFill(input, groupsX, groupsY, groupsZ))
|
||||
{
|
||||
_host.ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||
{
|
||||
_host.ResetBindings();
|
||||
|
|
@ -143,9 +151,25 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
_host.BindPipeline(PipelineBindPoint.Compute, in pipeline);
|
||||
// The shader's local workgroup axes may have been remapped at compile time
|
||||
// (see Gen5SpirvTranslator.ComputeWorkgroupAxisOrder) so the largest NUM_THREAD
|
||||
// axis lands on a physical axis Vulkan actually allows it on. The dispatch group
|
||||
// counts must be permuted the same way, or vkCmdDispatch would hand group counts
|
||||
// for the wrong physical axis to the pipeline it built with the remapped sizes.
|
||||
var axisOrder = Gen5SpirvTranslator.ComputeWorkgroupAxisOrder(
|
||||
input.ThreadsX,
|
||||
input.ThreadsY,
|
||||
input.ThreadsZ);
|
||||
var logicalGroups = new[] { groupsX, groupsY, groupsZ };
|
||||
var physicalGroups = new uint[3];
|
||||
for (var logical = 0; logical < 3; logical++)
|
||||
{
|
||||
physicalGroups[axisOrder[logical]] = logicalGroups[logical];
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 || !_host.TryDispatchIndirect(indirectArgumentsAddress))
|
||||
{
|
||||
_host.Dispatch(groupsX, groupsY, groupsZ);
|
||||
_host.Dispatch(physicalGroups[0], physicalGroups[1], physicalGroups[2]);
|
||||
}
|
||||
_host.ShaderAccessBarrier();
|
||||
}
|
||||
|
|
@ -276,6 +300,46 @@ public sealed partial class RenderExecutor
|
|||
return new ComputeImageClear(descriptor, clear, size);
|
||||
}
|
||||
|
||||
// A constant fill that covers a whole image or DCC metadata becomes a clear. Anything
|
||||
// else, including a fill of plain buffer memory, runs as the guest wrote it.
|
||||
private bool TryConsumeConstantFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ)
|
||||
{
|
||||
var program = input.Stage.Program!;
|
||||
if (program.ConstantFill is not { } fill || program.UserDataBase != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var userData = input.Stage.Resources.UserData;
|
||||
if (fill.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
|
||||
input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 || groupsY != 1 || groupsZ != 1 ||
|
||||
fill.DestinationScalarResource + 4 > userData.Length || fill.SourceScalarResource + 4 > userData.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destination = BufferDescriptorWords.From(userData.AsSpan((int)fill.DestinationScalarResource, 4).ToArray());
|
||||
var source = BufferDescriptorWords.From(userData.AsSpan((int)fill.SourceScalarResource, 4).ToArray());
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(uint)];
|
||||
if (destination.Format != Format32UInt || destination.Stride != sizeof(uint) || destination.SwizzleEnabled || destination.AddThreadId ||
|
||||
(ulong)groupsX * input.ThreadsX != destination.RecordCount || source.RecordCount == 0 ||
|
||||
!_host.TryReadGuest(source.Address, valueBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(valueBytes);
|
||||
var size = (ulong)destination.RecordCount * sizeof(uint);
|
||||
var consumed = _host.TryClearImageFromBuffer(destination.Address, size, value) ||
|
||||
_host.TryAbsorbDccFill(destination.Address, size, value);
|
||||
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
|
||||
{
|
||||
RenderTrace.Write($"Constant fill: shader=0x{program.Hash:X16} address=0x{destination.Address:X16} size=0x{size:X} value=0x{value:X8} consumed={consumed}");
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
private bool TryConsumeImageClear(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
|
||||
{
|
||||
if (TryDecodeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator) is not { } clear)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,16 @@ public enum GuestIndexType : uint
|
|||
// Resolves draw and dispatch state from the register banks and records it through the host.
|
||||
public sealed partial class RenderExecutor
|
||||
{
|
||||
private const uint PrimitiveShaderStageMask = 0x02002000;
|
||||
// Prospero uses both the full NGG mask and compact primitive-shader masks
|
||||
// when the task/mesh front end has already folded the upper enable bits
|
||||
// away. Little Nightmares also emits 0x2030 for a primitive path with no
|
||||
// geometry shader; the remaining bits describe the fixed NGG setup.
|
||||
// GS_W32_EN (bit 22) and VS_W32_EN (bit 23) only select the wave size;
|
||||
// graphics stages always compile as wave32, so they do not change the path.
|
||||
private const uint VgtShaderStagesWaveSizeBits = (1u << 22) | (1u << 23);
|
||||
|
||||
private static bool IsPrimitiveShaderStageMask(uint stages) =>
|
||||
(stages & ~VgtShaderStagesWaveSizeBits) is 0x02002000 or 0x00002000 or 0x00002030;
|
||||
private const uint MaxOutputPerSubgroupLimit = 0x40;
|
||||
private static int _geometryWarningShown;
|
||||
|
||||
|
|
@ -400,15 +409,16 @@ public sealed partial class RenderExecutor
|
|||
var vertex = banks.Shader.Vertex;
|
||||
var stages = context.ShaderStages;
|
||||
var primitiveShaderVertexPath =
|
||||
stages == PrimitiveShaderStageMask && vertex.ExportAddress != 0 &&
|
||||
shaderInterface.GeometryMaxVerticesOut == 0 && IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType);
|
||||
var unsupportedStageMask = stages != 0 && stages != PrimitiveShaderStageMask;
|
||||
IsPrimitiveShaderStageMask(stages) && vertex.ExportAddress != 0 &&
|
||||
(vertex.GeometryAddress == 0 ||
|
||||
(shaderInterface.GeometryMaxVerticesOut == 0 && IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType)));
|
||||
var unsupportedStageMask = stages != 0 && !IsPrimitiveShaderStageMask(stages);
|
||||
var unsupportedGeometryStage = vertex.ExportAddress != 0 && vertex.GeometryAddress != 0 && !primitiveShaderVertexPath;
|
||||
var geometryRegisters =
|
||||
(shaderInterface.PrimitiveShaderSubgroupControl != 0 && shaderInterface.PrimitiveShaderSubgroupControl != 1) ||
|
||||
shaderInterface.GeometryMaxVerticesOut != 0 ||
|
||||
!IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType) ||
|
||||
shaderInterface.MaxOutputPerSubgroup > MaxOutputPerSubgroupLimit;
|
||||
var geometryRegisters = !primitiveShaderVertexPath &&
|
||||
((shaderInterface.PrimitiveShaderSubgroupControl != 0 && shaderInterface.PrimitiveShaderSubgroupControl != 1) ||
|
||||
shaderInterface.GeometryMaxVerticesOut != 0 ||
|
||||
!IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType) ||
|
||||
shaderInterface.MaxOutputPerSubgroup > MaxOutputPerSubgroupLimit);
|
||||
if (!unsupportedStageMask && !unsupportedGeometryStage && !geometryRegisters)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -416,7 +426,15 @@ public sealed partial class RenderExecutor
|
|||
|
||||
if (Interlocked.Exchange(ref _geometryWarningShown, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine("Warning: the title uses unsupported graphics pipelines; some draw calls were skipped.");
|
||||
Console.Error.WriteLine(
|
||||
"Warning: the title uses unsupported graphics pipelines; some draw calls were skipped. " +
|
||||
$"stages=0x{stages:X8} subgroup=0x{shaderInterface.PrimitiveShaderSubgroupControl:X8} " +
|
||||
$"maxOutput=0x{shaderInterface.MaxOutputPerSubgroup:X8} " +
|
||||
$"maxVerticesOut=0x{shaderInterface.GeometryMaxVerticesOut:X8} " +
|
||||
$"outputPrimitive=0x{shaderInterface.GeometryOutputPrimitiveType:X8} " +
|
||||
$"export=0x{vertex.ExportAddress:X16} geometry=0x{vertex.GeometryAddress:X16} " +
|
||||
$"unsupportedMask={unsupportedStageMask} unsupportedGeometry={unsupportedGeometryStage} " +
|
||||
$"unsupportedRegisters={geometryRegisters}.");
|
||||
}
|
||||
|
||||
if (RenderTrace.Enabled && RenderTrace.GeometryStageSkip())
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public class ShaderProgramInfo
|
|||
public int InstanceOffsetScalarRegister { get; init; } = NoScalarRegister;
|
||||
public bool UsesDeviceAddresses { get; init; }
|
||||
public bool HasBitwiseExclusiveOr { get; init; }
|
||||
public Pipelines.ConstantFill? ConstantFill { get; init; }
|
||||
public BufferResourceInfo[] Buffers { get; init; } = [];
|
||||
public ImageResourceInfo[] Images { get; init; } = [];
|
||||
public int SamplerCount { get; init; }
|
||||
|
|
|
|||
|
|
@ -33,6 +33,24 @@ public static class ImeExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VkqLPArfFdc",
|
||||
ExportName = "sceImeKeyboardGetInfo",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardGetInfo(CpuContext ctx)
|
||||
{
|
||||
var infoAddress = ctx[CpuRegister.Rsi];
|
||||
if (infoAddress != 0)
|
||||
{
|
||||
Span<byte> info = stackalloc byte[0x88];
|
||||
_ = ctx.Memory.TryWrite(infoAddress, info);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dKadqZFgKKQ",
|
||||
ExportName = "sceImeKeyboardGetResourceId",
|
||||
|
|
|
|||
|
|
@ -60,8 +60,16 @@ public static class KernelEventFlagCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
// Event flag names are commonly stored in executable/read-only image
|
||||
// pages. Use the kernel compatibility reader so those pages can use
|
||||
// its guarded host-read fallback when the guest mapping's protection
|
||||
// metadata temporarily rejects a normal TryRead call.
|
||||
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(
|
||||
ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] event_flag name read failed address=0x{nameAddress:X16} " +
|
||||
$"range={ctx.Memory.DescribeReadRange(nameAddress, (ulong)(MaxEventFlagNameLength + 1))}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +86,11 @@ public static class KernelEventFlagCompatExports
|
|||
Bits = initialPattern,
|
||||
};
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, outAddress, handle))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] event_flag handle write failed address=0x{outAddress:X16} " +
|
||||
$"range={ctx.Memory.DescribeReadRange(outAddress, sizeof(ulong))}");
|
||||
_eventFlags.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public static class KernelExports
|
|||
private static readonly object _cxaGate = new();
|
||||
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||
private static readonly object _coredumpGate = new();
|
||||
private const string RepeatedGuestAssertion = "Assertion failed: 0";
|
||||
private static int _repeatedGuestAssertionCount;
|
||||
private static ulong _coredumpHandler;
|
||||
private static ulong _coredumpHandlerContext;
|
||||
|
||||
|
|
@ -30,7 +32,8 @@ public static class KernelExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
|
||||
// KytyPS5 libKernel.cpp KernelGetOperationMode: *mode = 2 (PS5 base console, not Pro),
|
||||
// *submode = 0 (none). Games poll this at high frequency.
|
||||
[SysAbiExport(
|
||||
Nid = "NH6xARDOVv8",
|
||||
ExportName = "sceKernelGetOperationMode",
|
||||
|
|
@ -38,8 +41,19 @@ public static class KernelExports
|
|||
LibraryName = "libKernel")]
|
||||
public static int KernelGetOperationMode(CpuContext ctx)
|
||||
{
|
||||
// SCE_KERNEL_MODE_RELEASE = 0. Reporting retail/release mode keeps guest
|
||||
// code from taking devkit/tool-only branches we do not emulate.
|
||||
var modeAddress = ctx[CpuRegister.Rdi];
|
||||
var submodeAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (modeAddress != 0 && !ctx.TryWriteUInt32(modeAddress, 2))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (submodeAddress != 0 && !ctx.TryWriteUInt32(submodeAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
|
@ -223,7 +237,8 @@ public static class KernelExports
|
|||
ctx,
|
||||
attrAddress,
|
||||
out var priority,
|
||||
out var affinityMask);
|
||||
out var affinityMask,
|
||||
out var requestedStackSize);
|
||||
KernelPthreadExtendedCompatExports.RegisterThreadStart(
|
||||
threadHandle,
|
||||
name,
|
||||
|
|
@ -252,7 +267,8 @@ public static class KernelExports
|
|||
attrAddress,
|
||||
name,
|
||||
priority,
|
||||
affinityMask);
|
||||
affinityMask,
|
||||
requestedStackSize);
|
||||
if (!scheduler.TryStartThread(ctx, request, out var error))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -402,19 +418,42 @@ public static class KernelExports
|
|||
ulong fmtPtr = ctx[CpuRegister.Rdi];
|
||||
string fmt = ReadCString(ctx, fmtPtr, 4096);
|
||||
string outStr = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, fmt, firstGpArgIndex: 1);
|
||||
if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
|
||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(outStr);
|
||||
if (ShouldWriteGuestPrintf(outStr))
|
||||
{
|
||||
Console.Write($"[DEBUG][PRINF] {outStr}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
|
||||
if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
|
||||
{
|
||||
Console.Write($"[DEBUG][PRINF] {outStr}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = (ulong)System.Text.Encoding.UTF8.GetByteCount(outStr);
|
||||
ctx[CpuRegister.Rax] = (ulong)byteCount;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Demon\'s Souls can print this same guest-side diagnostic thousands of times during
|
||||
// bootstrap. Printing it does not convey new state and serializes the emulation threads,
|
||||
// but printf must still report its original byte count to the guest.
|
||||
private static bool ShouldWriteGuestPrintf(string text)
|
||||
{
|
||||
if (!string.Equals(text.TrimEnd('\r', '\n'), RepeatedGuestAssertion, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var count = Interlocked.Increment(ref _repeatedGuestAssertionCount);
|
||||
if (count == 2)
|
||||
{
|
||||
Console.WriteLine("[DEBUG][PRINF] Further repeated 'Assertion failed: 0' messages are suppressed.");
|
||||
}
|
||||
|
||||
return count == 1;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EMutwaQ34Jo",
|
||||
ExportName = "perror",
|
||||
|
|
@ -521,4 +560,5 @@ public static class KernelExports
|
|||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,16 @@ public static partial class KernelMemoryCompatExports
|
|||
private const int MemoryAccessDenied = unchecked((int)0x8002000D);
|
||||
private const int MemoryFault = unchecked((int)0x8002000E);
|
||||
private const int MemoryInvalidArgument = unchecked((int)0x80020016);
|
||||
// Titles use sceKernelReserveVirtualRange as an address-space reservation
|
||||
// primitive. Some of them ask for hundreds of GiB even though only small
|
||||
// portions are ever mapped. Windows cannot always create one contiguous
|
||||
// placeholder that large after the executable and runtime are loaded.
|
||||
private const ulong SparseReservationThreshold = 0x10_0000_0000UL;
|
||||
// macOS reserves 0x10_0000_0000..0x70_0000_0000 for graphics memory (see
|
||||
// DefaultMapSearchBase), so sparse reservations start above that window there.
|
||||
private static ulong SparseReservationBase =>
|
||||
OperatingSystem.IsMacOS() ? 0x70_0000_0000UL : 0x40_0000_0000UL;
|
||||
private const ulong UserAddressLimit = 0xFC_0000_0000UL;
|
||||
private static FlexibleBackingPool _flexibleBacking =
|
||||
new(GuestMemoryLayout.FlexibleOffset, GuestMemoryLayout.FlexibleBytes);
|
||||
private static IGuestBackedSpace? _backingOwner;
|
||||
|
|
@ -249,7 +259,9 @@ public static partial class KernelMemoryCompatExports
|
|||
{
|
||||
if (requested >= 0x10_0000_0000UL && requested < 0xFC_0000_0000UL &&
|
||||
Interlocked.Exchange(ref _prtMapWarning, 1) == 0)
|
||||
Console.Error.WriteLine("[LOADER][WARN] Shared mapping cannot replace the reserved aperture.");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Shared mapping cannot replace the reserved aperture: address=0x{requested:X16} size=0x{length:X} flags=0x{flags:X} " +
|
||||
$"regions=[{string.Join(",", GetMappingSlices(requested, length).Select(region => $"0x{region.Address:X}+0x{region.Length:X}:{(region.IsReserved ? "reserved" : region.IsDirect ? "direct" : region.IsFlexible ? "flexible" : "other")}:backed={space.IsBackedView(region.Address)}"))}].");
|
||||
return false;
|
||||
}
|
||||
address = requested;
|
||||
|
|
@ -335,7 +347,10 @@ public static partial class KernelMemoryCompatExports
|
|||
if (pointer == 0 || !IsValidMapRange(length, alignment))
|
||||
return MemoryInvalidArgument;
|
||||
if (!ctx.TryReadUInt64(pointer, out var requested))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] reserve_virtual pointer read failed pointer=0x{pointer:X16} size=0x{length:X}");
|
||||
return MemoryFault;
|
||||
}
|
||||
alignment = alignment == 0 ? OrbisPageSize : alignment;
|
||||
if ((flags & OrbisKernelMapFixed) != 0 &&
|
||||
(requested == 0 || !IsAligned(requested, OrbisPageSize) || requested > ulong.MaxValue - length))
|
||||
|
|
@ -343,15 +358,107 @@ public static partial class KernelMemoryCompatExports
|
|||
lock (_memoryGate)
|
||||
{
|
||||
var space = ResolveBackingSpace(ctx);
|
||||
if (space is null || !TrySelectBackingAddress(space, requested, length, alignment, flags, out var address, reuseReservation: false))
|
||||
var address = 0UL;
|
||||
var sparseSelected = (flags & OrbisKernelMapFixed) == 0 &&
|
||||
TrySelectSparseReservationAddress(requested, length, alignment, flags, out address);
|
||||
var selected = sparseSelected;
|
||||
if (!selected && space is not null)
|
||||
{
|
||||
selected = TrySelectBackingAddress(space, requested, length, alignment, flags,
|
||||
out address, reuseReservation: false);
|
||||
}
|
||||
|
||||
// A sparse reservation is only a guest mapping-table entry and
|
||||
// intentionally does not need a host backing space yet. The host
|
||||
// range is acquired later when a subrange is committed or mapped.
|
||||
if (!selected || (!sparseSelected && space is null))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] reserve_virtual address selection failed requested=0x{requested:X16} size=0x{length:X} sparse={sparseSelected} backing={space is not null}");
|
||||
return MemoryNoSpace;
|
||||
}
|
||||
if (ShouldTraceDirectMemory())
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual selected address=0x{address:X16} requested=0x{requested:X16} size=0x{length:X} sparse={sparseSelected}");
|
||||
ReplaceMappedRegionRangeLocked(new MappedRegion(address, length, 0, false, false, 0, IsReserved: true));
|
||||
if (ShouldTraceDirectMemory())
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual address=0x{address:X} requested=0x{requested:X} size=0x{length:X} flags=0x{flags:X}");
|
||||
return ctx.TryWriteUInt64(pointer, address) ? 0 : MemoryFault;
|
||||
if (!ctx.TryWriteUInt64(pointer, address))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] reserve_virtual pointer write failed pointer=0x{pointer:X16} address=0x{address:X16}");
|
||||
return MemoryFault;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep very large virtual reservations in the guest mapping table without
|
||||
// requiring one equally large host placeholder. Actual backed mappings still
|
||||
// reserve their individual host ranges when they are committed below this
|
||||
// reservation. Fixed reservations never take this path: they must preserve
|
||||
// the exact host address semantics of the original request.
|
||||
private static bool TrySelectSparseReservationAddress(
|
||||
ulong requested,
|
||||
ulong length,
|
||||
ulong alignment,
|
||||
ulong flags,
|
||||
out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
if ((flags & OrbisKernelMapFixed) != 0 || length < SparseReservationThreshold ||
|
||||
length > UserAddressLimit || alignment == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidate = requested == 0
|
||||
? SparseReservationBase
|
||||
: Math.Max(requested, SparseReservationBase);
|
||||
candidate = AlignUp(candidate, alignment);
|
||||
if (candidate == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The requested address is a hint for a non-fixed reservation. If the
|
||||
// hint is too high to contain the complete range, restart from the
|
||||
// sparse aperture instead of treating the hint as a lower bound.
|
||||
if (candidate > UserAddressLimit - length)
|
||||
{
|
||||
candidate = AlignUp(SparseReservationBase, alignment);
|
||||
if (candidate == 0 || candidate > UserAddressLimit - length)
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var region in _mappedRegions.Values)
|
||||
{
|
||||
if (region.Address >= candidate + length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var regionEnd = region.Address + region.Length;
|
||||
if (regionEnd <= candidate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidate = AlignUp(regionEnd, alignment);
|
||||
if (candidate == 0 || candidate > UserAddressLimit - length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate == 0 || candidate > UserAddressLimit - length ||
|
||||
_mappedRegions.Values.Any(region =>
|
||||
region.Address < candidate + length && candidate < region.Address + region.Length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
address = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasPhysicalSpan(ulong start, ulong length)
|
||||
=> _directAllocations.ContainsAllocatedRange(start, length);
|
||||
|
||||
|
|
|
|||
|
|
@ -2064,6 +2064,13 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VAzswvTOCzI",
|
||||
ExportName = "unlink",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixUnlink(CpuContext ctx) => KernelUnlink(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1-LFLmRFxxM",
|
||||
ExportName = "sceKernelMkdir",
|
||||
|
|
@ -2281,6 +2288,16 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
// A host movie watchdog may have closed the decoder while the guest
|
||||
// still owns its Bink file descriptor. Return EOF so the guest closes
|
||||
// that descriptor and proceeds to the next movie instead of waiting
|
||||
// forever on a stale native decoder.
|
||||
if (HostMovieBridge.ShouldForceGuestMovieEof(stream.Name))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
long positionBefore;
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ public static class KernelPthreadCompatExports
|
|||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||
private static readonly Dictionary<ulong, PthreadCondAttrState> _condAttrStates = new();
|
||||
// Only these exact allocation bases are owned by the pthread compatibility
|
||||
// layer. Guest pthread objects are commonly stored inside reusable blocks;
|
||||
// never pass one of those storage addresses to TryFreeGuestMemory when a
|
||||
// stale handle is destroyed.
|
||||
private static readonly ConcurrentDictionary<ulong, byte> _opaqueAllocations = new();
|
||||
private static readonly bool _tracePthreads =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _tracePthreadConds =
|
||||
|
|
@ -283,7 +288,11 @@ public static class KernelPthreadCompatExports
|
|||
public static int PthreadYield(CpuContext ctx)
|
||||
{
|
||||
_ = ctx;
|
||||
Thread.Yield();
|
||||
if (!Thread.Yield())
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
|
|
@ -294,6 +303,13 @@ public static class KernelPthreadCompatExports
|
|||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6XG4B33N09g",
|
||||
ExportName = "sched_yield",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int SchedYield(CpuContext ctx) => PthreadYield(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GBUY7ywdULE",
|
||||
ExportName = "scePthreadRename",
|
||||
|
|
@ -1696,6 +1712,8 @@ public static class KernelPthreadCompatExports
|
|||
return false;
|
||||
}
|
||||
|
||||
_opaqueAllocations[address] = 0;
|
||||
|
||||
Span<byte> initialData = stackalloc byte[size];
|
||||
initialData.Clear();
|
||||
if (ctx.Memory.TryWrite(address, initialData))
|
||||
|
|
@ -1710,7 +1728,9 @@ public static class KernelPthreadCompatExports
|
|||
|
||||
private static void FreeOpaqueObject(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (address != 0 && ctx.Memory is IGuestMemoryAllocator allocator)
|
||||
if (address != 0 &&
|
||||
_opaqueAllocations.TryRemove(address, out _) &&
|
||||
ctx.Memory is IGuestMemoryAllocator allocator)
|
||||
{
|
||||
_ = allocator.TryFreeGuestMemory(address);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||
private const ulong NativeGuestStackSize = 0x20_0000UL;
|
||||
private const ulong NativeGuestStackStride = 0x100_0000UL;
|
||||
private const int DefaultInheritSched = 4;
|
||||
private const int DefaultSoloSched = 0;
|
||||
private const int DefaultSchedPolicy = 1;
|
||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
||||
|
|
@ -44,12 +45,14 @@ public static class KernelPthreadExtendedCompatExports
|
|||
CpuContext ctx,
|
||||
ulong attrAddress,
|
||||
out int priority,
|
||||
out ulong affinityMask)
|
||||
out ulong affinityMask,
|
||||
out ulong stackSize)
|
||||
{
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
priority = DefaultThreadPriority;
|
||||
affinityMask = DefaultThreadAffinityMask;
|
||||
stackSize = DefaultStackSize;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +62,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||
var attributes = GetOrCreateAttrStateLocked(resolvedAddress);
|
||||
priority = attributes.SchedPriority;
|
||||
affinityMask = attributes.AffinityMask;
|
||||
stackSize = attributes.StackSize;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +193,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||
ulong GuardSize,
|
||||
int InheritSched,
|
||||
int SchedPolicy,
|
||||
int SchedPriority)
|
||||
int SchedPriority,
|
||||
int SoloSched)
|
||||
{
|
||||
public static PthreadAttrState Default =>
|
||||
new(
|
||||
|
|
@ -200,7 +205,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||
DefaultGuardSize,
|
||||
DefaultInheritSched,
|
||||
DefaultSchedPolicy,
|
||||
DefaultSchedPriority);
|
||||
DefaultSchedPriority,
|
||||
DefaultSoloSched);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -860,6 +866,59 @@ public static class KernelPthreadExtendedCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Dk6FC-TI+7Q",
|
||||
ExportName = "scePthreadAttrSetsolosched",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetsolosched(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
var soloSched = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
_attrStates[attrAddress] = state with { SoloSched = soloSched };
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9RnL-m0+diQ",
|
||||
ExportName = "scePthreadAttrGetsolosched",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetsolosched(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
var outSoloSchedAddress = ctx[CpuRegister.Rsi];
|
||||
if (attrAddress == 0 || outSoloSchedAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
PthreadAttrState state;
|
||||
lock (_stateGate)
|
||||
{
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outSoloSchedAddress, state.SoloSched))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The POSIX-named alias of <see cref="PthreadAttrGetschedparam"/>. libKernel
|
||||
/// exports the same routine under two NIDs; middleware compiled against the
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ public static class KernelSemaphoreCompatExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(
|
||||
ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ internal static class HostMovieBridge
|
|||
private static byte[]? _frameBuffer;
|
||||
private static bool _frameBufferPresented;
|
||||
private static MediaFramePlayback? _playback;
|
||||
private static Timer? _playbackWatchdog;
|
||||
private static long _frameSerial;
|
||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||
|
|
@ -287,6 +288,7 @@ internal static class HostMovieBridge
|
|||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
_frameBufferPresented = false;
|
||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||
ArmPlaybackWatchdogLocked(hostPath);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + ".");
|
||||
|
|
@ -301,6 +303,53 @@ internal static class HostMovieBridge
|
|||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_playback = new MediaFramePlayback(decoder);
|
||||
ArmPlaybackWatchdogLocked(hostPath);
|
||||
}
|
||||
|
||||
private static void ArmPlaybackWatchdogLocked(string hostPath)
|
||||
{
|
||||
_playbackWatchdog?.Dispose();
|
||||
var timeout = TryReadBinkInfo(hostPath, out var info)
|
||||
? GetPlaybackWatchdogTimeout(info)
|
||||
: TimeSpan.FromSeconds(150);
|
||||
_playbackWatchdog = new Timer(
|
||||
static state =>
|
||||
{
|
||||
var path = (string)state!;
|
||||
lock (Gate)
|
||||
{
|
||||
if (!string.Equals(_activePath, path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 host watchdog expired for " +
|
||||
Path.GetFileName(path) + "; advancing to the next movie.");
|
||||
TimedOutMoviePaths.Add(path);
|
||||
CloseActiveLocked();
|
||||
AttachNextQueuedMovieLocked();
|
||||
}
|
||||
},
|
||||
hostPath,
|
||||
timeout,
|
||||
Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
// Must outlast the whole movie: Demon's Souls ships a 171 s intro and 756 s credits.
|
||||
internal static TimeSpan GetPlaybackWatchdogTimeout(Bink2MovieInfo info)
|
||||
{
|
||||
if (info.FrameCount == 0 ||
|
||||
info.FramesPerSecondNumerator == 0 ||
|
||||
info.FramesPerSecondDenominator == 0)
|
||||
{
|
||||
return TimeSpan.FromSeconds(150);
|
||||
}
|
||||
|
||||
var duration = (double)info.FrameCount *
|
||||
info.FramesPerSecondDenominator /
|
||||
info.FramesPerSecondNumerator;
|
||||
return TimeSpan.FromSeconds(Math.Max(90, duration + 60));
|
||||
}
|
||||
|
||||
internal static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||
|
|
@ -319,6 +368,7 @@ internal static class HostMovieBridge
|
|||
info = new Bink2MovieInfo(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(8, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x20, 4)));
|
||||
return info.FramesPerSecondNumerator != 0 &&
|
||||
|
|
@ -349,6 +399,8 @@ internal static class HostMovieBridge
|
|||
|
||||
private static void CloseActiveLocked()
|
||||
{
|
||||
_playbackWatchdog?.Dispose();
|
||||
_playbackWatchdog = null;
|
||||
_playback?.Dispose();
|
||||
_playback = null;
|
||||
_activePath = null;
|
||||
|
|
@ -364,6 +416,7 @@ internal static class HostMovieBridge
|
|||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal readonly struct Bink2MovieInfo
|
||||
{
|
||||
public readonly uint FrameCount;
|
||||
public readonly uint Width;
|
||||
public readonly uint Height;
|
||||
public readonly uint FramesPerSecondNumerator;
|
||||
|
|
@ -372,14 +425,25 @@ internal static class HostMovieBridge
|
|||
internal Bink2MovieInfo(
|
||||
uint width,
|
||||
uint height,
|
||||
uint frameCount,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
FrameCount = frameCount;
|
||||
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||
}
|
||||
|
||||
internal Bink2MovieInfo(
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
: this(width, height, 0, framesPerSecondNumerator, framesPerSecondDenominator)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private enum MovieMode
|
||||
|
|
@ -393,6 +457,16 @@ internal static class HostMovieBridge
|
|||
private static readonly Queue<string> PendingMoviePaths = new();
|
||||
private static readonly HashSet<string> PendingMoviePathSet =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<string> TimedOutMoviePaths =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool ShouldForceGuestMovieEof(string hostPath)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return TimedOutMoviePaths.Contains(hostPath);
|
||||
}
|
||||
}
|
||||
private static void AttachNextQueuedMovieLocked()
|
||||
{
|
||||
while (PendingMoviePaths.Count > 0)
|
||||
|
|
@ -475,6 +549,7 @@ internal static class HostMovieBridge
|
|||
{
|
||||
lock (Gate)
|
||||
{
|
||||
TimedOutMoviePaths.Remove(hostPath);
|
||||
if (PendingMoviePathSet.Remove(hostPath))
|
||||
{
|
||||
var retained = PendingMoviePaths
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ public static partial class HttpExports
|
|||
|
||||
private sealed record HttpContext(int NetMemoryId, int SslContextId, ulong PoolSize);
|
||||
|
||||
private sealed record HttpTemplate(int ContextId, ulong UserAgentAddress, int HttpVersion, bool AutoProxyConfig);
|
||||
private sealed record HttpTemplate(
|
||||
int ContextId,
|
||||
ulong UserAgentAddress,
|
||||
int HttpVersion,
|
||||
bool AutoProxyConfig,
|
||||
uint ConnectTimeoutMicroseconds = 30_000_000);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "A9cVMUtEp4Y",
|
||||
|
|
@ -78,6 +83,30 @@ public static partial class HttpExports
|
|||
: ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0S9tTH0uqTU",
|
||||
ExportName = "sceHttpSetConnectTimeOut",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceHttp")]
|
||||
public static int HttpSetConnectTimeOut(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var timeoutMicroseconds = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
if (timeoutMicroseconds == 0)
|
||||
{
|
||||
return ctx.SetReturn(HttpErrorInvalidValue);
|
||||
}
|
||||
|
||||
if (!Templates.TryGetValue(id, out var template))
|
||||
{
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
Templates[id] = template with { ConnectTimeoutMicroseconds = timeoutMicroseconds };
|
||||
TraceHttp("set_connect_timeout", id, timeoutMicroseconds, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Ik-KpLTlf7Q",
|
||||
ExportName = "sceHttpTerm",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public static class NetExports
|
|||
private const int NetErrnoAddressInUse = 48;
|
||||
private const int NetErrnoNotInitialized = 200;
|
||||
private const int MaxNameLength = 256;
|
||||
private static ReadOnlySpan<byte> OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
|
||||
|
||||
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
||||
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
||||
|
|
@ -117,6 +118,24 @@ public static class NetExports
|
|||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6Oc0bLsIYe0",
|
||||
ExportName = "sceNetGetMacAddress",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetGetMacAddress(CpuContext ctx)
|
||||
{
|
||||
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (destinationAddress == 0 || flags != 0 || !ctx.Memory.TryWrite(destinationAddress, OfflineMacAddress))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
TraceNet("get_mac_address", 0, destinationAddress, unchecked((ulong)flags), 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Q4qBuN-c0ZM",
|
||||
ExportName = "sceNetSocket",
|
||||
|
|
|
|||
|
|
@ -23,15 +23,22 @@ public static class NpEntitlementAccessExports
|
|||
|
||||
private const int NpEntitlementAccessErrorParameter = unchecked((int)0x817D0002);
|
||||
private const int NpEntitlementAccessErrorNoEntitlement = unchecked((int)0x817D0007);
|
||||
private const int EntitlementKeySize = 16;
|
||||
|
||||
// Offline add-on entitlements titles query through NpEntitlementAccess.
|
||||
// Offline entitlements queried by titles through NpEntitlementAccess.
|
||||
// GTA V Enhanced (PPSA04264) gates Story Mode on these three labels; without
|
||||
// them the frontend offers "Buy GTAV Story Mode" despite a full dump.
|
||||
// Ghost of Yotei (PPSA26344) separately checks its application and base
|
||||
// game entitlements before it creates the frontend. These represent the
|
||||
// locally installed main package only; optional editions and add-ons remain
|
||||
// unowned.
|
||||
private static readonly AddcontEntitlement[] OwnedAddcontEntitlements =
|
||||
[
|
||||
new("85y-je", PackageTypePsal, DownloadStatusInstalled),
|
||||
new("5d5c48", PackageTypePsal, DownloadStatusInstalled),
|
||||
new("_mtqu6", PackageTypePsal, DownloadStatusInstalled),
|
||||
new("GHOST2APP0000000", PackageTypePsal, DownloadStatusInstalled),
|
||||
new("GHOST2BASE000000", PackageTypePsal, DownloadStatusInstalled),
|
||||
];
|
||||
|
||||
private readonly record struct AddcontEntitlement(
|
||||
|
|
@ -193,6 +200,40 @@ public static class NpEntitlementAccessExports
|
|||
return ctx.SetReturn(NpEntitlementAccessErrorNoEntitlement);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "5LiMEPuW0DQ",
|
||||
ExportName = "sceNpEntitlementAccessGetEntitlementKey",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessGetEntitlementKey(CpuContext ctx)
|
||||
{
|
||||
var labelAddress = ctx[CpuRegister.Rsi];
|
||||
var keyAddress = ctx[CpuRegister.Rdx];
|
||||
if (labelAddress == 0 || keyAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NpEntitlementAccessErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> labelBytes = stackalloc byte[EntitlementLabelSize];
|
||||
if (!ctx.Memory.TryRead(labelAddress, labelBytes))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var label = ReadEntitlementLabel(labelBytes);
|
||||
Span<byte> key = stackalloc byte[EntitlementKeySize];
|
||||
key.Clear();
|
||||
if (!ctx.Memory.TryWrite(keyAddress, key))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(OwnedAddcontEntitlements.Any(entitlement =>
|
||||
string.Equals(label, entitlement.Label, StringComparison.Ordinal))
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: NpEntitlementAccessErrorNoEntitlement);
|
||||
}
|
||||
|
||||
private static bool TryWriteAddcontEntitlementInfo(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public static class NpManagerExports
|
|||
private const int NpTitleIdSize = 16;
|
||||
private const int NpTitleSecretSize = 128;
|
||||
private const int NpErrorInvalidArgument = unchecked((int)0x80550003);
|
||||
private static int _nextNpRequestId;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3Zl8BePTh9Y",
|
||||
|
|
@ -23,6 +24,48 @@ public static class NpManagerExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "eiqMCt9UshI", ExportName = "sceNpCreateAsyncRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
|
||||
public static int NpCreateAsyncRequest(CpuContext ctx) => SetReturn(ctx, NextRequestId());
|
||||
|
||||
[SysAbiExport(Nid = "KfGZg2y73oM", ExportName = "sceNpCheckNpReachability", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
|
||||
public static int NpCheckNpReachability(CpuContext ctx) =>
|
||||
SetReturn(ctx, unchecked((int)ctx[CpuRegister.Rdi]) > 0 ? (int)OrbisGen2Result.ORBIS_GEN2_OK : NpErrorInvalidArgument);
|
||||
|
||||
[SysAbiExport(Nid = "GpLQDNKICac", ExportName = "sceNpCreateRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
|
||||
public static int NpCreateRequest(CpuContext ctx) => SetReturn(ctx, NextRequestId());
|
||||
|
||||
[SysAbiExport(Nid = "OzKvTvg3ZYU", ExportName = "sceNpAbortRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
|
||||
public static int NpAbortRequest(CpuContext ctx) => SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
|
||||
[SysAbiExport(Nid = "uqcPJLWL08M", ExportName = "sceNpPollAsync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
|
||||
public static int NpPollAsync(CpuContext ctx)
|
||||
{
|
||||
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (requestId <= 0)
|
||||
{
|
||||
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (resultAddress != 0)
|
||||
{
|
||||
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result, 0);
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int NextRequestId()
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextNpRequestId) & 0x7FFF_FFFF;
|
||||
return id == 0 ? 1 : id;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "S7QTn72PrDw",
|
||||
ExportName = "sceNpDeleteRequest",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ namespace SharpEmu.Libs.Np;
|
|||
|
||||
public static class NpSessionSignalingExports
|
||||
{
|
||||
private static int _nextContextId;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ysmw6J-P8Ak",
|
||||
ExportName = "sceNpSessionSignalingInitialize",
|
||||
|
|
@ -17,4 +19,16 @@ public static class NpSessionSignalingExports
|
|||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aBuX0PX-T7I",
|
||||
ExportName = "sceNpSessionSignalingCreateContext2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpSessionSignaling")]
|
||||
public static int NpSessionSignalingCreateContext2(CpuContext ctx)
|
||||
{
|
||||
var contextId = Interlocked.Increment(ref _nextContextId);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)contextId);
|
||||
return contextId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,13 @@ public static class NpWebApi2Exports
|
|||
return Interlocked.Increment(ref _nextPushEventHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QafxeZM3WK4",
|
||||
ExportName = "sceNpWebApi2PushEventDeletePushContext",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2PushEventDeletePushContext(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
private static bool IsValidLibraryContextId(int libraryContextId)
|
||||
{
|
||||
if (libraryContextId <= 0 || libraryContextId >= 0x8000)
|
||||
|
|
|
|||
|
|
@ -652,6 +652,32 @@ public static class PadExports
|
|||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rIZnR6eSpvk",
|
||||
ExportName = "scePadResetOrientation",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadResetOrientation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fCWdlnmB1Ks",
|
||||
ExportName = "scePadIsRemoteController",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadIsRemoteController(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[PadDataSize];
|
||||
|
|
|
|||
|
|
@ -464,6 +464,7 @@ internal static class RenderPhaseProfile
|
|||
|
||||
ReportImageUploads();
|
||||
BufferUploadProfile.Report();
|
||||
Console.Error.WriteLine(SharpEmu.ShaderCompiler.Resources.ResourceMaterializationCache.TakeReport());
|
||||
SharpEmu.ShaderCompiler.Resources.ResourceMaterializationProfile.WriteReport();
|
||||
SharpEmu.Libs.Diagnostics.AgcRegisterPacketProfile.WriteReport();
|
||||
SharpEmu.HLE.GpuMemory.GpuMemoryAccessProfile.WriteReport();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
// Keeps a storage image out of the Vulkan color-attachment list when the guest
|
||||
// disabled color writes. A storage image with active color writes still needs a
|
||||
// pass split; silently binding both views would make the result undefined.
|
||||
internal static class VulkanStorageFeedbackTargetResolver
|
||||
{
|
||||
public static bool TryResolve(
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestBlendState> blends,
|
||||
out GuestRenderTarget[] resolvedTargets,
|
||||
out bool usedCompatibilityAttachment,
|
||||
out ulong unsupportedAddress)
|
||||
{
|
||||
if (targets.Count != blends.Count)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"color attachment and blend-state counts must match",
|
||||
nameof(blends));
|
||||
}
|
||||
|
||||
resolvedTargets = targets.ToArray();
|
||||
usedCompatibilityAttachment = false;
|
||||
unsupportedAddress = 0;
|
||||
|
||||
for (var targetIndex = 0; targetIndex < targets.Count; targetIndex++)
|
||||
{
|
||||
var target = targets[targetIndex];
|
||||
if (target.Address == 0 || !textures.Any(texture =>
|
||||
texture.IsStorage && texture.Address == target.Address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targets.Count != 1 || blends[targetIndex].WriteMask != 0)
|
||||
{
|
||||
unsupportedAddress = target.Address;
|
||||
return false;
|
||||
}
|
||||
|
||||
resolvedTargets[targetIndex] = target with { Address = 0 };
|
||||
usedCompatibilityAttachment = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,18 @@ using VkBuffer = Silk.NET.Vulkan.Buffer;
|
|||
|
||||
internal static unsafe partial class VulkanVideoPresenter
|
||||
{
|
||||
private readonly record struct FeedbackSnapshotKey(
|
||||
Format Format,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint MipLevels,
|
||||
uint Layers,
|
||||
uint Samples,
|
||||
ImageAspectFlags Aspect);
|
||||
|
||||
private const int FeedbackSnapshotPoolMaxEntriesPerKey = 2;
|
||||
private const ulong FeedbackSnapshotPoolMaxBytes = 256UL * 1024 * 1024;
|
||||
|
||||
// Display buffers the guest registered; the only image addresses a flip may capture.
|
||||
private static readonly HashSet<ulong> _knownDisplayBuffers = new();
|
||||
|
||||
|
|
@ -115,12 +127,17 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
public uint DestinationSelect;
|
||||
// One view per mip level for a storage image the program indexes by mip; empty otherwise.
|
||||
public ImageView[] MipViews = [];
|
||||
// A draw-local copy used when the guest samples a depth aspect while the
|
||||
// same image is attached for writes. It retires with the submission.
|
||||
public bool IsFeedbackSnapshot;
|
||||
}
|
||||
|
||||
private sealed partial class Presenter
|
||||
{
|
||||
private readonly List<ResourceSlotIdentifier> _trackedImageBindings = new();
|
||||
private static int _guestImageDumpSequence;
|
||||
private readonly Dictionary<FeedbackSnapshotKey, Stack<CachedImage>> _feedbackSnapshotPool = [];
|
||||
private ulong _feedbackSnapshotPoolBytes;
|
||||
|
||||
// Shader images for work without color targets: resolve, acquire, transition and release the bindings.
|
||||
private TextureResource[] ResolveDrawTextures(IReadOnlyList<GuestDrawTexture> textures)
|
||||
|
|
@ -514,10 +531,10 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
};
|
||||
}
|
||||
|
||||
private Sampler ResolveSampler(in GuestSampler sampler)
|
||||
private Sampler ResolveSampler(in GuestSampler sampler, Format viewFormat)
|
||||
{
|
||||
Span<uint> words = stackalloc uint[4] { sampler.Word0, sampler.Word1, sampler.Word2, sampler.Word3 };
|
||||
return _samplerStore.GetSampler(new SamplerDescriptorWords(words));
|
||||
return _samplerStore.GetSampler(new SamplerDescriptorWords(words), ViewFormatRules.IsIntegerFormat(viewFormat));
|
||||
}
|
||||
|
||||
// Views for every cached binding after the targets are bound; a stale image is found again first.
|
||||
|
|
@ -571,12 +588,12 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
binding.Image = image.Backing.Handle;
|
||||
if (!binding.IsStorage)
|
||||
{
|
||||
binding.Sampler = ResolveSampler(binding.SamplerState);
|
||||
binding.Sampler = ResolveSampler(binding.SamplerState, binding.Request.View.Format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CachedImage AcquireStencilStorage(TextureResource binding, CachedImage attachment)
|
||||
private CachedImage AcquireStencilStorage(TextureResource binding, CachedImage attachment, bool writeBack = true)
|
||||
{
|
||||
var description = binding.Request.Description;
|
||||
if (description.Data.Address != attachment.Description.Stencil.Address || description.Data.Size > attachment.Description.Stencil.Size ||
|
||||
|
|
@ -586,7 +603,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
throw SubmissionScheduler.Fatal("The stencil storage request does not cover its attachment's stencil layout.");
|
||||
}
|
||||
|
||||
if (_hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier &&
|
||||
if (writeBack && _hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier &&
|
||||
(_boundDepthLoadState.StencilTestEnabled || _boundDepthLoadState.StencilClearEnabled))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("A stencil storage write cannot share an active stencil attachment.");
|
||||
|
|
@ -595,7 +612,16 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var preparation = RequirePreparation();
|
||||
var stencilImages = preparation.StencilStorageImages ??= new();
|
||||
if (stencilImages.TryGetValue(attachment, out var storage))
|
||||
{
|
||||
if (writeBack)
|
||||
{
|
||||
preparation.StencilStorageWriteBackImages ??= new();
|
||||
preparation.StencilStorageWriteBackImages.Add(attachment);
|
||||
storage.Binding.ShaderWrite = true;
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
var sampledRequest = binding.Request with
|
||||
{
|
||||
|
|
@ -603,10 +629,19 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
View = binding.Request.View with { Usage = ImageUsageFlags.SampledBit },
|
||||
};
|
||||
_imageCache.AcquireTextureView(binding.ImageIdentifier, sampledRequest);
|
||||
_imageCache.MarkGpuWritten(binding.ImageIdentifier);
|
||||
if (writeBack)
|
||||
{
|
||||
_imageCache.MarkGpuWritten(binding.ImageIdentifier);
|
||||
}
|
||||
|
||||
storage = attachment.CreateStencilStorageImage();
|
||||
stencilImages.Add(attachment, storage);
|
||||
storage.Binding.ShaderWrite = true;
|
||||
storage.Binding.ShaderWrite = writeBack;
|
||||
if (writeBack)
|
||||
{
|
||||
preparation.StencilStorageWriteBackImages ??= new();
|
||||
preparation.StencilStorageWriteBackImages.Add(attachment);
|
||||
}
|
||||
attachment.CopyStencilStorage(storage, _bufferCache.GetUtilityBuffer(GpuBufferUsage.DeviceLocal), writeBack: false);
|
||||
if (_hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier)
|
||||
{
|
||||
|
|
@ -619,6 +654,182 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
return storage;
|
||||
}
|
||||
|
||||
private CachedImage CreateDepthFeedbackSnapshot(CachedImage source, ImageAspectFlags aspect)
|
||||
{
|
||||
if (source.Backing.ImageType != ImageType.Type2D || source.Backing.MipLevels == 0 ||
|
||||
source.Backing.Samples == 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"A depth feedback snapshot needs a 2D image: type={source.Backing.ImageType} " +
|
||||
$"mips={source.Backing.MipLevels} samples={source.Backing.Samples}.");
|
||||
}
|
||||
|
||||
var key = new FeedbackSnapshotKey(
|
||||
source.Backing.Format,
|
||||
source.Backing.Extent.Width,
|
||||
source.Backing.Extent.Height,
|
||||
source.Backing.MipLevels,
|
||||
source.Backing.Layers,
|
||||
source.Backing.Samples,
|
||||
ImageAspectFlags.None);
|
||||
var snapshot = RentFeedbackSnapshot(key);
|
||||
if (snapshot is null)
|
||||
{
|
||||
var description = ImageDescription.Create();
|
||||
description.PixelFormat = source.Backing.Format;
|
||||
description.GuestFormat = source.Description.GuestFormat;
|
||||
description.Type = GuestImageType.Color2D;
|
||||
description.Extent = new Extent3D(source.Backing.Extent.Width, source.Backing.Extent.Height, 1);
|
||||
description.Resources = new SubresourceCount(source.Backing.MipLevels, source.Backing.Layers);
|
||||
description.BytesPerBlock = source.Description.BytesPerBlock == 0 ? 1 : source.Description.BytesPerBlock;
|
||||
description.Samples = source.Backing.Samples;
|
||||
// The snapshot is host-owned. Keeping guest placement empty prevents
|
||||
// the image cache from treating it as guest memory that needs upload.
|
||||
description.Pitch = 0;
|
||||
description.TileMode = GuestTileMode.Linear;
|
||||
snapshot = new CachedImage(_deviceInfo, _scheduler, _guestBacking, description);
|
||||
}
|
||||
var range = new SubresourceRange(0, source.Backing.MipLevels, 0, source.Backing.Layers);
|
||||
if ((aspect & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit)) != 0)
|
||||
{
|
||||
snapshot.CopyDepthStencilFrom(
|
||||
source,
|
||||
range,
|
||||
source.Backing.Extent,
|
||||
aspect & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit));
|
||||
var depthView = _boundDepth.Target.Target.Request.View;
|
||||
var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
|
||||
source.Transition(
|
||||
_boundDepthLayout,
|
||||
AccessFlags.DepthStencilAttachmentReadBit |
|
||||
(writes != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0),
|
||||
new SubresourceRange(depthView.BaseLevel, depthView.LevelCount, depthView.BaseLayer, depthView.LayerCount),
|
||||
BeginBatchedGuestCommands());
|
||||
snapshot.Transition(
|
||||
ImageLayout.DepthStencilReadOnlyOptimal,
|
||||
AccessFlags.ShaderReadBit,
|
||||
range,
|
||||
BeginBatchedGuestCommands());
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.CopyFrom(source);
|
||||
snapshot.Transition(
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
AccessFlags.ShaderReadBit,
|
||||
range,
|
||||
BeginBatchedGuestCommands());
|
||||
}
|
||||
|
||||
var preparation = RequirePreparation();
|
||||
preparation.FeedbackSnapshots ??= new();
|
||||
preparation.FeedbackSnapshots.Add(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private CachedImage? RentFeedbackSnapshot(FeedbackSnapshotKey key)
|
||||
{
|
||||
if (!_feedbackSnapshotPool.TryGetValue(key, out var entries) || !entries.TryPop(out var snapshot))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_feedbackSnapshotPoolBytes -= snapshot.Backing.AllocationSize;
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
_feedbackSnapshotPool.Remove(key);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private void RetireFeedbackSnapshot(CachedImage snapshot)
|
||||
{
|
||||
var key = new FeedbackSnapshotKey(
|
||||
snapshot.Backing.Format,
|
||||
snapshot.Backing.Extent.Width,
|
||||
snapshot.Backing.Extent.Height,
|
||||
snapshot.Backing.MipLevels,
|
||||
snapshot.Backing.Layers,
|
||||
snapshot.Backing.Samples,
|
||||
ImageAspectFlags.None);
|
||||
var allocation = snapshot.Backing.AllocationSize;
|
||||
_feedbackSnapshotPool.TryGetValue(key, out var entries);
|
||||
if (entries is null && _feedbackSnapshotPoolBytes + allocation <= FeedbackSnapshotPoolMaxBytes)
|
||||
{
|
||||
entries = new Stack<CachedImage>();
|
||||
_feedbackSnapshotPool[key] = entries;
|
||||
}
|
||||
|
||||
if (entries is not null && entries.Count < FeedbackSnapshotPoolMaxEntriesPerKey &&
|
||||
_feedbackSnapshotPoolBytes + allocation <= FeedbackSnapshotPoolMaxBytes)
|
||||
{
|
||||
entries.Push(snapshot);
|
||||
_feedbackSnapshotPoolBytes += allocation;
|
||||
return;
|
||||
}
|
||||
|
||||
snapshot.Dispose();
|
||||
}
|
||||
|
||||
private void DestroyFeedbackSnapshotPool()
|
||||
{
|
||||
foreach (var entries in _feedbackSnapshotPool.Values)
|
||||
{
|
||||
foreach (var snapshot in entries)
|
||||
{
|
||||
snapshot.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_feedbackSnapshotPool.Clear();
|
||||
_feedbackSnapshotPoolBytes = 0;
|
||||
}
|
||||
|
||||
// Vulkan core does not permit a sampled depth aspect to alias a depth
|
||||
// attachment that the same draw may write. Preserve the guest's
|
||||
// read-before-write semantics with a private copy for this draw.
|
||||
private void PrepareDepthFeedback(TextureResource[] bindings)
|
||||
{
|
||||
if (!_hasBoundDepth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var depthImage = _imageCache.GetImage(_boundDepth.Image);
|
||||
var attachmentView = _boundDepth.Target.Target.Request.View;
|
||||
var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
|
||||
var writableDepth = writes & ImageAspectFlags.DepthBit;
|
||||
if (writableDepth == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.IsHostMovie || binding.IsStorage || binding.CachedImage is not { } image ||
|
||||
!ReferenceEquals(image, depthImage) || !ViewsOverlap(binding.Request.View, attachmentView))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sampledAspect = ViewFormatRules.IsStencilViewFormat(binding.Request.View.Format)
|
||||
? ImageAspectFlags.StencilBit
|
||||
: ImageAspectFlags.DepthBit;
|
||||
if ((sampledAspect & writableDepth) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var snapshot = CreateDepthFeedbackSnapshot(image, sampledAspect);
|
||||
binding.CachedImage = snapshot;
|
||||
binding.Image = snapshot.Backing.Handle;
|
||||
binding.View = snapshot.GetOrCreateView(binding.Request.View);
|
||||
binding.MipViews = [];
|
||||
binding.IsFeedbackSnapshot = true;
|
||||
}
|
||||
}
|
||||
|
||||
// The layout each binding reads through; a target read by its own draw uses the general layout.
|
||||
private void RecordTextureTransitions(TextureResource[] bindings) =>
|
||||
RecordDrawTextureTransitions(bindings, null, GuestDepthState.Default);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
OneDimensional: image.Dimension is ImageDimension.Dim1D or ImageDimension.Dim1DArray,
|
||||
R128: image.R128,
|
||||
Multisampled: image.Dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray,
|
||||
DepthCompare: image.DepthCompare);
|
||||
DepthCompare: image.DepthCompare,
|
||||
Atomic: image.Atomic);
|
||||
|
||||
// Render-state discovery for one shader image; the view is acquired later with the draw.
|
||||
private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index)
|
||||
|
|
@ -114,6 +115,20 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
|
||||
BindImage(imageIdentifier, storage);
|
||||
var descriptor = new TextureDescriptorWords(words);
|
||||
if (ShouldTraceTextureBindings())
|
||||
{
|
||||
var cached = _imageCache.GetImage(imageIdentifier);
|
||||
var description = cached.Description;
|
||||
Console.Error.WriteLine(
|
||||
$"TextureBinding stage={program.Stage} hash=0x{program.Hash:X16} index={index} " +
|
||||
$"address=0x{new TextureDescriptorWords(words).BaseAddress:X16} " +
|
||||
$"descriptor={descriptor.BaseAddress:X16} size={descriptor.Width + 1}x{descriptor.Height + 1} " +
|
||||
$"format={(uint)descriptor.Format} tile={(uint)descriptor.TileMode} " +
|
||||
$"image=0x{description.Data.Address:X16} size=0x{description.Data.Size:X} " +
|
||||
$"extent={description.Extent.Width}x{description.Extent.Height} pitch={description.Pitch} " +
|
||||
$"guestFormat={(uint)description.GuestFormat} imageTile={(uint)description.TileMode} " +
|
||||
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format}");
|
||||
}
|
||||
return new TextureResource
|
||||
{
|
||||
Address = descriptor.BaseAddress,
|
||||
|
|
@ -128,7 +143,31 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
// Compare bits stay only on depth-compare samplers; a forced point sampler drops its filters.
|
||||
private Sampler ResolveSampler(SamplerResource sampler, uint[] words, ShaderProgramInfo program, int index, ShaderStageResources stage)
|
||||
// A sampler takes the numeric class of the views it samples; integer only when every
|
||||
// paired view is integer, since a float view needs a float border and filtering.
|
||||
private static bool SamplesIntegerViews(ShaderResourceInfo info, TextureResource[] images, int sampler)
|
||||
{
|
||||
var paired = false;
|
||||
foreach (var pair in info.SampledPairs)
|
||||
{
|
||||
if (pair.Sampler != sampler || pair.Image >= images.Length || images[pair.Image].IsHostMovie)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ViewFormatRules.IsIntegerFormat(images[pair.Image].Request.View.Format))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
paired = true;
|
||||
}
|
||||
|
||||
return paired;
|
||||
}
|
||||
|
||||
private Sampler ResolveSampler(SamplerResource sampler, uint[] words, ShaderProgramInfo program, int index, ShaderStageResources stage,
|
||||
bool integerView)
|
||||
{
|
||||
if (words.Length < 4)
|
||||
{
|
||||
|
|
@ -164,7 +203,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
$"user_data=[{string.Join(",", stage.Resources.UserData.Select(word => $"{word:X8}"))}]");
|
||||
}
|
||||
|
||||
return _samplerStore.GetSampler(descriptor);
|
||||
return _samplerStore.GetSampler(descriptor, integerView);
|
||||
}
|
||||
|
||||
// The guest textures the movie path matches; built only while a decoded frame is active.
|
||||
|
|
@ -216,7 +255,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
descriptors.Samplers = new Sampler[info.Samplers.Count];
|
||||
for (var index = 0; index < info.Samplers.Count; index++)
|
||||
{
|
||||
descriptors.Samplers[index] = ResolveSampler(info.Samplers[index], snapshot.Samplers[index], program, index, stage);
|
||||
descriptors.Samplers[index] = ResolveSampler(info.Samplers[index], snapshot.Samplers[index], program, index, stage,
|
||||
SamplesIntegerViews(info, descriptors.Images, index));
|
||||
}
|
||||
|
||||
var shaderData = new uint[layout.ShaderDataDwordCount];
|
||||
|
|
@ -852,6 +892,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
DebugName = bindPoint == PipelineBindPoint.Compute ? "SharpEmu dispatch" : "SharpEmu draw",
|
||||
Textures = textures,
|
||||
FeedbackSnapshots = preparation.FeedbackSnapshots?.ToArray() ?? [],
|
||||
OverflowBuffers = preparation.OverflowBuffers.Count == 0 ? null : preparation.OverflowBuffers.ToArray(),
|
||||
});
|
||||
preparation.OverflowBuffers.Clear();
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private string? _pipelineCachePath;
|
||||
private bool _pipelineCacheDirty;
|
||||
private long _lastPipelineCacheSaveTick;
|
||||
// Reading a mature driver cache can itself take several seconds. Keep the
|
||||
// render thread focused on pipeline warm-up; teardown still saves eagerly.
|
||||
private const long PipelineCacheCheckpointIntervalMs = 300_000;
|
||||
private Queue _queue;
|
||||
private uint _queueFamilyIndex;
|
||||
|
||||
|
|
@ -669,6 +672,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
|
||||
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
|
||||
ShaderInt64 = supportedFeatures.ShaderInt64,
|
||||
ShaderFloat64 = supportedFeatures.ShaderFloat64,
|
||||
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
|
||||
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
|
||||
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
|
||||
|
|
@ -699,6 +703,15 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
"The device lacks the shaderInt64 feature, which the device-address programs need.");
|
||||
}
|
||||
|
||||
if (!supportedFeatures.ShaderFloat64)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] GPU does not support shaderFloat64 " +
|
||||
"translated shaders using double precision will fail.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!supportedFeatures.VertexPipelineStoresAndAtomics || !supportedFeatures.FragmentStoresAndAtomics)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -786,19 +799,33 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
SType = StructureType.PhysicalDeviceBufferDeviceAddressFeatures,
|
||||
PNext = &timelineSemaphoreFeatures,
|
||||
};
|
||||
var atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
|
||||
PNext = &addressFeatures,
|
||||
};
|
||||
var featuresQuery = new PhysicalDeviceFeatures2
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceFeatures2,
|
||||
PNext = &addressFeatures,
|
||||
PNext = &atomicInt64Features,
|
||||
};
|
||||
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
|
||||
var supportsTimelineSemaphore = timelineSemaphoreFeatures.TimelineSemaphore;
|
||||
var supportsBufferDeviceAddress = addressFeatures.BufferDeviceAddress;
|
||||
var supportsSharedInt64Atomics = atomicInt64Features.ShaderSharedInt64Atomics;
|
||||
var supportsMaintenance8 = maintenance8Features.Maintenance8;
|
||||
var supportsRobustBufferAccess2 = robustness2Features.RobustBufferAccess2;
|
||||
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
|
||||
var supportsNullDescriptor = robustness2Features.NullDescriptor;
|
||||
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
|
||||
SetSharedInt64AtomicsCapability(supportsSharedInt64Atomics);
|
||||
if (!supportsSharedInt64Atomics)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] GPU does not support shaderSharedInt64Atomics " +
|
||||
"64-bit LDS atomics fall back to a non-atomic pair of 32-bit atomics.");
|
||||
}
|
||||
|
||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var deviceProperties);
|
||||
var deviceName = SilkMarshal.PtrToString((nint)deviceProperties.DeviceName) ?? "unknown";
|
||||
RequireRenderingFeature(vulkan13Features.DynamicRendering, "Vulkan 1.3 dynamicRendering", deviceName);
|
||||
|
|
@ -911,6 +938,17 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
PNext = &timelineSemaphoreFeatures,
|
||||
};
|
||||
void* renderingChain = &addressFeatures;
|
||||
if (supportsSharedInt64Atomics)
|
||||
{
|
||||
atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
|
||||
ShaderSharedInt64Atomics = true,
|
||||
PNext = renderingChain,
|
||||
};
|
||||
renderingChain = &atomicInt64Features;
|
||||
}
|
||||
|
||||
if (_supportsFragmentShaderBarycentric)
|
||||
{
|
||||
barycentricFeatures.PNext = renderingChain;
|
||||
|
|
@ -1143,7 +1181,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
// frame, so saving after every slow creation compounds a warm-up
|
||||
// hitch into a multi-minute stall. Coalesce all creations into one
|
||||
// periodic snapshot; shutdown still forces a final save.
|
||||
if (Environment.TickCount64 - _lastPipelineCacheSaveTick >= 30_000)
|
||||
if (Environment.TickCount64 - _lastPipelineCacheSaveTick >= PipelineCacheCheckpointIntervalMs)
|
||||
{
|
||||
SavePipelineCache(force: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
SavePipelineCache(force: true);
|
||||
DrainFrameSlots();
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
DestroyFeedbackSnapshotPool();
|
||||
DestroyRenderPipelines();
|
||||
_descriptorHeap.Dispose();
|
||||
_imageCache.Dispose();
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceTextureBindingsEnabled =
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly uint _traceGuestImageWidth =
|
||||
uint.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_WIDTH"),
|
||||
|
|
@ -259,6 +264,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private static bool ShouldTraceVulkanResources() =>
|
||||
_traceVulkanResourcesEnabled;
|
||||
|
||||
private static bool ShouldTraceTextureBindings() =>
|
||||
_traceTextureBindingsEnabled;
|
||||
|
||||
private void RecycleSubmissionUploads(SubmissionUploadResources resources)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ResourceDestroy);
|
||||
|
|
@ -270,6 +278,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var snapshot in resources.FeedbackSnapshots)
|
||||
{
|
||||
RetireFeedbackSnapshot(snapshot);
|
||||
}
|
||||
|
||||
foreach (var (buffer, memory) in resources.OverflowBuffers ?? [])
|
||||
{
|
||||
RecycleHostBuffer(buffer, memory);
|
||||
|
|
|
|||
|
|
@ -137,8 +137,12 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.MovieFramePolling);
|
||||
if (!HostMovieBridge.TryDecodeNextFrame(
|
||||
advanceClock: _hostMovieLumaTextureAddress != 0 &&
|
||||
_hostMovieChromaTextureAddress != 0,
|
||||
// Once the host bridge owns a movie, advance its clock on
|
||||
// every presenter tick. Texture discovery can lag behind
|
||||
// the first decoded frame; tying playback to those two
|
||||
// addresses leaves the guest stuck forever on attract
|
||||
// movies when Vulkan selects a different binding layout.
|
||||
advanceClock: true,
|
||||
out var pixels,
|
||||
out var width,
|
||||
out var height,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
|
||||
|
||||
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
|
||||
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
|
||||
|
|
@ -77,6 +78,16 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
|
||||
word = 0;
|
||||
// Resource planning can inspect a dynamic descriptor before the draw has
|
||||
// supplied a valid guest address. Do not pass an invalid range to the
|
||||
// page tracker: it treats that as an emulator invariant violation and
|
||||
// terminates the process. A failed read lets the materializer reject or
|
||||
// specialize the source normally.
|
||||
if (!_guestMemory.CanRead(address, sizeof(uint)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_bufferCache.TrySynchronizeCpuRead(address, sizeof(uint),
|
||||
SharpEmu.HLE.GuestMemory.GuestMemoryProfile.ReadbackSource.ShaderResourceRead))
|
||||
{
|
||||
|
|
@ -98,6 +109,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.CleanGuestRead);
|
||||
word = 0;
|
||||
if (!_guestMemory.CanRead(address, sizeof(uint)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_bufferCache.HasGpuDirtyPages(address, sizeof(uint)) ||
|
||||
_bufferCache.HasGpuDirtyBytes(address, sizeof(uint)) ||
|
||||
_imageCache.HasGpuModifiedImageBytes(address, sizeof(uint)))
|
||||
|
|
@ -115,6 +131,19 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
return true;
|
||||
}
|
||||
|
||||
// The same ownership rules as the two word readers, checked once for a whole range.
|
||||
public bool TryReadResidentGuestBytes(ulong address, Span<byte> destination, bool clean)
|
||||
{
|
||||
var size = (ulong)destination.Length;
|
||||
if (_bufferCache.HasGpuDirtyPages(address, size) ||
|
||||
(clean && (_bufferCache.HasGpuDirtyBytes(address, size) || _imageCache.HasGpuModifiedImageBytes(address, size))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _guestMemory.TryRead(address, destination);
|
||||
}
|
||||
|
||||
public ulong CreateShaderModule(IGuestCompiledShader shader, ShaderStage stage, ulong hash, ulong programId)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramCompile);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
public Dictionary<CachedImage, CachedImage>? StencilStorageImages { get; set; }
|
||||
|
||||
public HashSet<CachedImage>? StencilStorageWriteBackImages { get; set; }
|
||||
|
||||
public List<CachedImage>? FeedbackSnapshots { get; set; }
|
||||
|
||||
public bool Committed { get; set; }
|
||||
|
||||
public bool CommandsRecorded { get; set; }
|
||||
|
|
@ -69,7 +73,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
try
|
||||
{
|
||||
if (CommandsRecorded)
|
||||
if (CommandsRecorded && StencilStorageWriteBackImages is not null &&
|
||||
StencilStorageWriteBackImages.Contains(attachment))
|
||||
attachment.CopyStencilStorage(storage, owner._bufferCache.GetUtilityBuffer(GpuBufferUsage.DeviceLocal), writeBack: true);
|
||||
}
|
||||
finally
|
||||
|
|
@ -79,6 +84,14 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
}
|
||||
|
||||
if (!Committed && FeedbackSnapshots is { } feedbackSnapshots)
|
||||
{
|
||||
foreach (var snapshot in feedbackSnapshots)
|
||||
{
|
||||
snapshot.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (Committed)
|
||||
{
|
||||
return;
|
||||
|
|
@ -367,6 +380,40 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
? entry
|
||||
: throw SubmissionScheduler.Fatal($"The pipeline handle is unknown: pipeline={pipeline.Pipeline} layout={pipeline.Layout}.");
|
||||
|
||||
// Vulkan cannot sample a stencil aspect while the same aspect is attached for writes.
|
||||
// Use a per-draw storage copy for read-only stencil feedback and keep the attachment authoritative.
|
||||
private void PrepareStencilFeedback(TextureResource[] bindings)
|
||||
{
|
||||
if (!_hasBoundDepth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var depthImage = _imageCache.GetImage(_boundDepth.Image);
|
||||
var attachmentView = _boundDepth.Target.Target.Request.View;
|
||||
var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
|
||||
if ((writes & ImageAspectFlags.StencilBit) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.IsHostMovie || binding.CachedImage is not { } image ||
|
||||
!ReferenceEquals(image, depthImage) || !ViewFormatRules.IsStencilViewFormat(binding.Request.View.Format) ||
|
||||
!ViewsOverlap(binding.Request.View, attachmentView))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var storage = AcquireStencilStorage(binding, image, writeBack: false);
|
||||
binding.CachedImage = storage;
|
||||
binding.Image = storage.Backing.Handle;
|
||||
binding.View = storage.GetOrCreateView(binding.Request.View with { Aspect = ImageAspectFlags.ColorBit });
|
||||
binding.MipViews = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The layout each sampled image reads through; a target read by its own draw uses the general layout.
|
||||
private void RecordStageTextureTransitions(TextureResource[] bindings)
|
||||
|
|
@ -404,6 +451,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
}
|
||||
|
||||
// Create feedback copies after load clears have been materialized;
|
||||
// otherwise a shader would sample the pre-clear contents.
|
||||
PrepareDepthFeedback(bindings);
|
||||
PrepareStencilFeedback(bindings);
|
||||
|
||||
var command = BeginBatchedGuestCommands();
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using SharpEmu.Libs.AvPlayer;
|
|||
using SharpEmu.Libs.Media;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
|
|
@ -79,6 +80,15 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
private static int _nativeSubgroupSize;
|
||||
private static int _nativeSubgroupShaderStages;
|
||||
private static int _sharedInt64AtomicsSupported;
|
||||
|
||||
// True when the device supports shaderSharedInt64Atomics, so LDS 64-bit
|
||||
// atomics can be emitted as real 64-bit atomics.
|
||||
internal static bool SharedInt64AtomicsEnabled =>
|
||||
Volatile.Read(ref _sharedInt64AtomicsSupported) != 0;
|
||||
|
||||
private static void SetSharedInt64AtomicsCapability(bool supported) =>
|
||||
Volatile.Write(ref _sharedInt64AtomicsSupported, supported ? 1 : 0);
|
||||
|
||||
internal static bool GraphicsSubgroupOperationsEnabled =>
|
||||
VulkanGraphicsSubgroupPolicy.ShouldUseNativeGraphicsSubgroups(
|
||||
|
|
@ -233,6 +243,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
public string DebugName = "SharpEmu translated";
|
||||
public TextureResource[] Textures = [];
|
||||
// Draw-local feedback copies stay alive until the GPU has consumed
|
||||
// the descriptors that reference them.
|
||||
public CachedImage[] FeedbackSnapshots = [];
|
||||
// Host buffers that took uploads the stream ring could not hold; recycled with the draw.
|
||||
public (VkBuffer Buffer, DeviceMemory Memory)[]? OverflowBuffers;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ public static partial class Gen5MslTranslator
|
|||
// form is fma(src0, src1, src2) exactly like the SPIR-V translator.
|
||||
"VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" =>
|
||||
FloatResult(instruction, $"fma({F(instruction, 0)}, {F(instruction, 1)}, {F(instruction, 2)})"),
|
||||
"VFmaF16" => Float16Result(
|
||||
"VFmaF16" or "VFmaMkF16" or "VFmaAkF16" => Float16Result(
|
||||
instruction,
|
||||
destination,
|
||||
$"fma({F16(instruction, 0)}, {F16(instruction, 1)}, {F16(instruction, 2)})"),
|
||||
|
|
@ -216,6 +216,17 @@ public static partial class Gen5MslTranslator
|
|||
"VFractF32" => FloatResult(instruction, $"fract({F(instruction, 0)})"),
|
||||
"VSqrtF32" => FloatResult(instruction, $"sqrt({F(instruction, 0)})"),
|
||||
"VRsqF32" => FloatResult(instruction, $"rsqrt({F(instruction, 0)})"),
|
||||
"VRcpF16" => Float16Result(instruction, destination, $"(1.0f / {F16(instruction, 0)})"),
|
||||
"VSqrtF16" => Float16Result(instruction, destination, $"sqrt({F16(instruction, 0)})"),
|
||||
"VLogF16" => Float16Result(instruction, destination, $"log2({F16(instruction, 0)})"),
|
||||
"VExpF16" => Float16Result(instruction, destination, $"exp2({F16(instruction, 0)})"),
|
||||
"VFloorF16" => Float16Result(instruction, destination, $"floor({F16(instruction, 0)})"),
|
||||
"VCeilF16" => Float16Result(instruction, destination, $"ceil({F16(instruction, 0)})"),
|
||||
"VTruncF16" => Float16Result(instruction, destination, $"trunc({F16(instruction, 0)})"),
|
||||
"VRndneF16" => Float16Result(instruction, destination, $"rint({F16(instruction, 0)})"),
|
||||
"VFractF16" => Float16Result(instruction, destination, $"fract({F16(instruction, 0)})"),
|
||||
"VSinF16" => Float16Result(instruction, destination, $"sin({F16(instruction, 0)} * {TauLiteral})"),
|
||||
"VCosF16" => Float16Result(instruction, destination, $"cos({F16(instruction, 0)} * {TauLiteral})"),
|
||||
"VRsqF16" => Float16Result(
|
||||
instruction,
|
||||
destination,
|
||||
|
|
@ -228,6 +239,12 @@ public static partial class Gen5MslTranslator
|
|||
"VCosF32" => FloatResult(instruction, $"cos({F(instruction, 0)} * {TauLiteral})"),
|
||||
"VLdexpF32" =>
|
||||
FloatResult(instruction, $"ldexp({F(instruction, 0)}, as_type<int>({RawSource(instruction, 1)}))"),
|
||||
"VMin3F16" => Float16Result(instruction, destination,
|
||||
$"fmin(fmin({F16(instruction, 0)}, {F16(instruction, 1)}), {F16(instruction, 2)})"),
|
||||
"VMax3F16" => Float16Result(instruction, destination,
|
||||
$"fmax(fmax({F16(instruction, 0)}, {F16(instruction, 1)}), {F16(instruction, 2)})"),
|
||||
"VMed3F16" => Float16Result(instruction, destination,
|
||||
$"fmax(fmin({F16(instruction, 0)}, {F16(instruction, 1)}), fmin(fmax({F16(instruction, 0)}, {F16(instruction, 1)}), {F16(instruction, 2)}))"),
|
||||
"VMin3F32" =>
|
||||
FloatResult(instruction, $"fmin(fmin({F(instruction, 0)}, {F(instruction, 1)}), {F(instruction, 2)})"),
|
||||
"VMax3F32" =>
|
||||
|
|
@ -422,8 +439,10 @@ public static partial class Gen5MslTranslator
|
|||
}
|
||||
case "VSubbU32":
|
||||
case "VSubbrevU32":
|
||||
case "VSubCoCiU32":
|
||||
case "VSubrevCoCiU32":
|
||||
{
|
||||
var reverse = instruction.Opcode == "VSubbrevU32";
|
||||
var reverse = instruction.Opcode is "VSubbrevU32" or "VSubrevCoCiU32";
|
||||
var left = Temp("uint", RawSource(instruction, reverse ? 1 : 0));
|
||||
var right = Temp("uint", RawSource(instruction, reverse ? 0 : 1));
|
||||
var borrowIn = instruction.Sources.Count > 2
|
||||
|
|
@ -1107,7 +1126,18 @@ public static partial class Gen5MslTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode == "SFlbitI32B64")
|
||||
{
|
||||
var wide = Temp("ulong", RawSource64(instruction, 0));
|
||||
var result = Temp(
|
||||
"uint",
|
||||
$"{wide} == 0ul ? 0xFFFFFFFFu : (uint)clz({wide})");
|
||||
StoreScalar(destination, result);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.EndsWith("B64", StringComparison.Ordinal) ||
|
||||
instruction.Opcode == "SAshrI64" ||
|
||||
instruction.Opcode is "SBfeU64" or "SBfeI64")
|
||||
{
|
||||
return TryEmitScalar64(instruction, destination, out error);
|
||||
|
|
@ -1240,6 +1270,14 @@ public static partial class Gen5MslTranslator
|
|||
resultExpression = $"({left} - {right})";
|
||||
sccStatement = $"(((({left} ^ {right})) & ({left} ^ RESULT)) >> 31) != 0u";
|
||||
break;
|
||||
case "SAddF32":
|
||||
resultExpression = $"as_type<uint>(as_type<float>({left}) + as_type<float>({right}))";
|
||||
sccStatement = string.Empty;
|
||||
break;
|
||||
case "SSubF32":
|
||||
resultExpression = $"as_type<uint>(as_type<float>({left}) - as_type<float>({right}))";
|
||||
sccStatement = string.Empty;
|
||||
break;
|
||||
case "SAddcU32":
|
||||
{
|
||||
var partial = Temp("uint", $"{left} + {right}");
|
||||
|
|
@ -1479,6 +1517,16 @@ public static partial class Gen5MslTranslator
|
|||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
|
||||
// S_SWAPPC_B64 is a dynamic call/return operation. The Gen5
|
||||
// translator has already linearized the reachable shader body,
|
||||
// so there is no host program counter to exchange here. Its
|
||||
// single pair operand must not be treated as a binary 64-bit op.
|
||||
if (instruction.Opcode == "SSwappcB64")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var left = Temp("ulong", RawSource64(instruction, 0));
|
||||
if (instruction.Opcode.EndsWith("SaveexecB64", StringComparison.Ordinal))
|
||||
{
|
||||
|
|
@ -1536,12 +1584,15 @@ public static partial class Gen5MslTranslator
|
|||
value = $"({quadAny} * 0xFul)";
|
||||
break;
|
||||
}
|
||||
case "SLshlB64" or "SLshrB64":
|
||||
case "SLshlB64" or "SLshrB64" or "SAshrI64":
|
||||
{
|
||||
var shift = Temp("uint", $"({RawSource(instruction, 1)}) & 63u");
|
||||
value = instruction.Opcode == "SLshlB64"
|
||||
? $"({left} << {shift})"
|
||||
: $"({left} >> {shift})";
|
||||
value = instruction.Opcode switch
|
||||
{
|
||||
"SLshlB64" => $"({left} << {shift})",
|
||||
"SLshrB64" => $"({left} >> {shift})",
|
||||
_ => $"(ulong)(as_type<long>({left}) >> {shift})",
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "SBfmB64":
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,324 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
// IMAGE_BVH_INTERSECT_RAY tests one ray against one BVH node; the guest shader owns the
|
||||
// traversal loop and stack. This is the GFX10 (RT IP 1.1) node test as AMD's GPURT
|
||||
// software fallback defines it (IntersectCommon.hlsl, image_bvh64_intersect_ray_base),
|
||||
// with node layouts from GPURT's gfx10 TriangleNode1_0 and BoxNode1_0.
|
||||
public static partial class Gen5SpirvTranslator
|
||||
{
|
||||
private sealed partial class CompilationContext
|
||||
{
|
||||
private const uint InvalidNode = 0xFFFF_FFFF;
|
||||
|
||||
// Node pointer: the byte offset divided by eight above bits 2:0, which hold the type.
|
||||
private const uint NodeTypeTriangle1 = 1;
|
||||
private const uint NodeTypeBoxFloat16 = 4;
|
||||
private const uint NodeTypeBoxFloat32 = 5;
|
||||
|
||||
// Triangle node: four float3 vertices, then flags/indices, the triangle id at dword 15.
|
||||
private const uint TriangleIdDword = 15;
|
||||
private const int TriangleIdBitStride = 8;
|
||||
private const int TriangleIdISourceShift = 0;
|
||||
private const int TriangleIdJSourceShift = 2;
|
||||
|
||||
// Box nodes: four child pointers, then per child a float3 min/max pair (six floats)
|
||||
// or six packed halves (three dwords).
|
||||
private const uint BoxBoundsDword = 4;
|
||||
|
||||
private uint[]? _rayResult;
|
||||
|
||||
private void EmitRayIntersect(Gen5RayIntersectControl ray, bool bvh64)
|
||||
{
|
||||
_rayResult ??= DeclareRayResult();
|
||||
EmitExecConditional(() => EmitRayIntersectActive(ray, bvh64));
|
||||
}
|
||||
|
||||
private uint[] DeclareRayResult()
|
||||
{
|
||||
var result = new uint[Gen5RayIntersectControl.ResultDwords];
|
||||
for (var component = 0; component < result.Length; component++)
|
||||
{
|
||||
result[component] = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, UInt(InvalidNode));
|
||||
_module.AddName(result[component], $"rayIntersectResult{component}");
|
||||
_interfaces.Add(result[component]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EmitRayIntersectActive(Gen5RayIntersectControl ray, bool bvh64)
|
||||
{
|
||||
var result = _rayResult!;
|
||||
var nodeLow = LoadV(ray.GetAddressRegister(0));
|
||||
var nodePointer = bvh64 ? Pair64(nodeLow, LoadV(ray.GetAddressRegister(1))) : Widen(nodeLow);
|
||||
var nodeType = BitwiseAnd(nodeLow, UInt(7));
|
||||
var nodeIndex = ShiftRightLogical64(nodePointer, ULong(3));
|
||||
|
||||
// The BVH T#: base_address[39:0] in 256-byte units, box_grow_value [62:55],
|
||||
// box_sort_en [63], size [105:64] (the last valid 64-byte node index),
|
||||
// triangle_return_mode [120].
|
||||
var word0 = LoadS(ray.ScalarResource);
|
||||
var word1 = LoadS(ray.ScalarResource + 1);
|
||||
var word2 = LoadS(ray.ScalarResource + 2);
|
||||
var word3 = LoadS(ray.ScalarResource + 3);
|
||||
var baseUnits = Pair64(word0, BitwiseAnd(word1, UInt(0xFF)));
|
||||
var bvhBase = ShiftLeftLogical64(baseUnits, ULong(8));
|
||||
var lastNode = Pair64(word2, BitwiseAnd(word3, UInt(0x3FF)));
|
||||
var boxGrow = BitwiseAnd(ShiftRightLogical(word1, UInt(23)), UInt(0xFF));
|
||||
var boxSort = IsNotZero(BitwiseAnd(word1, UInt(0x8000_0000)));
|
||||
var barycentrics = IsNotZero(BitwiseAnd(word3, UInt(1u << 24)));
|
||||
var nodeAddress = IAdd64(bvhBase, ShiftLeftLogical64(And64(nodePointer, ULong(~7ul)), ULong(3)));
|
||||
|
||||
var present = _module.AddInstruction(SpirvOp.INotEqual, _boolType, baseUnits, ULong(0));
|
||||
var inRange = _module.AddInstruction(SpirvOp.ULessThanEqual, _boolType, nodeIndex, lastNode);
|
||||
// A float32 box node spans two 64-byte units.
|
||||
var wideInRange = _module.AddInstruction(SpirvOp.ULessThan, _boolType, nodeIndex, lastNode);
|
||||
var valid = LogicalAnd(present, inRange);
|
||||
var isTriangle = LogicalAnd(valid, _module.AddInstruction(SpirvOp.ULessThanEqual, _boolType, nodeType, UInt(NodeTypeTriangle1)));
|
||||
var isBox16 = LogicalAnd(valid, _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeBoxFloat16)));
|
||||
var isBox32 = LogicalAnd(LogicalAnd(present, wideInRange),
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeBoxFloat32)));
|
||||
|
||||
var extent = Bitcast(_floatType, LoadV(ray.GetAddressRegister(bvh64 ? 2 : 1)));
|
||||
var origin = new uint[3];
|
||||
for (var axis = 0; axis < 3; axis++)
|
||||
origin[axis] = Bitcast(_floatType, LoadV(ray.GetAddressRegister((bvh64 ? 3 : 2) + axis)));
|
||||
var direction = LoadRayVector(ray, bvh64, inverse: false);
|
||||
var inverseDirection = LoadRayVector(ray, bvh64, inverse: true);
|
||||
|
||||
// Other node types (user nodes 6 and 7, unused 2 and 3), a null BVH or an
|
||||
// out-of-range node return four invalid dwords.
|
||||
foreach (var variable in result)
|
||||
Store(variable, UInt(InvalidNode));
|
||||
EmitConditional(isTriangle, () =>
|
||||
{
|
||||
var values = EmitRayTriangle(nodeAddress, nodeType, barycentrics, origin, direction);
|
||||
for (var component = 0; component < values.Length; component++)
|
||||
Store(result[component], values[component]);
|
||||
});
|
||||
// Each box format reads only its own node bytes.
|
||||
void EmitBoxes(bool fp16)
|
||||
{
|
||||
var children = EmitRayBoxes(nodeAddress, fp16, boxGrow, boxSort, extent, origin, inverseDirection);
|
||||
for (var component = 0; component < children.Length; component++)
|
||||
Store(result[component], children[component]);
|
||||
}
|
||||
|
||||
EmitConditional(isBox16, () => EmitBoxes(fp16: true));
|
||||
EmitConditional(isBox32, () => EmitBoxes(fp16: false));
|
||||
|
||||
for (var component = 0u; component < Gen5RayIntersectControl.ResultDwords; component++)
|
||||
StoreV(ray.VectorData + component, Load(_uintType, result[component]));
|
||||
}
|
||||
|
||||
// Direction and inverse direction follow the origin; A16 packs both as six halves
|
||||
// into three dwords.
|
||||
private uint[] LoadRayVector(Gen5RayIntersectControl ray, bool bvh64, bool inverse)
|
||||
{
|
||||
var first = bvh64 ? 6 : 5;
|
||||
var values = new uint[3];
|
||||
if (!ray.A16)
|
||||
{
|
||||
for (var axis = 0; axis < 3; axis++)
|
||||
values[axis] = Bitcast(_floatType, LoadV(ray.GetAddressRegister(first + (inverse ? 3 : 0) + axis)));
|
||||
return values;
|
||||
}
|
||||
|
||||
for (var axis = 0; axis < 3; axis++)
|
||||
{
|
||||
var half = (inverse ? 3 : 0) + axis;
|
||||
var word = LoadV(ray.GetAddressRegister(first + half / 2));
|
||||
var bits = half % 2 == 0 ? BitwiseAnd(word, UInt(0xFFFF)) : ShiftRightLogical(word, UInt(16));
|
||||
values[axis] = Bitcast(_floatType, EmitHalfToFloat(bits));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private uint LoadNodeDword(uint nodeAddress, uint dword) =>
|
||||
LoadDeviceDword(IAdd64(nodeAddress, ULong(dword * 4ul)));
|
||||
|
||||
private uint LoadNodeFloat(uint nodeAddress, uint dword) =>
|
||||
Bitcast(_floatType, LoadNodeDword(nodeAddress, dword));
|
||||
|
||||
// fast_intersect_triangle and SwizzleBarycentrics. Node type 0 tests (v0, v1, v2),
|
||||
// type 1 tests (v1, v3, v2). A miss returns t_num = +inf over t_denom = 1. The
|
||||
// barycentric return mode yields the i/j numerators the builder's rotation maps
|
||||
// back; the other mode yields the triangle id and a hit flag.
|
||||
private uint[] EmitRayTriangle(uint nodeAddress, uint nodeType, uint barycentrics,
|
||||
IReadOnlyList<uint> origin, IReadOnlyList<uint> direction)
|
||||
{
|
||||
var second = _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeTriangle1));
|
||||
var v1 = new uint[3];
|
||||
var v2 = new uint[3];
|
||||
var v3 = new uint[3];
|
||||
for (var axis = 0u; axis < 3; axis++)
|
||||
{
|
||||
var first = LoadNodeFloat(nodeAddress, axis);
|
||||
var vertex1 = LoadNodeFloat(nodeAddress, 3 + axis);
|
||||
var vertex2 = LoadNodeFloat(nodeAddress, 6 + axis);
|
||||
var vertex3 = LoadNodeFloat(nodeAddress, 9 + axis);
|
||||
v1[axis] = SelectF(second, vertex1, first);
|
||||
v2[axis] = SelectF(second, vertex3, vertex1);
|
||||
v3[axis] = vertex2;
|
||||
}
|
||||
|
||||
var e1 = Subtract(v2, v1);
|
||||
var e2 = Subtract(v3, v1);
|
||||
var e3 = Subtract(origin, v1);
|
||||
var s1 = Cross(direction, e2);
|
||||
var s2 = Cross(e3, e1);
|
||||
var tNum = Dot(e2, s2);
|
||||
var tDenom = Dot(s1, e1);
|
||||
var iNum = Dot(e3, s1);
|
||||
var jNum = Dot(direction, s2);
|
||||
|
||||
var t = FDiv(tNum, tDenom);
|
||||
var u = FDiv(iNum, tDenom);
|
||||
var v = FDiv(jNum, tDenom);
|
||||
var zero = Float(0f);
|
||||
var one = Float(1f);
|
||||
var missed = Any(
|
||||
FCompare(SpirvOp.FOrdLessThan, u, zero),
|
||||
FCompare(SpirvOp.FOrdGreaterThan, u, one),
|
||||
FCompare(SpirvOp.FOrdLessThan, v, zero),
|
||||
FCompare(SpirvOp.FOrdGreaterThan, FAdd(u, v), one),
|
||||
FCompare(SpirvOp.FOrdLessThan, t, zero));
|
||||
tNum = SelectF(missed, Float(float.PositiveInfinity), tNum);
|
||||
tDenom = SelectF(missed, one, tDenom);
|
||||
|
||||
var triangleId = LoadNodeDword(nodeAddress, TriangleIdDword);
|
||||
var shift = ShiftLeftLogical(nodeType, UInt(3));
|
||||
uint Barycentric(int sourceShift)
|
||||
{
|
||||
var source = BitwiseAnd(ShiftRightLogical(triangleId, IAdd(shift, UInt((uint)sourceShift))), UInt(3));
|
||||
return SelectF(_module.AddInstruction(SpirvOp.IEqual, _boolType, source, UInt(1)), iNum,
|
||||
SelectF(_module.AddInstruction(SpirvOp.IEqual, _boolType, source, UInt(2)), jNum,
|
||||
FSub(FSub(tDenom, iNum), jNum)));
|
||||
}
|
||||
|
||||
var hit = LogicalNot(missed);
|
||||
return
|
||||
[
|
||||
Bitcast(_uintType, tNum),
|
||||
Bitcast(_uintType, tDenom),
|
||||
SelectU(barycentrics, Bitcast(_uintType, Barycentric(TriangleIdISourceShift)), triangleId),
|
||||
SelectU(barycentrics, Bitcast(_uintType, Barycentric(TriangleIdJSourceShift)), SelectU(hit, UInt(1), UInt(0))),
|
||||
];
|
||||
}
|
||||
|
||||
// IntersectNodeBvh4 with fast_intersect_bbox: slab test clipped to [0, extent], a
|
||||
// NaN interval misses, box_grow_value widens the exit time by that many 2^-24
|
||||
// steps, and the optional sort orders hit children by entry time.
|
||||
private uint[] EmitRayBoxes(uint nodeAddress, bool fp16, uint boxGrow, uint boxSort, uint extent,
|
||||
IReadOnlyList<uint> origin, IReadOnlyList<uint> inverseDirection)
|
||||
{
|
||||
var zero = Float(0f);
|
||||
var growFactor = FAdd(Float(1f), FMul(
|
||||
_module.AddInstruction(SpirvOp.ConvertUToF, _floatType, boxGrow), Float(5.960464478e-8f)));
|
||||
var children = new uint[4];
|
||||
var keys = new uint[4];
|
||||
for (var child = 0u; child < 4; child++)
|
||||
{
|
||||
var pointer = LoadNodeDword(nodeAddress, child);
|
||||
var enter = Float(float.NegativeInfinity);
|
||||
var leave = Float(float.PositiveInfinity);
|
||||
for (var axis = 0u; axis < 3; axis++)
|
||||
{
|
||||
var min = BoxBound(nodeAddress, fp16, child, axis);
|
||||
var max = BoxBound(nodeAddress, fp16, child, axis + 3);
|
||||
var planeMin = FMul(FSub(min, origin[(int)axis]), inverseDirection[(int)axis]);
|
||||
var planeMax = FMul(FSub(max, origin[(int)axis]), inverseDirection[(int)axis]);
|
||||
var positive = FCompare(SpirvOp.FOrdGreaterThanEqual, inverseDirection[(int)axis], zero);
|
||||
var near = SelectF(positive, planeMin, planeMax);
|
||||
var far = SelectF(positive, planeMax, planeMin);
|
||||
// HLSL max3/min3: NaN operands propagate, so the NaN check below sees them.
|
||||
enter = axis == 0 ? near : NanMax(enter, near);
|
||||
leave = axis == 0 ? far : NanMin(leave, far);
|
||||
}
|
||||
|
||||
var nan = _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, enter),
|
||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, leave));
|
||||
var minT = SelectF(nan, Float(float.PositiveInfinity), Ext(40, _floatType, enter, zero));
|
||||
var maxT = SelectF(nan, Float(float.NegativeInfinity), Ext(37, _floatType, leave, extent));
|
||||
var hit = FCompare(SpirvOp.FOrdLessThanEqual, minT, FMul(maxT, growFactor));
|
||||
children[child] = SelectU(hit, pointer, UInt(InvalidNode));
|
||||
keys[child] = minT;
|
||||
}
|
||||
|
||||
// The hardware sorting network; an invalid child always sinks.
|
||||
var sorted = (uint[])children.Clone();
|
||||
(int, int)[] network = [(0, 2), (1, 3), (0, 1), (2, 3), (1, 2)];
|
||||
foreach (var (a, b) in network)
|
||||
{
|
||||
var bValid = _module.AddInstruction(SpirvOp.INotEqual, _boolType, sorted[b], UInt(InvalidNode));
|
||||
var aInvalid = _module.AddInstruction(SpirvOp.IEqual, _boolType, sorted[a], UInt(InvalidNode));
|
||||
var swap = _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
||||
LogicalAnd(bValid, FCompare(SpirvOp.FOrdLessThan, keys[b], keys[a])), aInvalid);
|
||||
(sorted[a], sorted[b]) = (SelectU(swap, sorted[b], sorted[a]), SelectU(swap, sorted[a], sorted[b]));
|
||||
(keys[a], keys[b]) = (SelectF(swap, keys[b], keys[a]), SelectF(swap, keys[a], keys[b]));
|
||||
}
|
||||
|
||||
for (var child = 0; child < 4; child++)
|
||||
children[child] = SelectU(boxSort, sorted[child], children[child]);
|
||||
return children;
|
||||
}
|
||||
|
||||
// Bound 0-2 is the minimum, 3-5 the maximum of one child box.
|
||||
private uint BoxBound(uint nodeAddress, bool fp16, uint child, uint bound)
|
||||
{
|
||||
var index = child * 6 + bound;
|
||||
if (!fp16)
|
||||
return LoadNodeFloat(nodeAddress, BoxBoundsDword + index);
|
||||
|
||||
var packed = LoadNodeDword(nodeAddress, BoxBoundsDword + index / 2);
|
||||
var bits = index % 2 == 0 ? BitwiseAnd(packed, UInt(0xFFFF)) : ShiftRightLogical(packed, UInt(16));
|
||||
return Bitcast(_floatType, EmitHalfToFloat(bits));
|
||||
}
|
||||
|
||||
private uint NanMax(uint left, uint right) => SelectF(
|
||||
_module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
|
||||
FCompare(SpirvOp.FOrdGreaterThan, left, right)), left, right);
|
||||
|
||||
private uint NanMin(uint left, uint right) => SelectF(
|
||||
_module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
|
||||
FCompare(SpirvOp.FOrdLessThan, left, right)), left, right);
|
||||
|
||||
private uint[] Subtract(IReadOnlyList<uint> left, IReadOnlyList<uint> right) =>
|
||||
[FSub(left[0], right[0]), FSub(left[1], right[1]), FSub(left[2], right[2])];
|
||||
|
||||
private uint[] Cross(IReadOnlyList<uint> a, IReadOnlyList<uint> b) =>
|
||||
[
|
||||
FSub(FMul(a[1], b[2]), FMul(a[2], b[1])),
|
||||
FSub(FMul(a[2], b[0]), FMul(a[0], b[2])),
|
||||
FSub(FMul(a[0], b[1]), FMul(a[1], b[0])),
|
||||
];
|
||||
|
||||
private uint Dot(IReadOnlyList<uint> a, IReadOnlyList<uint> b) =>
|
||||
FAdd(FAdd(FMul(a[0], b[0]), FMul(a[1], b[1])), FMul(a[2], b[2]));
|
||||
|
||||
private uint Any(params uint[] conditions)
|
||||
{
|
||||
var result = conditions[0];
|
||||
for (var index = 1; index < conditions.Length; index++)
|
||||
result = _module.AddInstruction(SpirvOp.LogicalOr, _boolType, result, conditions[index]);
|
||||
return result;
|
||||
}
|
||||
|
||||
private uint FCompare(SpirvOp op, uint left, uint right) => _module.AddInstruction(op, _boolType, left, right);
|
||||
private uint FAdd(uint left, uint right) => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right);
|
||||
private uint FSub(uint left, uint right) => _module.AddInstruction(SpirvOp.FSub, _floatType, left, right);
|
||||
private uint FMul(uint left, uint right) => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right);
|
||||
private uint FDiv(uint left, uint right) => _module.AddInstruction(SpirvOp.FDiv, _floatType, left, right);
|
||||
private uint SelectF(uint condition, uint whenTrue, uint whenFalse) =>
|
||||
_module.AddInstruction(SpirvOp.Select, _floatType, condition, whenTrue, whenFalse);
|
||||
}
|
||||
}
|
||||
|
|
@ -115,6 +115,7 @@ public static partial class Gen5SpirvTranslator
|
|||
_localSizeX = Math.Max(request.LocalSizeX, 1);
|
||||
_localSizeY = Math.Max(request.LocalSizeY, 1);
|
||||
_localSizeZ = Math.Max(request.LocalSizeZ, 1);
|
||||
_physicalAxisOfLogical = ComputeWorkgroupAxisOrder(_localSizeX, _localSizeY, _localSizeZ);
|
||||
_emulateWave64 = _stage == Gen5SpirvStage.Compute && _waveLaneCount == 64 && (ulong)_localSizeX * _localSizeY * _localSizeZ == 64;
|
||||
_requiredVertexOutputCount = request.RequiredVertexOutputCount;
|
||||
_pixelInputEnable = request.PixelInputEnable;
|
||||
|
|
@ -322,31 +323,41 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
var isStorage = resourceClass == ImageResourceClass.Storage;
|
||||
var kind = numericClass switch
|
||||
{
|
||||
ImageNumericClass.Uint => ImageComponentKind.Uint,
|
||||
ImageNumericClass.Sint => ImageComponentKind.Sint,
|
||||
_ => ImageComponentKind.Float,
|
||||
};
|
||||
// Every atomic image (integer or float) is declared with the R32ui
|
||||
// format below, so its OpTypeImage Sampled Type must be uint too -
|
||||
// VUID-StandaloneSpirv-Image-04965 requires the two to match. The
|
||||
// float atomics (Fmin/Fmax) still operate on the real float bits;
|
||||
// they just get there through a bitcast + integer compare-exchange
|
||||
// (see Gen5SpirvTranslator.cs's ImageAtomicFmax/Fmin case) instead
|
||||
// of through the declared image/sampled type.
|
||||
var kind = atomic
|
||||
? ImageComponentKind.Uint
|
||||
: numericClass switch
|
||||
{
|
||||
ImageNumericClass.Uint => ImageComponentKind.Uint,
|
||||
ImageNumericClass.Sint => ImageComponentKind.Sint,
|
||||
_ => ImageComponentKind.Float,
|
||||
};
|
||||
var componentType = kind switch
|
||||
{
|
||||
ImageComponentKind.Sint => _intType,
|
||||
ImageComponentKind.Uint => _uintType,
|
||||
_ => _floatType,
|
||||
};
|
||||
// The image request builder binds guest 1D textures through 1D views,
|
||||
// so the shader declaration must be 1D as well.
|
||||
var spirvDimension = dimension switch
|
||||
{
|
||||
ImageDimension.Dim1D or ImageDimension.Dim1DArray => SpirvImageDim.Dim1D,
|
||||
ImageDimension.Dim3D => SpirvImageDim.Dim3D,
|
||||
_ => SpirvImageDim.Dim2D,
|
||||
};
|
||||
var arrayed = dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray;
|
||||
var multisampled = dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray;
|
||||
if (spirvDimension == SpirvImageDim.Dim1D)
|
||||
{
|
||||
_module.AddCapability(isStorage ? SpirvCapability.Image1D : SpirvCapability.Sampled1D);
|
||||
}
|
||||
|
||||
var arrayed = dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray;
|
||||
var multisampled = dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray;
|
||||
var format = atomic ? SpirvImageFormat.R32ui : SpirvImageFormat.Unknown;
|
||||
if (isStorage && !atomic)
|
||||
{
|
||||
|
|
@ -772,16 +783,44 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
else if (entry.Kind == MemoryResourceKind.ScalarBuffer)
|
||||
{
|
||||
if (entry.Resource == MemoryAccessInfo.NoResource)
|
||||
if (entry.Resource != MemoryAccessInfo.NoResource)
|
||||
{
|
||||
error = "scalar buffer load has no dense buffer";
|
||||
return false;
|
||||
var bindingIndex = (int)entry.Resource;
|
||||
var byteAddress = IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint))));
|
||||
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
|
||||
value = LoadBufferWord(bindingIndex, ShiftRightLogical(byteAddress, UInt(2)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// A runtime SRT V#: its SGPRs hold the descriptor, so build the
|
||||
// guest address from the base and go through the device-address
|
||||
// page table, bounded by the descriptor's stride and record count.
|
||||
if (instruction.Sources.Count == 0 || instruction.Sources[0].Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
error = "runtime scalar buffer load has no scalar base";
|
||||
return false;
|
||||
}
|
||||
|
||||
var bindingIndex = (int)entry.Resource;
|
||||
var byteAddress = IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint))));
|
||||
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
|
||||
value = LoadBufferWord(bindingIndex, ShiftRightLogical(byteAddress, UInt(2)));
|
||||
var baseRegister = instruction.Sources[0].Value;
|
||||
var descriptorWord1 = LoadS(baseRegister + 1);
|
||||
var stride = BitwiseAnd(ShiftRightLogical(descriptorWord1, UInt(16)), UInt(0x3FFF));
|
||||
var records = LoadS(baseRegister + 2);
|
||||
var size = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
|
||||
records,
|
||||
_module.AddInstruction(SpirvOp.IMul, _uintType, stride, records));
|
||||
var baseAddress = Pair64(LoadS(baseRegister), BitwiseAnd(descriptorWord1, UInt(0xFFFF)));
|
||||
var byteOffset = IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint))));
|
||||
var address = And64(IAdd64(baseAddress, Widen(byteOffset)), ULong(DeviceAddressMask & ~3ul));
|
||||
var inRange = _module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
ShiftRightLogical(byteOffset, UInt(2)),
|
||||
ShiftRightLogical(size, UInt(2)));
|
||||
value = LoadBoundedDeviceDword(address, inRange);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -846,6 +885,11 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
address = IAdd64(address, SignedOffset64(control.OffsetBytes));
|
||||
var entry = request.Memory[memoryIndex];
|
||||
if (control.UsesFlatAddress && entry.AddressSpace != FlatAddressSpace.Global)
|
||||
{
|
||||
return TryEmitFlatLocalMemory(instruction, control, memoryOpcode, entry.AddressSpace, address, out error);
|
||||
}
|
||||
|
||||
var writes = entry.Access is MemoryAccess.Write or MemoryAccess.Atomic;
|
||||
var accessBytes = Math.Max((entry.DataBits + 7) / 8, 1u) * Math.Max(entry.DataDwords, 1u);
|
||||
var allowed = writes ? IsWrittenAccessAllowed(memoryIndex, address, accessBytes) : _module.ConstantBool(true);
|
||||
|
|
@ -927,6 +971,100 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
// Routes each lane of an aperture-derived FLAT access like the hardware: LDS or
|
||||
// scratch at the low dword when the high dword is in an aperture, otherwise a
|
||||
// global access through the page table.
|
||||
private bool TryEmitFlatLocalMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5GlobalMemoryControl control,
|
||||
string memoryOpcode,
|
||||
FlatAddressSpace addressSpace,
|
||||
uint address,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var store = memoryOpcode.StartsWith("GlobalStoreDword", StringComparison.Ordinal);
|
||||
var load = memoryOpcode.StartsWith("GlobalLoadDword", StringComparison.Ordinal);
|
||||
if (!store && !load)
|
||||
{
|
||||
error = $"{instruction.Opcode} through the {addressSpace} flat aperture is not implemented";
|
||||
return false;
|
||||
}
|
||||
|
||||
var canShare = addressSpace is FlatAddressSpace.Shared or FlatAddressSpace.SharedOrPrivate;
|
||||
var canPrivate = addressSpace is FlatAddressSpace.Private or FlatAddressSpace.SharedOrPrivate;
|
||||
if (canShare && _lds == 0)
|
||||
{
|
||||
error = "a shared flat aperture access has no LDS storage";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canPrivate && (_scratch == 0 || _scratchDwordCount == 0))
|
||||
{
|
||||
error = "a private flat aperture access has no scratch storage";
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = _module.AddInstruction(SpirvOp.UConvert, _uintType, address);
|
||||
var aperture = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
ShiftRightLogical64(address, _module.Constant64(_ulongType, 32 + Gen5InlineConstants.ApertureShift)));
|
||||
var isShared = _module.AddInstruction(SpirvOp.IEqual, _boolType, aperture,
|
||||
UInt(Gen5InlineConstants.SharedApertureHigh >> Gen5InlineConstants.ApertureShift));
|
||||
var isPrivate = _module.AddInstruction(SpirvOp.IEqual, _boolType, aperture,
|
||||
UInt(Gen5InlineConstants.PrivateApertureHigh >> Gen5InlineConstants.ApertureShift));
|
||||
|
||||
void Shared()
|
||||
{
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
var pointer = LdsPointer(offset, index * sizeof(uint));
|
||||
if (store)
|
||||
Store(pointer, LoadV(control.SourceVectorRegister + index));
|
||||
else
|
||||
StoreV(control.DestinationVectorRegister + index, Load(_uintType, pointer));
|
||||
}
|
||||
}
|
||||
|
||||
void Private()
|
||||
{
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
var pointer = ScratchPointer(offset, index * sizeof(uint));
|
||||
if (store)
|
||||
Store(pointer, LoadV(control.SourceVectorRegister + index));
|
||||
else
|
||||
StoreV(control.DestinationVectorRegister + index, Load(_uintType, pointer));
|
||||
}
|
||||
}
|
||||
|
||||
void Global()
|
||||
{
|
||||
var aligned = And64(address, ULong(~3ul));
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
var dwordAddress = index == 0 ? aligned : IAdd64(aligned, ULong((ulong)index * sizeof(uint)));
|
||||
if (store)
|
||||
StoreDeviceDword(dwordAddress, LoadV(control.SourceVectorRegister + index), _module.ConstantBool(true));
|
||||
else
|
||||
StoreV(control.DestinationVectorRegister + index, LoadDeviceDword(dwordAddress));
|
||||
}
|
||||
}
|
||||
|
||||
// Inactive lanes must not touch any of the three storages.
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
if (canShare && canPrivate)
|
||||
EmitConditional(isShared, Shared, () => EmitConditional(isPrivate, Private, Global));
|
||||
else if (canShare)
|
||||
EmitConditional(isShared, Shared, Global);
|
||||
else
|
||||
EmitConditional(isPrivate, Private, Global);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryEmitScratchMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5GlobalMemoryControl control,
|
||||
|
|
@ -942,6 +1080,10 @@ public static partial class Gen5SpirvTranslator
|
|||
var address = control.ScalarAddress < 125
|
||||
? LoadS(control.ScalarAddress)
|
||||
: LoadV(control.VectorAddress);
|
||||
if (control.DynamicOffsetRegister is { } dynamicOffsetRegister)
|
||||
{
|
||||
address = IAdd(address, LoadS(dynamicOffsetRegister));
|
||||
}
|
||||
if (control.OffsetBytes != 0)
|
||||
{
|
||||
address = IAdd(address, UInt(unchecked((uint)control.OffsetBytes)));
|
||||
|
|
@ -961,7 +1103,9 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
Store(
|
||||
ScratchPointer(address, index * sizeof(uint)),
|
||||
LoadV(control.SourceVectorRegister + index));
|
||||
control.SourceIsScalar
|
||||
? LoadS(control.SourceVectorRegister + index)
|
||||
: LoadV(control.SourceVectorRegister + index));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
|
|
@ -1085,6 +1229,160 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
// BufferLoweringStrategy.PhysicalStorageBuffer: a raw buffer access whose
|
||||
// V# was read from a runtime scalar address. The descriptor dwords live in
|
||||
// the SGPRs named by the instruction, so build the guest address from the
|
||||
// runtime base and go through the device-address page table.
|
||||
// num_records/stride bound the access, so a null or empty descriptor binds
|
||||
// nothing, matching the static path's range check. Formatted/typed/atomic
|
||||
// accesses never reach here; the chooser escalates them to
|
||||
// BoundedCandidateTable.
|
||||
private bool TryEmitPhysicalStorageBufferMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (control.Typed || instruction.Opcode.Contains("Format", StringComparison.Ordinal))
|
||||
{
|
||||
error = $"runtime buffer descriptor does not support the formatted access {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
|
||||
{
|
||||
error = $"runtime buffer descriptor does not support the atomic access {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Sources.Count < 2 ||
|
||||
instruction.Sources[1].Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
error = "runtime buffer descriptor has no scalar resource base";
|
||||
return false;
|
||||
}
|
||||
|
||||
var srsrc = instruction.Sources[1].Value;
|
||||
var scalarOffset = instruction.Sources.Count > 2
|
||||
? GetRawSource(instruction, 2)
|
||||
: UInt(0);
|
||||
var vectorIndex = control.IndexEnabled ? LoadV(control.VectorAddress) : UInt(0);
|
||||
var vectorOffset = control.OffsetEnabled
|
||||
? LoadV(control.VectorAddress + (control.IndexEnabled ? 1u : 0u))
|
||||
: UInt(0);
|
||||
|
||||
var descriptorWord1 = LoadS(srsrc + 1);
|
||||
var stride = BitwiseAnd(ShiftRightLogical(descriptorWord1, UInt(16)), UInt(0x3FFF));
|
||||
var byteOffset = IAdd(UInt(unchecked((uint)control.OffsetBytes)), scalarOffset);
|
||||
byteOffset = IAdd(byteOffset, vectorOffset);
|
||||
byteOffset = IAdd(
|
||||
byteOffset,
|
||||
_module.AddInstruction(SpirvOp.IMul, _uintType, vectorIndex, stride));
|
||||
|
||||
// size = stride == 0 ? num_records : stride * num_records, in bytes.
|
||||
var records = LoadS(srsrc + 2);
|
||||
var size = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
|
||||
records,
|
||||
_module.AddInstruction(SpirvOp.IMul, _uintType, stride, records));
|
||||
|
||||
var dwordOffset = ShiftRightLogical(byteOffset, UInt(2));
|
||||
var dwordSize = ShiftRightLogical(size, UInt(2));
|
||||
var baseAddress = Pair64(
|
||||
LoadS(srsrc),
|
||||
BitwiseAnd(descriptorWord1, UInt(0xFFFF)));
|
||||
var address = IAdd64(baseAddress, Widen(byteOffset));
|
||||
|
||||
uint InRange(uint index)
|
||||
{
|
||||
var dword = index == 0 ? dwordOffset : IAdd(dwordOffset, UInt(index));
|
||||
return _module.AddInstruction(SpirvOp.ULessThan, _boolType, dword, dwordSize);
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.StartsWith("BufferStoreByte", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.StartsWith("BufferStoreShort", StringComparison.Ordinal))
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
if (TryGetSubdwordStoreInfo(instruction.Opcode, out var byteCount, out var sourceShift))
|
||||
{
|
||||
StoreDeviceBytes(
|
||||
address,
|
||||
LoadV(control.VectorData),
|
||||
byteCount,
|
||||
sourceShift,
|
||||
InRange(0));
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
StoreDeviceDword(
|
||||
index == 0 ? address : IAdd64(address, ULong((ulong)index * sizeof(uint))),
|
||||
LoadV(control.VectorData + index),
|
||||
InRange(index));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetSubdwordLoadInfo(
|
||||
instruction.Opcode,
|
||||
out var loadByteCount,
|
||||
out var signExtend,
|
||||
out var d16,
|
||||
out var d16High))
|
||||
{
|
||||
StoreV(
|
||||
control.VectorData,
|
||||
LoadSubdwordDeviceValue(
|
||||
address,
|
||||
LoadV(control.VectorData),
|
||||
loadByteCount,
|
||||
signExtend,
|
||||
d16,
|
||||
d16High));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!instruction.Opcode.StartsWith("BufferLoad", StringComparison.Ordinal) &&
|
||||
!instruction.Opcode.StartsWith("TBufferLoad", StringComparison.Ordinal))
|
||||
{
|
||||
error = $"unsupported runtime buffer opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
StoreV(
|
||||
control.VectorData + index,
|
||||
LoadBoundedDeviceDword(
|
||||
index == 0 ? address : IAdd64(address, ULong((ulong)index * sizeof(uint))),
|
||||
InRange(index)));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private uint LoadBoundedDeviceDword(uint address64, uint inRange)
|
||||
{
|
||||
var (pointer, valid) = ResolveDeviceAddress(address64);
|
||||
Store(_deviceWordScratch, UInt(0));
|
||||
EmitConditional(LogicalAnd(inRange, valid), () =>
|
||||
Store(
|
||||
_deviceWordScratch,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Load,
|
||||
_uintType,
|
||||
DeviceWordPointer(pointer),
|
||||
2u,
|
||||
4u)));
|
||||
return Load(_uintType, _deviceWordScratch);
|
||||
}
|
||||
|
||||
// ---- images ----
|
||||
|
||||
// An access over several descriptors takes one constant-element case per descriptor:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,38 @@ public static partial class Gen5SpirvTranslator
|
|||
uint numberType) =>
|
||||
CompilationContext.DecodeStorageImageFormat(dataFormat, numberType);
|
||||
|
||||
// Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64) and
|
||||
// maxComputeWorkGroupInvocations >= 1024, but a GNM NUM_THREAD layout can put its
|
||||
// largest axis on Z (real PS5 hardware has no such asymmetry between axes). Remap
|
||||
// logical GNM axes onto physical SPIR-V/dispatch axes so the largest logical size
|
||||
// always lands on the physical axis with the most headroom (X), and the smallest
|
||||
// lands on Z, which is the only axis with a tight guaranteed limit. This is a pure
|
||||
// function of the three thread-group sizes so the host dispatch path (which permutes
|
||||
// vkCmdDispatch's group counts the same way) can recompute it independently without
|
||||
// any shader-compiler metadata plumbing.
|
||||
//
|
||||
// Returns physicalAxisOfLogical: physicalAxisOfLogical[logical 0=X/1=Y/2=Z] is the
|
||||
// physical axis (0=X/1=Y/2=Z) that logical axis is remapped onto.
|
||||
public static int[] ComputeWorkgroupAxisOrder(uint sizeX, uint sizeY, uint sizeZ)
|
||||
{
|
||||
if (sizeZ <= 64)
|
||||
{
|
||||
return [0, 1, 2];
|
||||
}
|
||||
|
||||
var sizes = new[] { sizeX, sizeY, sizeZ };
|
||||
var byDescendingSize = new[] { 0, 1, 2 };
|
||||
Array.Sort(byDescendingSize, (a, b) => sizes[b].CompareTo(sizes[a]));
|
||||
|
||||
var physicalAxisOfLogical = new int[3];
|
||||
for (var physical = 0; physical < 3; physical++)
|
||||
{
|
||||
physicalAxisOfLogical[byDescendingSize[physical]] = physical;
|
||||
}
|
||||
|
||||
return physicalAxisOfLogical;
|
||||
}
|
||||
|
||||
private sealed partial class CompilationContext
|
||||
{
|
||||
private const int ScalarRegisterCount = 128;
|
||||
|
|
@ -80,6 +112,13 @@ public static partial class Gen5SpirvTranslator
|
|||
private readonly uint _localSizeX;
|
||||
private readonly uint _localSizeY;
|
||||
private readonly uint _localSizeZ;
|
||||
|
||||
// Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64), but a GNM
|
||||
// NUM_THREAD layout can put its largest axis on Z (real hardware has no such asymmetry).
|
||||
// physicalAxisOfLogical[logical 0=X/1=Y/2=Z] gives which physical SPIR-V axis (and which
|
||||
// vkCmdDispatch group-count slot) that logical axis is remapped onto, so the largest size
|
||||
// always lands on the physical axis with the most headroom.
|
||||
private readonly int[] _physicalAxisOfLogical;
|
||||
private readonly uint _pixelInputEnable;
|
||||
private readonly uint _pixelInputAddress;
|
||||
private readonly uint[] _pixelInputCntl;
|
||||
|
|
@ -122,6 +161,7 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint _storageUintPointer;
|
||||
private uint _lds;
|
||||
private uint _ldsElementPointer;
|
||||
private uint _lds64ElementPointer;
|
||||
private uint _ldsDwordMask;
|
||||
private uint _scratch;
|
||||
private uint _scratchElementPointer;
|
||||
|
|
@ -172,11 +212,11 @@ public static partial class Gen5SpirvTranslator
|
|||
ImageComponentKind ComponentKind,
|
||||
bool IsStorage,
|
||||
bool Arrayed,
|
||||
bool Cube,
|
||||
bool Multisampled,
|
||||
SpirvImageDim Dimension,
|
||||
uint ConversionFormat,
|
||||
uint ShaderSwizzle,
|
||||
bool Cube = false,
|
||||
bool Multisampled = false,
|
||||
SpirvImageDim Dimension = SpirvImageDim.Dim2D,
|
||||
uint ConversionFormat = 0,
|
||||
uint ShaderSwizzle = 0,
|
||||
int EmulatedCompareFunction = -1);
|
||||
|
||||
private readonly record struct SpirvVertexInput(
|
||||
|
|
@ -428,12 +468,19 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
else if (_stage == Gen5SpirvStage.Compute)
|
||||
{
|
||||
var logicalSizes = new[] { _localSizeX, _localSizeY, _localSizeZ };
|
||||
var physicalSizes = new uint[3];
|
||||
for (var logical = 0; logical < 3; logical++)
|
||||
{
|
||||
physicalSizes[_physicalAxisOfLogical[logical]] = logicalSizes[logical];
|
||||
}
|
||||
|
||||
_module.AddExecutionMode(
|
||||
main,
|
||||
SpirvExecutionMode.LocalSize,
|
||||
_localSizeX,
|
||||
_localSizeY,
|
||||
_localSizeZ);
|
||||
physicalSizes[0],
|
||||
physicalSizes[1],
|
||||
physicalSizes[2]);
|
||||
}
|
||||
|
||||
var attributeCount = _stage == Gen5SpirvStage.Vertex
|
||||
|
|
@ -576,7 +623,9 @@ public static partial class Gen5SpirvTranslator
|
|||
private void DeclareScratch()
|
||||
{
|
||||
if (!_request.Program.Instructions.Any(static instruction =>
|
||||
instruction.Opcode.StartsWith("Scratch", StringComparison.Ordinal)))
|
||||
instruction.Opcode.StartsWith("Scratch", StringComparison.Ordinal)) &&
|
||||
!_request.Memory.Entries.Any(static memory =>
|
||||
memory.AddressSpace is FlatAddressSpace.Private or FlatAddressSpace.SharedOrPrivate))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -661,6 +710,7 @@ public static partial class Gen5SpirvTranslator
|
|||
var ldsArrayType = _module.TypeArray(_uintType, dwordCount);
|
||||
var ldsPointer = _module.TypePointer(storageClass, ldsArrayType);
|
||||
_ldsElementPointer = _module.TypePointer(storageClass, _uintType);
|
||||
_lds64ElementPointer = _module.TypePointer(storageClass, _ulongType);
|
||||
_lds = storageClass == SpirvStorageClass.Workgroup
|
||||
? _module.AddGlobalVariable(ldsPointer, storageClass)
|
||||
: _module.AddGlobalVariable(
|
||||
|
|
@ -855,6 +905,8 @@ public static partial class Gen5SpirvTranslator
|
|||
for (var index = 0; index < attributes.Length; index++)
|
||||
{
|
||||
var attribute = attributes[index];
|
||||
// V_INTERP_MOV reads one vertex of the primitive, so its attribute is a
|
||||
// per-vertex array rather than an interpolated value.
|
||||
var variable = _module.AddGlobalVariable(
|
||||
_perVertexAttributes.Contains(attribute)
|
||||
? _module.TypePointer(SpirvStorageClass.Input, _module.TypeArray(_vec4Type, 3))
|
||||
|
|
@ -1074,18 +1126,23 @@ public static partial class Gen5SpirvTranslator
|
|||
var invocationInBounds = _module.ConstantBool(true);
|
||||
for (uint component = 0; component < 3; component++)
|
||||
{
|
||||
// gl_LocalInvocationId/gl_WorkGroupId are indexed by the physical SPIR-V
|
||||
// axis, which can differ from the logical GNM axis when the workgroup was
|
||||
// remapped to respect the device's tighter Z-axis limit (see
|
||||
// ComputeWorkgroupAxisOrder).
|
||||
var physicalComponent = (uint)_physicalAxisOfLogical[component];
|
||||
var localComponent = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_uintType,
|
||||
localId,
|
||||
component);
|
||||
physicalComponent);
|
||||
StoreV(component, localComponent, guardWithExec: false);
|
||||
|
||||
var groupComponent = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_uintType,
|
||||
workGroupId,
|
||||
component);
|
||||
physicalComponent);
|
||||
var localSize = component switch
|
||||
{
|
||||
0 => _localSizeX,
|
||||
|
|
@ -1120,15 +1177,15 @@ public static partial class Gen5SpirvTranslator
|
|||
StoreComputeSystemRegister(
|
||||
registers.WorkGroupXRegister,
|
||||
workGroupId,
|
||||
0);
|
||||
(uint)_physicalAxisOfLogical[0]);
|
||||
StoreComputeSystemRegister(
|
||||
registers.WorkGroupYRegister,
|
||||
workGroupId,
|
||||
1);
|
||||
(uint)_physicalAxisOfLogical[1]);
|
||||
StoreComputeSystemRegister(
|
||||
registers.WorkGroupZRegister,
|
||||
workGroupId,
|
||||
2);
|
||||
(uint)_physicalAxisOfLogical[2]);
|
||||
if (registers.ThreadGroupSizeRegister is { } sizeRegister)
|
||||
{
|
||||
StoreS(
|
||||
|
|
@ -1172,6 +1229,8 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
if ((_pixelInputAddress & (1u << bit)) != 0)
|
||||
{
|
||||
// Shaders that interpolate by hand (P1/P2 on per-vertex attributes) read
|
||||
// the I/J barycentrics from these registers.
|
||||
if (_barycentricInputs.TryGetValue(bit, out var barycentricInput))
|
||||
{
|
||||
var coordinates = LoadBarycentricCoordinates(bit, barycentricInput);
|
||||
|
|
@ -1182,6 +1241,7 @@ public static partial class Gen5SpirvTranslator
|
|||
StoreV(vgpr + component, Bitcast(_uintType, coordinate), guardWithExec: false);
|
||||
}
|
||||
}
|
||||
|
||||
vgpr += dwordCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -1433,8 +1493,11 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
if (_stage == Gen5SpirvStage.Compute)
|
||||
{
|
||||
// s_waitcnt vmcnt(0) + s_barrier also publishes buffer and image
|
||||
// stores to the workgroup: AcquireRelease over uniform, workgroup
|
||||
// and image memory.
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(0x108);
|
||||
var semantics = UInt(0x8 | 0x40 | 0x100 | 0x800);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
|
|
@ -1461,7 +1524,8 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
if (instruction.Control is Gen5RayIntersectControl rayIntersect)
|
||||
{
|
||||
EmitRayIntersectMiss(rayIntersect);
|
||||
_deviceAddressInstructionPc = instruction.Pc;
|
||||
EmitRayIntersect(rayIntersect, instruction.Opcode == "ImageBvh64IntersectRay");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1540,6 +1604,8 @@ public static partial class Gen5SpirvTranslator
|
|||
case "DsAppend":
|
||||
case "DsConsume":
|
||||
return TryEmitDataShareWaveCounter(instruction, control, out error);
|
||||
case "DsMskorB32":
|
||||
return TryEmitDataShareAtomic(instruction, control, out error);
|
||||
case "DsWriteB32":
|
||||
case "DsWriteAddtidB32":
|
||||
{
|
||||
|
|
@ -1575,6 +1641,92 @@ public static partial class Gen5SpirvTranslator
|
|||
GetRawSource(instruction, 2));
|
||||
return true;
|
||||
}
|
||||
case "DsAddU64":
|
||||
case "DsOrB64":
|
||||
{
|
||||
if (instruction.Sources.Count < 3)
|
||||
{
|
||||
error = "missing LDS 64-bit atomic source";
|
||||
return false;
|
||||
}
|
||||
|
||||
var atomicAddress = GetRawSource(instruction, 0);
|
||||
var isOr = instruction.Opcode == "DsOrB64";
|
||||
// The LDS allocation is a uint array. Reinterpreting its
|
||||
// pointer as ulong with OpBitcast is invalid SPIR-V, so use
|
||||
// the dword fallback even when native 64-bit atomics exist.
|
||||
// The pair is not atomic against concurrent 64-bit use.
|
||||
var lowPointer = LdsPointer(
|
||||
atomicAddress,
|
||||
control.SingleOffsetBytes);
|
||||
var highPointer = LdsPointer(
|
||||
atomicAddress,
|
||||
control.SingleOffsetBytes + sizeof(uint));
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var lowValue = GetRawSource(instruction, 1);
|
||||
var highValue = GetRawSource(instruction, 2);
|
||||
if (isOr)
|
||||
{
|
||||
// OR is bitwise-independent: no dword influences the
|
||||
// other, so two 32-bit ORs are the 64-bit OR.
|
||||
EmitAtomic(
|
||||
SpirvOp.AtomicOr,
|
||||
_uintType,
|
||||
lowPointer,
|
||||
scope: 2,
|
||||
semantics: 0x108,
|
||||
value: () => lowValue,
|
||||
comparator: () => lowValue);
|
||||
EmitAtomic(
|
||||
SpirvOp.AtomicOr,
|
||||
_uintType,
|
||||
highPointer,
|
||||
scope: 2,
|
||||
semantics: 0x108,
|
||||
value: () => highValue,
|
||||
comparator: () => highValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// ADD needs the carry out of the low dword folded into
|
||||
// the high one. Each lane detects its own wrap from the
|
||||
// pre-add value its atomic returned and adds exactly one
|
||||
// carry, so the high dword ends up with the true count of
|
||||
// wraps however the lanes interleave.
|
||||
var originalLow = EmitAtomic(
|
||||
SpirvOp.AtomicIAdd,
|
||||
_uintType,
|
||||
lowPointer,
|
||||
scope: 2,
|
||||
semantics: 0x108,
|
||||
value: () => lowValue,
|
||||
comparator: () => lowValue);
|
||||
var sumLow = IAdd(originalLow, lowValue);
|
||||
var wrapped = _module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
sumLow,
|
||||
originalLow);
|
||||
var carry = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
wrapped,
|
||||
UInt(1),
|
||||
UInt(0));
|
||||
var highWithCarry = IAdd(highValue, carry);
|
||||
EmitAtomic(
|
||||
SpirvOp.AtomicIAdd,
|
||||
_uintType,
|
||||
highPointer,
|
||||
scope: 2,
|
||||
semantics: 0x108,
|
||||
value: () => highWithCarry,
|
||||
comparator: () => highWithCarry);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
case "DsWriteB96":
|
||||
case "DsWriteB128":
|
||||
{
|
||||
|
|
@ -1898,6 +2050,40 @@ public static partial class Gen5SpirvTranslator
|
|||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (instruction.Opcode == "DsMskorB32")
|
||||
{
|
||||
if (instruction.Sources.Count < 3)
|
||||
{
|
||||
error = "missing LDS masked-OR source";
|
||||
return false;
|
||||
}
|
||||
|
||||
var maskedPointer = LdsPointer(
|
||||
GetRawSource(instruction, 0),
|
||||
control.SingleOffsetBytes);
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var original = Load(_uintType, maskedPointer);
|
||||
var updated = BitwiseOr(
|
||||
BitwiseAnd(
|
||||
original,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
GetRawSource(instruction, 1))),
|
||||
GetRawSource(instruction, 2));
|
||||
EmitAtomic(
|
||||
SpirvOp.AtomicCompareExchange,
|
||||
_uintType,
|
||||
maskedPointer,
|
||||
scope: 2,
|
||||
semantics: 0x108,
|
||||
value: () => updated,
|
||||
comparator: () => original);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode is "DsMinF32" or "DsMaxF32")
|
||||
{
|
||||
if (instruction.Sources.Count < 3)
|
||||
|
|
@ -2208,6 +2394,13 @@ public static partial class Gen5SpirvTranslator
|
|||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (instruction.Opcode is "BufferWbinvl1" or "BufferWbinvl1Vol")
|
||||
{
|
||||
// The host storage path is already coherent; the guest instruction
|
||||
// invalidates a cache level and does not access the buffer itself.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (control.Typed && instruction.Opcode.Contains("D16", StringComparison.Ordinal))
|
||||
{
|
||||
error = $"unsupported buffer opcode {instruction.Opcode}";
|
||||
|
|
@ -2225,23 +2418,65 @@ public static partial class Gen5SpirvTranslator
|
|||
return TryEmitDeviceDescriptorBufferMemory(instruction, control, out error);
|
||||
}
|
||||
|
||||
int bindingIndex;
|
||||
uint stride;
|
||||
uint descriptorWord3;
|
||||
uint descriptorFormat;
|
||||
if (_request.Memory.TryGetIndex(instruction.Pc, 0, out var accessMemoryIndex))
|
||||
{
|
||||
// The dense buffer, its stride and its format come from the specialization.
|
||||
if (!TryResolveLayoutBuffer(instruction.Pc, out bindingIndex, out var specialized))
|
||||
var accessMemory = _request.Memory[accessMemoryIndex];
|
||||
if (accessMemory.PlanningOnly)
|
||||
{
|
||||
error = "missing buffer-memory binding";
|
||||
return false;
|
||||
// The access belongs to the linearized tail of a fused
|
||||
// shader. Its descriptor is supplied by the continuation
|
||||
// object and is intentionally absent from this plan; keep
|
||||
// the dead linear instruction side-effect free.
|
||||
return true;
|
||||
}
|
||||
|
||||
stride = UInt(specialized.PackedStride & 0x3FFF);
|
||||
descriptorFormat = specialized.DescriptorFormat;
|
||||
descriptorWord3 = UInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF));
|
||||
var strategy = accessMemory.BufferDescriptor?.ChooseStrategy(
|
||||
control.Typed,
|
||||
accessMemory.Formatted,
|
||||
accessMemory.Access == MemoryAccess.Atomic)
|
||||
?? BufferLoweringStrategy.NativeBinding;
|
||||
if (strategy == BufferLoweringStrategy.PhysicalStorageBuffer)
|
||||
{
|
||||
return TryEmitPhysicalStorageBufferMemory(instruction, control, out error);
|
||||
}
|
||||
|
||||
if (strategy == BufferLoweringStrategy.BoundedCandidateTable)
|
||||
{
|
||||
return TryEmitBoundedCandidateTableMemory(instruction, control, accessMemoryIndex, out error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryResolveLayoutBuffer(instruction.Pc, out var bindingIndex, out _))
|
||||
{
|
||||
error = "missing buffer-memory binding";
|
||||
return false;
|
||||
}
|
||||
|
||||
return EmitResolvedBufferMemory(instruction, control, bindingIndex, out error);
|
||||
}
|
||||
|
||||
// One buffer operation against a resolved candidate binding: the dense binding path
|
||||
// and every arm of a bounded candidate table share it, so the operation itself
|
||||
// stays independent of how its descriptor was selected.
|
||||
private bool EmitResolvedBufferMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
int bindingIndex,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var info = _request.Resources.Info;
|
||||
if (bindingIndex < 0 || bindingIndex >= info.Buffers.Count)
|
||||
{
|
||||
error = $"buffer binding {bindingIndex} is out of range";
|
||||
return false;
|
||||
}
|
||||
|
||||
var specialized = info.Buffers[bindingIndex];
|
||||
var stride = UInt(specialized.PackedStride & 0x3FFF);
|
||||
var descriptorFormat = specialized.DescriptorFormat;
|
||||
var descriptorWord3 = UInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF));
|
||||
|
||||
var scalarOffset = instruction.Sources.Count > 2
|
||||
? GetRawSource(instruction, 2)
|
||||
: UInt(0);
|
||||
|
|
@ -2261,6 +2496,44 @@ public static partial class Gen5SpirvTranslator
|
|||
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
|
||||
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
|
||||
|
||||
if (instruction.Opcode is "BufferAtomicSwapX2" or "BufferAtomicOrX2")
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var firstInRange = IsBufferWordInRange(bindingIndex, dwordAddress);
|
||||
var secondAddress = IAdd(dwordAddress, UInt(1));
|
||||
var secondInRange = IsBufferWordInRange(bindingIndex, secondAddress);
|
||||
EmitConditional(LogicalAnd(firstInRange, secondInRange), () =>
|
||||
{
|
||||
var atomicOp = instruction.Opcode == "BufferAtomicSwapX2"
|
||||
? SpirvOp.AtomicExchange
|
||||
: SpirvOp.AtomicOr;
|
||||
var first = EmitAtomic(
|
||||
atomicOp,
|
||||
_uintType,
|
||||
BufferWordPointer(bindingIndex, dwordAddress),
|
||||
scope: 1,
|
||||
semantics: 0x48,
|
||||
value: () => LoadV(control.VectorData),
|
||||
comparator: () => UInt(0));
|
||||
var second = EmitAtomic(
|
||||
atomicOp,
|
||||
_uintType,
|
||||
BufferWordPointer(bindingIndex, secondAddress),
|
||||
scope: 1,
|
||||
semantics: 0x48,
|
||||
value: () => LoadV(control.VectorData + 1),
|
||||
comparator: () => UInt(0));
|
||||
if (control.Glc)
|
||||
{
|
||||
StoreV(control.VectorData, first);
|
||||
StoreV(control.VectorData + 1, second);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
|
||||
{
|
||||
var atomicSuffix = instruction.Opcode["BufferAtomic".Length..];
|
||||
|
|
@ -2481,6 +2754,114 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
// BufferLoweringStrategy.BoundedCandidateTable: a runtime V# whose descriptors cannot
|
||||
// be reconstructed from its raw words. The runtime V# sits in the scalar resource
|
||||
// registers; its base-address dword is the probe key into the flattened candidate
|
||||
// mapping, and each arm binds a native candidate and runs the ordinary buffer op.
|
||||
private bool TryEmitBoundedCandidateTableMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
int memoryIndex,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (!_request.BufferCandidateTableByMemoryIndex.TryGetValue(memoryIndex, out var table))
|
||||
{
|
||||
// A formatted load needs a statically known Vulkan view format. Most
|
||||
// runtime V#s prove a bounded SRT candidate set above, but a few Yotei
|
||||
// material-lookup paths merge descriptors through control flow and have
|
||||
// no finite, safe candidate table. Do not reinterpret an arbitrary
|
||||
// device address with a guessed format: a null read is the defined
|
||||
// fallback, matching the bindless-image fallback in ResourceTracker.
|
||||
if (instruction.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal))
|
||||
{
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
StoreV(control.VectorData + index, UInt(0));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
error = $"runtime buffer descriptor has no candidate table for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (table.CandidateCount == 0)
|
||||
{
|
||||
error = "runtime buffer descriptor candidate table is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (table.CandidateCount == 1)
|
||||
{
|
||||
return EmitResolvedBufferMemory(instruction, control, (int)table.FirstCandidate, out error);
|
||||
}
|
||||
|
||||
if (!HasFlattenedTable)
|
||||
{
|
||||
error = "runtime buffer descriptor candidate table without a flattened table binding";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Sources.Count < 2 || instruction.Sources[1].Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
error = "runtime buffer descriptor has no scalar resource base";
|
||||
return false;
|
||||
}
|
||||
|
||||
var probeKey = LoadS(instruction.Sources[1].Value);
|
||||
var selector = SelectBufferCandidate(table, probeKey);
|
||||
var emitted = true;
|
||||
var caseError = string.Empty;
|
||||
for (uint index = 0; index < table.CandidateCount && emitted; index++)
|
||||
{
|
||||
var candidate = table.FirstCandidate + index;
|
||||
EmitConditional(_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(index)), () =>
|
||||
{
|
||||
if (!EmitResolvedBufferMemory(instruction, control, (int)candidate, out caseError))
|
||||
{
|
||||
emitted = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
error = caseError;
|
||||
return emitted;
|
||||
}
|
||||
|
||||
// Searches the sorted base-address mapping of a candidate table for the runtime V#'s
|
||||
// probe key; the result is the candidate-local index, candidate 0 when absent.
|
||||
private uint SelectBufferCandidate(BufferCandidateTableUse table, uint key)
|
||||
{
|
||||
var mapping = UInt(table.MappingOffset);
|
||||
var count = LoadFlattenedWord(mapping);
|
||||
var low = UInt(0);
|
||||
var high = count;
|
||||
for (uint iteration = 0; iteration < table.SearchIterations; iteration++)
|
||||
{
|
||||
var span = _module.AddInstruction(SpirvOp.ISub, _uintType, high, low);
|
||||
var middle = IAdd(low, ShiftRightLogical(span, UInt(1)));
|
||||
var probeSlot = IAdd(IAdd(mapping, UInt(1)), ShiftLeftLogical(middle, UInt(1)));
|
||||
var probeKey = LoadFlattenedWord(probeSlot);
|
||||
var moveUp = LogicalAnd(
|
||||
_module.AddInstruction(SpirvOp.ULessThan, _boolType, probeKey, key),
|
||||
_module.AddInstruction(SpirvOp.ULessThan, _boolType, low, high));
|
||||
low = _module.AddInstruction(SpirvOp.Select, _uintType, moveUp, IAdd(middle, UInt(1)), low);
|
||||
high = _module.AddInstruction(SpirvOp.Select, _uintType, moveUp, high, middle);
|
||||
}
|
||||
|
||||
var foundSlot = IAdd(IAdd(mapping, UInt(1)), ShiftLeftLogical(low, UInt(1)));
|
||||
var found = LogicalAnd(
|
||||
_module.AddInstruction(SpirvOp.ULessThan, _boolType, low, count),
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, LoadFlattenedWord(foundSlot), key));
|
||||
var mapped = LoadFlattenedWord(IAdd(foundSlot, UInt(1)));
|
||||
var inRange = LogicalAnd(
|
||||
found,
|
||||
_module.AddInstruction(SpirvOp.ULessThan, _boolType, mapped, UInt(table.CandidateCount)));
|
||||
return _module.AddInstruction(SpirvOp.Select, _uintType, inRange, mapped, UInt(0));
|
||||
}
|
||||
|
||||
private bool TryEmitDeviceDescriptorBufferMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
|
|
@ -3695,8 +4076,10 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
// Compare-exchange loop: the merge runs on the observed word until the
|
||||
// exchange succeeds.
|
||||
private void EmitAtomicWordUpdate(uint pointer, Func<uint, uint> merge)
|
||||
// exchange succeeds. Returns the observed (pre-exchange) word at the
|
||||
// point the loop exits, which is a valid use here because it is
|
||||
// defined by the OpPhi in `header`, and `header` dominates `mergeLabel`.
|
||||
private uint EmitAtomicWordUpdate(uint pointer, Func<uint, uint> merge)
|
||||
{
|
||||
var preheader = _module.AllocateId();
|
||||
var header = _module.AllocateId();
|
||||
|
|
@ -3733,6 +4116,7 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddLabel(continueLabel);
|
||||
_module.AddStatement(SpirvOp.Branch, header);
|
||||
_module.AddLabel(mergeLabel);
|
||||
return observed;
|
||||
}
|
||||
|
||||
private static bool TryGetSubdwordLoadInfo(
|
||||
|
|
@ -3833,48 +4217,12 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
// BVH nodes are not traversed yet: every ray misses the node it is tested against.
|
||||
// The node type sits in the low three bits of the node pointer. A box node (4-7)
|
||||
// returns four invalid child pointers, a triangle node (0-3) returns t = +inf over
|
||||
// a denominator of 1, so the guest's traversal loop ends with no hit.
|
||||
private void EmitRayIntersectMiss(Gen5RayIntersectControl ray)
|
||||
{
|
||||
var nodeType = BitwiseAnd(LoadV(ray.GetAddressRegister(0)), UInt(7));
|
||||
var isTriangle = _module.AddInstruction(SpirvOp.ULessThan, _boolType, nodeType, UInt(4));
|
||||
uint[] triangleMiss = [0x7F800000u, 0x3F800000u, 0u, 0u];
|
||||
for (var component = 0u; component < Gen5RayIntersectControl.ResultDwords; component++)
|
||||
{
|
||||
var value = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
isTriangle,
|
||||
UInt(triangleMiss[component]),
|
||||
UInt(0xFFFFFFFFu));
|
||||
StoreV(ray.VectorData + component, value);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryEmitImage(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5ImageControl image,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (instruction.Opcode is "ImageBvhIntersectRay" or "ImageBvh64IntersectRay")
|
||||
{
|
||||
// The host path does not expose Vulkan ray-query or an
|
||||
// acceleration-structure descriptor for GFX10's raw BVH
|
||||
// texture. Return a deterministic miss instead of rejecting
|
||||
// the complete compute shader. The instruction always writes
|
||||
// four DWORD result registers.
|
||||
for (uint component = 0; component < 4; component++)
|
||||
{
|
||||
StoreV(image.VectorData + component, UInt(0));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SpirvImageResource resource;
|
||||
uint imageObject;
|
||||
uint dstSelect;
|
||||
|
|
@ -3954,7 +4302,8 @@ public static partial class Gen5SpirvTranslator
|
|||
resource.IsStorage || resource.Multisampled
|
||||
? [queryImage]
|
||||
: [queryImage, LoadImageIntegerAddress(image, 0)]);
|
||||
var levels = !resource.IsStorage && !resource.Multisampled
|
||||
// NVIDIA's compiler crashes on an unused level query of a 3D image.
|
||||
var levels = (image.Dmask & 0x8u) != 0 && !resource.IsStorage && !resource.Multisampled
|
||||
? _module.AddInstruction(
|
||||
SpirvOp.ImageQueryLevels,
|
||||
_uintType,
|
||||
|
|
@ -4097,6 +4446,50 @@ public static partial class Gen5SpirvTranslator
|
|||
return false;
|
||||
}
|
||||
|
||||
// IMAGE_ATOMIC_FMIN/FMAX target float-format storage images and
|
||||
// have no native SPIR-V storage-image atomic without pulling in
|
||||
// SPV_EXT_shader_atomic_float_min_max. Lower them as a
|
||||
// compare-and-swap loop on the underlying bit pattern instead
|
||||
// (same technique used by other float-atomic emulations that
|
||||
// avoid that extension dependency).
|
||||
if (instruction.Opcode is "ImageAtomicFmax" or "ImageAtomicFmin")
|
||||
{
|
||||
var isMax = instruction.Opcode == "ImageAtomicFmax";
|
||||
var floatCoordinateCount = ImageCoordinateComponentCount(resource);
|
||||
var floatCoordinates = BuildIntegerCoordinates(
|
||||
image,
|
||||
0,
|
||||
floatCoordinateCount);
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var pointer = _module.AddInstruction(
|
||||
SpirvOp.ImageTexelPointer,
|
||||
_module.TypePointer(SpirvStorageClass.Image, _uintType),
|
||||
resource.Variable,
|
||||
floatCoordinates,
|
||||
UInt(0));
|
||||
var srcBits = Bitcast(_uintType, LoadV(image.VectorData));
|
||||
var old = EmitAtomicWordUpdate(pointer, observed =>
|
||||
{
|
||||
var oldFloat = Bitcast(_floatType, observed);
|
||||
var srcFloat = Bitcast(_floatType, srcBits);
|
||||
var pickSrc = _module.AddInstruction(
|
||||
isMax ? SpirvOp.FOrdLessThan : SpirvOp.FOrdGreaterThan,
|
||||
_boolType,
|
||||
oldFloat,
|
||||
srcFloat);
|
||||
var chosen = _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, pickSrc, srcFloat, oldFloat);
|
||||
return Bitcast(_uintType, chosen);
|
||||
});
|
||||
if (image.Glc)
|
||||
{
|
||||
StoreV(image.VectorData, old);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resource.ComponentKind == ImageComponentKind.Float ||
|
||||
!TryGetAtomicOp(instruction.Opcode["ImageAtomic".Length..], out var atomicOp))
|
||||
{
|
||||
|
|
@ -4903,6 +5296,12 @@ public static partial class Gen5SpirvTranslator
|
|||
? _intType
|
||||
: _module.TypeVector(_intType, componentCount);
|
||||
|
||||
private uint IntegerTypeForComponents(uint componentCount) =>
|
||||
componentCount == 1 ? _intType : _module.TypeVector(_intType, componentCount);
|
||||
|
||||
private uint FloatTypeForComponents(uint componentCount) =>
|
||||
componentCount == 1 ? _floatType : _module.TypeVector(_floatType, componentCount);
|
||||
|
||||
private uint BuildFloatCoordinates(
|
||||
Gen5ImageControl image,
|
||||
int start,
|
||||
|
|
@ -6143,9 +6542,8 @@ public static partial class Gen5SpirvTranslator
|
|||
value = 0;
|
||||
var packedSource = exportInstruction.Sources[component >> 1];
|
||||
var tracePackedExport =
|
||||
Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_TRACE_PACKED_EXPORT") == "1" &&
|
||||
_request.Program.Address == 0x0000000500781200ul;
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PACKED_EXPORT") == "1" &&
|
||||
TraceShaderAddressMatches("SHARPEMU_TRACE_PACKED_EXPORT_ADDRESS");
|
||||
if (tracePackedExport)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -6261,6 +6659,28 @@ public static partial class Gen5SpirvTranslator
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool TraceShaderAddressMatches(string environmentVariable)
|
||||
{
|
||||
var filter = Environment.GetEnvironmentVariable(environmentVariable);
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var span = filter.AsSpan();
|
||||
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
span = span[2..];
|
||||
}
|
||||
|
||||
return ulong.TryParse(
|
||||
span,
|
||||
System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var address) &&
|
||||
_request.Program.Address == address;
|
||||
}
|
||||
|
||||
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
|
||||
kind switch
|
||||
{
|
||||
|
|
@ -6536,6 +6956,15 @@ public static partial class Gen5SpirvTranslator
|
|||
left,
|
||||
BitwiseAnd64(right, _module.Constant64(_ulongType, 63)));
|
||||
|
||||
private uint ShiftRightArithmetic64(uint left, uint right) =>
|
||||
Bitcast(
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightArithmetic,
|
||||
_longType,
|
||||
Bitcast(_longType, left),
|
||||
BitwiseAnd64(right, _module.Constant64(_ulongType, 63))));
|
||||
|
||||
private uint BitwiseAnd(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, left, right);
|
||||
|
||||
|
|
@ -6845,7 +7274,9 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
private bool UsesLds() =>
|
||||
_request.Program.Instructions.Any(instruction =>
|
||||
instruction.Control is Gen5DataShareControl);
|
||||
instruction.Control is Gen5DataShareControl) ||
|
||||
_request.Memory.Entries.Any(static memory =>
|
||||
memory.AddressSpace is FlatAddressSpace.Shared or FlatAddressSpace.SharedOrPrivate);
|
||||
|
||||
private bool UsesSubgroupShuffle() =>
|
||||
_request.Program.Instructions.Any(instruction =>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ public enum SpirvCapability : uint
|
|||
Float16 = 9,
|
||||
Float64 = 10,
|
||||
Int64 = 11,
|
||||
Int64Atomics = 12,
|
||||
Int16 = 22,
|
||||
ImageGatherExtended = 25,
|
||||
StorageImageExtendedFormats = 49,
|
||||
|
|
@ -207,6 +208,8 @@ public enum SpirvCapability : uint
|
|||
GroupNonUniformVote = 62,
|
||||
GroupNonUniformBallot = 64,
|
||||
GroupNonUniformShuffle = 65,
|
||||
ShaderLayer = 5253,
|
||||
ShaderViewportIndex = 5254,
|
||||
ShaderViewportIndexLayerExt = 5254,
|
||||
RuntimeDescriptorArray = 5302,
|
||||
PhysicalStorageBufferAddresses = 5347,
|
||||
|
|
|
|||
|
|
@ -201,14 +201,55 @@ public static class TilerShaders
|
|||
_ => [X(6, 0x0c0), X(7, 0x200), X(8, 0x800), X(9, 0x2000), X(10, 0x8000), Y(4, 0x030), Y(6, 0x100), Y(7, 0x400), Y(8, 0x1000), Y(9, 0x4000)],
|
||||
};
|
||||
|
||||
private static Term[] RenderTargetTerms(uint bytes) => bytes switch
|
||||
private readonly record struct AddressBit(Axis Source, byte Bit);
|
||||
|
||||
private static AddressBit BX(byte bit) => new(Axis.X, bit);
|
||||
|
||||
private static AddressBit BY(byte bit) => new(Axis.Y, bit);
|
||||
|
||||
private static AddressBit[] BXY(byte x, byte y) => [BX(x), BY(y)];
|
||||
|
||||
// PS5 Oberon RB+ R_X addressing. The destination bit index is the array
|
||||
// index. Keep this in lockstep with GnmTiling.RbPlus64KRenderX and
|
||||
// TileGeometry.RenderTargetOffset.
|
||||
private static readonly AddressBit[][][] RenderTargetAddressBits =
|
||||
[
|
||||
[
|
||||
[BX(0)], [BX(1)], [BX(2)], [BX(3)], [BY(0)], [BY(1)], [BY(2)], [BY(3)],
|
||||
BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BX(6)], [BY(6)], BXY(7, 8), BXY(8, 7),
|
||||
],
|
||||
[
|
||||
[], [BX(0)], [BX(1)], [BX(2)], [BY(0)], [BY(1)], [BY(2)], [BX(3)],
|
||||
BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BY(3)], [BX(6)], BXY(7, 7), BXY(8, 6),
|
||||
],
|
||||
[
|
||||
[], [], [BX(0)], [BX(1)], [BY(0)], [BY(1)], [BX(2)], [BY(2)],
|
||||
BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BX(3)], [BY(3)], BXY(6, 7), BXY(7, 6),
|
||||
],
|
||||
[
|
||||
[], [], [], [BX(0)], [BY(0)], [BX(1)], [BX(2)], [BY(1)],
|
||||
BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BY(2)], [BX(3)], BXY(7, 3), BXY(6, 6),
|
||||
],
|
||||
[
|
||||
[], [], [], [], [BX(0)], [BY(0)], [BX(1)], [BY(1)],
|
||||
BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BX(2)], [BY(2)], BXY(6, 3), BXY(3, 6),
|
||||
],
|
||||
];
|
||||
|
||||
private static uint RenderTargetOffset(ShaderModuleContext shaderModule, uint x, uint y, uint bytes)
|
||||
{
|
||||
1 => [Y(2, 0x008), Y(4, 0x010), Y(3, 0x0a0), Y(5, 0xf00), Y(6, 0x1000), Y(7, 0x4000), X(0, 7), X(3, 0x040), X(5, 0x300), X(4, 0x400), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
|
||||
2 => [Y(4, 0x070), Y(5, 0xf00), Y(8, 0x5000), X(1, 0x00e), X(4, 0x480), X(5, 0x300), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
|
||||
4 => [Y(4, 0x070), Y(5, 0xf00), Y(9, 0x1000), Y(8, 0x4000), X(2, 0x00c), X(5, 0x380), X(4, 0x400), X(6, 0x800), X(9, 0xa000)],
|
||||
8 => [Y(4, 0x010), Y(6, 0x080), Y(5, 0xf00), Y(10, 0x5000), X(3, 0x008), X(4, 0x460), X(5, 0x300), X(6, 0x800), X(10, 0x2000), X(9, 0x8000)],
|
||||
_ => [X(4, 0x410), X(5, 0x340), X(6, 0x800), X(11, 0xa000), Y(5, 0xf20), Y(6, 0x080), Y(10, 0x1000), Y(11, 0x4000)],
|
||||
};
|
||||
uint offset = shaderModule.GetUnsignedConstant(0);
|
||||
var table = RenderTargetAddressBits[Log2(bytes)];
|
||||
for (uint destination = 0; destination < table.Length; destination++)
|
||||
{
|
||||
foreach (var bit in table[destination])
|
||||
{
|
||||
offset = shaderModule.Xor(offset, Bit(shaderModule, bit.Source == Axis.X ? x : y, bit.Bit, destination));
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private static Term[] DepthTerms(uint bytes) => bytes switch
|
||||
{
|
||||
|
|
@ -306,7 +347,7 @@ public static class TilerShaders
|
|||
return shaderModule.Xor(Standard64KB3DOffset(shaderModule, x, y, z, bytes), delta);
|
||||
}
|
||||
case TilerBlockShape.RenderTarget64KB:
|
||||
return shaderModule.Xor(XorTerms(shaderModule, x, y, z, RenderTargetTerms(bytes)), ZSpread(shaderModule, z));
|
||||
return shaderModule.Xor(RenderTargetOffset(shaderModule, x, y, bytes), ZSpread(shaderModule, z));
|
||||
case TilerBlockShape.Depth64KB:
|
||||
return shaderModule.Xor(ZSpread(shaderModule, z), XorTerms(shaderModule, x, y, z, DepthTerms(bytes)));
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -9,8 +9,52 @@ namespace SharpEmu.ShaderCompiler;
|
|||
/// </summary>
|
||||
public static class Gen5InlineConstants
|
||||
{
|
||||
public const uint SharedBase = 235;
|
||||
public const uint SharedLimit = 236;
|
||||
public const uint PrivateBase = 237;
|
||||
public const uint PrivateLimit = 238;
|
||||
|
||||
// The PS5 FLAT apertures, as title shaders hard-code them: a high dword of
|
||||
// 0x7xxxxxxx selects LDS and 0x8xxxxxxx scratch. Guest device addresses fit in
|
||||
// 40 bits and never reach them.
|
||||
public const uint SharedApertureHigh = 0x7000_0000;
|
||||
public const uint PrivateApertureHigh = 0x8000_0000;
|
||||
|
||||
// Each aperture is the window of high dwords sharing its top nibble.
|
||||
public const int ApertureShift = 28;
|
||||
|
||||
public static bool IsSharedApertureHigh(uint high) => high >> ApertureShift == SharedApertureHigh >> ApertureShift;
|
||||
|
||||
public static bool IsPrivateApertureHigh(uint high) => high >> ApertureShift == PrivateApertureHigh >> ApertureShift;
|
||||
|
||||
public static bool IsAperture(uint encoded) => encoded is >= SharedBase and <= PrivateLimit;
|
||||
|
||||
public static bool IsSharedAperture(uint encoded) => encoded is SharedBase or SharedLimit;
|
||||
|
||||
// The 64-bit aperture value: a base has a zero low dword, a limit spans the
|
||||
// whole 32-bit offset range. A 32-bit read returns the high dword.
|
||||
public static ulong DecodeAperture64(uint encoded)
|
||||
{
|
||||
var high = IsSharedAperture(encoded) ? SharedApertureHigh : PrivateApertureHigh;
|
||||
var low = encoded is SharedLimit or PrivateLimit ? uint.MaxValue : 0u;
|
||||
return ((ulong)high << 32) | low;
|
||||
}
|
||||
|
||||
public static bool TryDecode(uint encoded, out uint value)
|
||||
{
|
||||
if (IsAperture(encoded))
|
||||
{
|
||||
value = (uint)(DecodeAperture64(encoded) >> 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
// POPS_EXITING_WAVE_ID: no primitive-ordered pixel shading is modeled.
|
||||
if (encoded == 239)
|
||||
{
|
||||
value = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (encoded == 125)
|
||||
{
|
||||
value = 0;
|
||||
|
|
|
|||
|
|
@ -160,7 +160,11 @@ public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
|||
return new(Gen5OperandKind.LiteralConstant, literal.Value);
|
||||
}
|
||||
|
||||
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
|
||||
// 108..123 are the temporary SGPRs (TTMP0..TTMP15). They use the
|
||||
// same scalar register file as ordinary SGPRs; treating them as
|
||||
// encoded constants loses values in shader continuations that spill
|
||||
// through a temporary register.
|
||||
if (encoded <= 105 || encoded is 106 or 107 or (>= 108 and <= 123) or 124 or 126 or 127)
|
||||
{
|
||||
return Scalar(encoded);
|
||||
}
|
||||
|
|
@ -223,7 +227,9 @@ public sealed record Gen5GlobalMemoryControl(
|
|||
int OffsetBytes,
|
||||
bool Glc,
|
||||
bool Slc,
|
||||
bool UsesFlatAddress = false) : Gen5InstructionControl;
|
||||
bool UsesFlatAddress = false,
|
||||
uint? DynamicOffsetRegister = null,
|
||||
bool SourceIsScalar = false) : Gen5InstructionControl;
|
||||
|
||||
// A typed access carries the unified format from the instruction; a formatted
|
||||
// untyped access reads the descriptor format when it executes.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ public static partial class Gen5ShaderTranslator
|
|||
{
|
||||
private static int _dppVectorsValidated;
|
||||
|
||||
private const int MaxInstructions = 16384;
|
||||
// Some shipped Gen5 shaders exceed 64 KiB of encoded instructions before
|
||||
// reaching S_ENDPGM. Keep the malformed-shader guard, but size it to the
|
||||
// largest declared shader object accepted by the decoder (1 MiB).
|
||||
private const int MaxInstructions = (int)(MaximumDeclaredShaderSizeBytes / sizeof(uint));
|
||||
private const ulong ShaderSizeOffset = 0x44;
|
||||
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
|
||||
private static readonly ConditionalWeakTable<object, FusedProgramRegistry> _fusedProgramsByMemory = new();
|
||||
|
|
@ -120,7 +123,13 @@ public static partial class Gen5ShaderTranslator
|
|||
ctx,
|
||||
address,
|
||||
maximumBytes: null,
|
||||
stopAtSetProgramCounter: false,
|
||||
// An unregistered S_SETPC transfers control to a separately
|
||||
// allocated code object. The bytes that follow it in this
|
||||
// object are padding or unrelated data, so decoding through them
|
||||
// produces bogus ALU instructions and can reject an otherwise
|
||||
// valid shader. Registered fused programs take the dedicated
|
||||
// path above and splice their continuation explicitly.
|
||||
stopAtSetProgramCounter: true,
|
||||
out program,
|
||||
out _,
|
||||
out error);
|
||||
|
|
@ -266,6 +275,8 @@ public static partial class Gen5ShaderTranslator
|
|||
|
||||
var instructions = new List<Gen5ShaderInstruction>();
|
||||
var instructionCount = 0;
|
||||
var lastPc = 0u;
|
||||
var lastWord = 0u;
|
||||
uint furthestForwardBranchTarget = 0;
|
||||
for (uint pc = 0;
|
||||
instructionCount < MaxInstructions &&
|
||||
|
|
@ -283,6 +294,9 @@ public static partial class Gen5ShaderTranslator
|
|||
return false;
|
||||
}
|
||||
|
||||
lastPc = pc;
|
||||
lastWord = word;
|
||||
|
||||
if (!TryDecodeInstruction(
|
||||
ctx,
|
||||
address,
|
||||
|
|
@ -329,7 +343,12 @@ public static partial class Gen5ShaderTranslator
|
|||
if (Gen5IrBranchResolver.Instance.TryGetBranchTarget(
|
||||
instruction,
|
||||
out var branchTargetPc) &&
|
||||
branchTargetPc > instruction.Pc)
|
||||
branchTargetPc > instruction.Pc &&
|
||||
// S_CBRANCH_CDBG* only fire under an attached GPU debugger, and
|
||||
// the translators emit them as never taken. Their targets are
|
||||
// debugger stubs placed after S_ENDPGM that read devkit-only
|
||||
// addresses, so they must not extend the decoded program.
|
||||
!instruction.Opcode.StartsWith("SCbranchCdbg", StringComparison.Ordinal))
|
||||
{
|
||||
furthestForwardBranchTarget = Math.Max(
|
||||
furthestForwardBranchTarget,
|
||||
|
|
@ -337,7 +356,12 @@ public static partial class Gen5ShaderTranslator
|
|||
}
|
||||
|
||||
pc += sizeDwords * sizeof(uint);
|
||||
if (string.Equals(name, "SEndpgm", StringComparison.Ordinal) &&
|
||||
// An unconditional backward S_BRANCH with no forward target
|
||||
// pending is the last reachable instruction too: compilers place
|
||||
// out-of-line loop blocks after S_ENDPGM, and the code object's
|
||||
// metadata trailer follows them.
|
||||
if ((string.Equals(name, "SEndpgm", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "SBranch", StringComparison.Ordinal)) &&
|
||||
pc > furthestForwardBranchTarget)
|
||||
{
|
||||
program = new Gen5ShaderProgram(address, instructions);
|
||||
|
|
@ -355,8 +379,8 @@ public static partial class Gen5ShaderTranslator
|
|||
}
|
||||
|
||||
error = maximumBytes.HasValue
|
||||
? $"unterminated limit=0x{maximumBytes.Value:X}"
|
||||
: "unterminated";
|
||||
? $"unterminated limit=0x{maximumBytes.Value:X} pc=0x{lastPc:X} word=0x{lastWord:X8} count={instructionCount}"
|
||||
: $"unterminated pc=0x{lastPc:X} word=0x{lastWord:X8} count={instructionCount}";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -589,8 +613,12 @@ public static partial class Gen5ShaderTranslator
|
|||
0x10 => "SBcnt1I32B64",
|
||||
0x13 => "SFF1I32B32",
|
||||
0x14 => "SFF1I32B64",
|
||||
0x15 => "SFlbitI32B32",
|
||||
0x16 => "SFlbitI32B64",
|
||||
0x1B => "SBitset0B32",
|
||||
0x1C => "SBitset0B64",
|
||||
0x1D => "SBitset1B32",
|
||||
0x1E => "SBitset1B64",
|
||||
0x1F => "SGetpcB64",
|
||||
0x20 => "SSetpcB64",
|
||||
0x21 => "SSwappcB64",
|
||||
|
|
@ -605,6 +633,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x34 => "SAbsI32",
|
||||
0x37 => "SAndn1SaveexecB64",
|
||||
0x38 => "SOrn1SaveexecB64",
|
||||
0x3B => "SBitreplicateB64B32",
|
||||
0x3C => "SAndSaveexecB32",
|
||||
0x3D => "SOrSaveexecB32",
|
||||
0x3E => "SXorSaveexecB32",
|
||||
|
|
@ -681,6 +710,11 @@ public static partial class Gen5ShaderTranslator
|
|||
0x34 => "SPackHhB32B16",
|
||||
0x35 => "SMulHiU32",
|
||||
0x36 => "SMulHiI32",
|
||||
// RDNA scalar floating-point forms use the extended SOP2 opcode
|
||||
// range. They keep the raw SGPR bit representation and do not
|
||||
// alter SCC.
|
||||
0x40 => "SAddF32",
|
||||
0x41 => "SSubF32",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
|
@ -798,6 +832,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x00 => "VNop",
|
||||
0x01 => "VMovB32",
|
||||
0x02 => "VReadfirstlaneB32",
|
||||
0x04 => "VCvtF64I32",
|
||||
0x05 => "VCvtF32I32",
|
||||
0x06 => "VCvtF32U32",
|
||||
0x07 => "VCvtU32F32",
|
||||
|
|
@ -807,10 +842,12 @@ public static partial class Gen5ShaderTranslator
|
|||
0x0C => "VCvtRpiI32F32",
|
||||
0x0D => "VCvtFlrI32F32",
|
||||
0x0E => "VCvtOffF32I4",
|
||||
0x0F => "VCvtF32F64",
|
||||
0x11 => "VCvtF32Ubyte0",
|
||||
0x12 => "VCvtF32Ubyte1",
|
||||
0x13 => "VCvtF32Ubyte2",
|
||||
0x14 => "VCvtF32Ubyte3",
|
||||
0x16 => "VCvtF64U32",
|
||||
0x20 => "VFractF32",
|
||||
0x21 => "VTruncF32",
|
||||
0x22 => "VCeilF32",
|
||||
|
|
@ -821,7 +858,10 @@ public static partial class Gen5ShaderTranslator
|
|||
0x2A => "VRcpF32",
|
||||
0x2B => "VRcpIflagF32",
|
||||
0x2E => "VRsqF32",
|
||||
0x2F => "VRcpF64",
|
||||
0x30 => "VRsqF64",
|
||||
0x33 => "VSqrtF32",
|
||||
0x34 => "VSqrtF64",
|
||||
0x35 => "VSinF32",
|
||||
0x36 => "VCosF32",
|
||||
0x37 => "VNotB32",
|
||||
|
|
@ -833,7 +873,18 @@ public static partial class Gen5ShaderTranslator
|
|||
0x44 => "VMovrelsdB32",
|
||||
0x48 => "VMovrelsd2B32",
|
||||
0x52 => "VCvtU16F16",
|
||||
0x54 => "VRcpF16",
|
||||
0x55 => "VSqrtF16",
|
||||
0x56 => "VRsqF16",
|
||||
0x57 => "VLogF16",
|
||||
0x58 => "VExpF16",
|
||||
0x5B => "VFloorF16",
|
||||
0x5C => "VCeilF16",
|
||||
0x5D => "VTruncF16",
|
||||
0x5E => "VRndneF16",
|
||||
0x5F => "VFractF16",
|
||||
0x60 => "VSinF16",
|
||||
0x61 => "VCosF16",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
|
@ -854,11 +905,15 @@ public static partial class Gen5ShaderTranslator
|
|||
}
|
||||
|
||||
var src0 = word & 0x1FF;
|
||||
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D ||
|
||||
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D or 0x37 or 0x38 ||
|
||||
src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||
error = string.Empty;
|
||||
name = opcode switch
|
||||
{
|
||||
// Some Gen5 shader streams use the VOP2 opcode-zero form as a
|
||||
// padding/no-op instruction. Keep it as VNop so the translator
|
||||
// can consume the instruction without manufacturing a result.
|
||||
0x00 => "VNop",
|
||||
0x01 => "VCndmaskB32",
|
||||
0x02 => "VDot2cF32F16",
|
||||
0x03 => "VAddF32",
|
||||
|
|
@ -906,8 +961,13 @@ public static partial class Gen5ShaderTranslator
|
|||
0x33 => "VSubF16",
|
||||
0x34 => "VSubrevF16",
|
||||
0x35 => "VMulF16",
|
||||
// RDNA encodes FMAMK/FMAAK as VOP2 instructions with a literal
|
||||
// dword immediately following the opcode word.
|
||||
0x37 => "VFmaMkF16",
|
||||
0x38 => "VFmaAkF16",
|
||||
0x39 => "VMaxF16",
|
||||
0x3A => "VMinF16",
|
||||
0x3C => "VPkFmacF16",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
|
@ -979,6 +1039,24 @@ public static partial class Gen5ShaderTranslator
|
|||
0x8D => "VCmpNeI16",
|
||||
0x8E => "VCmpGeI16",
|
||||
0x98 => "VCmpxClassF32",
|
||||
0xA9 => "VCmpLtU16",
|
||||
0xAA => "VCmpEqU16",
|
||||
0xAB => "VCmpLeU16",
|
||||
0xAC => "VCmpGtU16",
|
||||
0xAD => "VCmpNeU16",
|
||||
0xAE => "VCmpGeU16",
|
||||
0x99 => "VCmpxLtI16",
|
||||
0x9A => "VCmpxEqI16",
|
||||
0x9B => "VCmpxLeI16",
|
||||
0x9C => "VCmpxGtI16",
|
||||
0x9D => "VCmpxNeI16",
|
||||
0x9E => "VCmpxGeI16",
|
||||
0xB9 => "VCmpxLtU16",
|
||||
0xBA => "VCmpxEqU16",
|
||||
0xBB => "VCmpxLeU16",
|
||||
0xBC => "VCmpxGtU16",
|
||||
0xBD => "VCmpxNeU16",
|
||||
0xBE => "VCmpxGeU16",
|
||||
0x90 => "VCmpxFI32",
|
||||
0x91 => "VCmpxLtI32",
|
||||
0x92 => "VCmpxEqI32",
|
||||
|
|
@ -987,12 +1065,6 @@ public static partial class Gen5ShaderTranslator
|
|||
0x95 => "VCmpxNeI32",
|
||||
0x96 => "VCmpxGeI32",
|
||||
0x97 => "VCmpxTI32",
|
||||
0x99 => "VCmpxLtI16",
|
||||
0x9A => "VCmpxEqI16",
|
||||
0x9B => "VCmpxLeI16",
|
||||
0x9C => "VCmpxGtI16",
|
||||
0x9D => "VCmpxNeI16",
|
||||
0x9E => "VCmpxGeI16",
|
||||
0xA0 => "VCmpFI64",
|
||||
0xA1 => "VCmpLtI64",
|
||||
0xA2 => "VCmpEqI64",
|
||||
|
|
@ -1122,6 +1194,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x12B => "VFmacF32",
|
||||
0x12F => "VCvtPkrtzF16F32",
|
||||
0x141 => "VMadF32",
|
||||
0x142 => "VMadI32I24",
|
||||
0x143 => "VMadU32U24",
|
||||
0x144 => "VCubeidF32",
|
||||
0x145 => "VCubescF32",
|
||||
|
|
@ -1131,6 +1204,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x14E => "VAlignbitB32",
|
||||
0x14F => "VAlignbyteB32",
|
||||
0x14B => "VFmaF32",
|
||||
0x14C => "VFmaF64",
|
||||
0x151 => "VMin3F32",
|
||||
0x152 => "VMin3I32",
|
||||
0x153 => "VMin3U32",
|
||||
|
|
@ -1147,12 +1221,37 @@ public static partial class Gen5ShaderTranslator
|
|||
0x15E => "VCvtPkU8F32",
|
||||
0x148 => "VBfeU32",
|
||||
0x149 => "VBfeI32",
|
||||
0x165 => "VMulF64",
|
||||
0x169 => "VMulLoU32",
|
||||
0x16A => "VMulHiU32",
|
||||
0x16B => "VMulLoI32",
|
||||
0x16C => "VMulHiI32",
|
||||
// VOP3-encoded VOP1 (opcode + 0x180) for the double-precision
|
||||
// conversions and transcendentals.
|
||||
0x184 => "VCvtF64I32",
|
||||
0x18F => "VCvtF32F64",
|
||||
0x196 => "VCvtF64U32",
|
||||
0x1AF => "VRcpF64",
|
||||
0x1B0 => "VRsqF64",
|
||||
0x1B4 => "VSqrtF64",
|
||||
// VOP3-encoded half-precision transcendentals and rounding.
|
||||
0x1D4 => "VRcpF16",
|
||||
0x1D5 => "VSqrtF16",
|
||||
0x1D7 => "VLogF16",
|
||||
0x1D8 => "VExpF16",
|
||||
0x1DB => "VFloorF16",
|
||||
0x1DC => "VCeilF16",
|
||||
0x1DD => "VTruncF16",
|
||||
0x1DE => "VRndneF16",
|
||||
0x1DF => "VFractF16",
|
||||
0x1E0 => "VSinF16",
|
||||
0x1E1 => "VCosF16",
|
||||
0x1D6 => "VRsqF16",
|
||||
0x303 => "VAddNcU16",
|
||||
0x34B => "VFmaF16",
|
||||
0x351 => "VMin3F16",
|
||||
0x354 => "VMax3F16",
|
||||
0x357 => "VMed3F16",
|
||||
0x360 => "VReadlaneB32",
|
||||
0x361 => "VWritelaneB32",
|
||||
0x362 => "VLdexpF32",
|
||||
|
|
@ -1164,6 +1263,9 @@ public static partial class Gen5ShaderTranslator
|
|||
0x369 => "VCvtPknormU16F32",
|
||||
0x36A => "VCvtPkU16U32",
|
||||
0x36B => "VCvtPkI16I32",
|
||||
// GFX10 reverse 64-bit shifts.
|
||||
0x2FF => "VLshlrevB64",
|
||||
0x300 => "VLshrrevB64",
|
||||
0x373 => "VMadU32U16",
|
||||
0x346 => "VLshlAddU32",
|
||||
0x345 => "VXadU32",
|
||||
|
|
@ -1173,11 +1275,38 @@ public static partial class Gen5ShaderTranslator
|
|||
0x178 => "VXor3B32",
|
||||
// VOP1 opcode 0x52 is available through VOP3 as opcode 0x1D2.
|
||||
0x1D2 => "VCvtU16F16",
|
||||
0x300 => "VLshrrevB64",
|
||||
0x371 => "VAndOrB32",
|
||||
0x372 => "VOr3U32",
|
||||
0x377 => "VPermlane16B32",
|
||||
0x378 => "VPermlanex16B32",
|
||||
// GFX10 16-bit integer ALU, VOP3-only.
|
||||
0x304 => "VSubNcU16",
|
||||
0x307 => "VLshrrevB16",
|
||||
0x308 => "VAshrrevI16",
|
||||
0x309 => "VMaxU16",
|
||||
0x30A => "VMaxI16",
|
||||
0x30B => "VMinU16",
|
||||
0x30C => "VMinI16",
|
||||
0x30D => "VAddNcI16",
|
||||
0x30E => "VSubNcI16",
|
||||
0x314 => "VLshlrevB16",
|
||||
// VOP3-encoded 64-bit VOPC (opcode < 0x100): V_CMP_*_U64 / V_CMPX_*_U64.
|
||||
0x0E0 => "VCmpFU64",
|
||||
0x0E1 => "VCmpLtU64",
|
||||
0x0E2 => "VCmpEqU64",
|
||||
0x0E3 => "VCmpLeU64",
|
||||
0x0E4 => "VCmpGtU64",
|
||||
0x0E5 => "VCmpNeU64",
|
||||
0x0E6 => "VCmpGeU64",
|
||||
0x0E7 => "VCmpTU64",
|
||||
0x0F0 => "VCmpxFU64",
|
||||
0x0F1 => "VCmpxLtU64",
|
||||
0x0F2 => "VCmpxEqU64",
|
||||
0x0F3 => "VCmpxLeU64",
|
||||
0x0F4 => "VCmpxGtU64",
|
||||
0x0F5 => "VCmpxNeU64",
|
||||
0x0F6 => "VCmpxGeU64",
|
||||
0x0F7 => "VCmpxTU64",
|
||||
_ => $"Vop3Raw{opcode:X3}",
|
||||
};
|
||||
|
||||
|
|
@ -1222,6 +1351,8 @@ public static partial class Gen5ShaderTranslator
|
|||
// loudly at emission rather than being silently mis-emitted.
|
||||
name = opcode switch
|
||||
{
|
||||
0x02 => "VPkAddI16",
|
||||
0x03 => "VPkSubI16",
|
||||
0x0E => "VPkFmaF16",
|
||||
0x0F => "VPkAddF16",
|
||||
0x10 => "VPkMulF16",
|
||||
|
|
@ -1284,6 +1415,13 @@ public static partial class Gen5ShaderTranslator
|
|||
0x39 => "DsReadI8",
|
||||
0x3D => "DsConsume",
|
||||
0x3E => "DsAppend",
|
||||
// gfx10 groups the 64-bit LDS atomics at 0x40..0x4C, directly ahead
|
||||
// of DS_WRITE_B64 (0x4D). Only the no-return forms are named here:
|
||||
// the RTN variants cannot be split into two 32-bit atomics without
|
||||
// losing the atomicity of the returned pair, so they stay unknown
|
||||
// and keep failing loudly.
|
||||
0x40 => "DsAddU64",
|
||||
0x4A => "DsOrB64",
|
||||
0x4D => "DsWriteB64",
|
||||
0x4E => "DsWrite2B64",
|
||||
0x4F => "DsWrite2St64B64",
|
||||
|
|
@ -1409,8 +1547,21 @@ public static partial class Gen5ShaderTranslator
|
|||
0x3B => "BufferAtomicXor",
|
||||
0x3C => "BufferAtomicInc",
|
||||
0x3D => "BufferAtomicDec",
|
||||
0x3E => "BufferWbinvl1",
|
||||
0x3F => "BufferAtomicFmin",
|
||||
0x40 => "BufferAtomicFmax",
|
||||
// GFX9 buffer atomic encodings used by the guest shaders.
|
||||
0x41 => "BufferAtomicCmpswap",
|
||||
0x42 => "BufferAtomicAdd",
|
||||
0x43 => "BufferAtomicSub",
|
||||
0x45 => "BufferAtomicSmin",
|
||||
0x46 => "BufferAtomicUmin",
|
||||
0x47 => "BufferAtomicSmax",
|
||||
0x48 => "BufferAtomicUmax",
|
||||
0x49 => "BufferAtomicAnd",
|
||||
0x4A => "BufferAtomicOr",
|
||||
0x4B => "BufferAtomicXor",
|
||||
0x4C => "BufferAtomicInc",
|
||||
0x50 => "BufferAtomicSwapX2",
|
||||
0x5A => "BufferAtomicOrX2",
|
||||
_ => $"MubufRaw{opcode:X2}",
|
||||
|
|
@ -1494,10 +1645,13 @@ public static partial class Gen5ShaderTranslator
|
|||
0x0A => "SBufferLoadDwordx4",
|
||||
0x0B => "SBufferLoadDwordx8",
|
||||
0x0C => "SBufferLoadDwordx16",
|
||||
0x15 => "ScratchStoreDword",
|
||||
0x16 => "ScratchStoreDwordx2",
|
||||
0x17 => "ScratchStoreDwordx4",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return FinishDecode(name, $"unknown-smrd op=0x{opcode:X2}", out error);
|
||||
return FinishDecode(name, $"unknown-smrd op=0x{opcode:X2} word=0x{word:X8}", out error);
|
||||
}
|
||||
|
||||
private static bool DecodeSmem(uint word, out string name, out uint sizeDwords, out string error)
|
||||
|
|
@ -1558,6 +1712,13 @@ public static partial class Gen5ShaderTranslator
|
|||
0x1A => "ImageAtomicXor",
|
||||
0x1B => "ImageAtomicInc",
|
||||
0x1C => "ImageAtomicDec",
|
||||
// The float image atomics close the gfx10 atomic block. No
|
||||
// backend emits them yet, so they still fail - but naming them
|
||||
// lets the decoder finish the program, which is what makes the
|
||||
// shader's bindings and global-memory usage visible in the trace.
|
||||
0x1D => "ImageAtomicFcmpswap",
|
||||
0x1E => "ImageAtomicFmin",
|
||||
0x1F => "ImageAtomicFmax",
|
||||
0x20 => "ImageSample",
|
||||
0x21 => "ImageSampleCl",
|
||||
0x22 => "ImageSampleD",
|
||||
|
|
@ -1593,6 +1754,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x40 => "ImageGather4",
|
||||
0x47 => "ImageGather4Lz",
|
||||
0x48 => "ImageGather4C",
|
||||
0x4E => "ImageGather4CBCl",
|
||||
0x4F => "ImageGather4CLz",
|
||||
0x57 => "ImageGather4LzO",
|
||||
0x58 => "ImageGather4CO",
|
||||
|
|
@ -1775,7 +1937,8 @@ public static partial class Gen5ShaderTranslator
|
|||
"DsAddRtnU32" or "DsSubRtnU32" or "DsIncRtnU32" or "DsDecRtnU32" or
|
||||
"DsMinRtnI32" or "DsMaxRtnI32" or "DsMinRtnU32" or "DsMaxRtnU32" or
|
||||
"DsAndRtnB32" or "DsOrRtnB32" or "DsXorRtnB32" or
|
||||
"DsWrxchgRtnB32" or "DsCmpstRtnB32" => true,
|
||||
"DsWrxchgRtnB32" or "DsCmpstRtnB32" or
|
||||
"DsAddU64" or "DsOrB64" => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
|
@ -1932,6 +2095,50 @@ public static partial class Gen5ShaderTranslator
|
|||
var scalarDestination = (word >> 15) & 0x7F;
|
||||
var immediate = ((word >> 8) & 1) != 0;
|
||||
var offset = word & 0xFF;
|
||||
if (opcode.StartsWith("ScratchStore", StringComparison.Ordinal))
|
||||
{
|
||||
var storeCount = ScalarMemoryDwordCount(opcode);
|
||||
uint? storeDynamicOffsetRegister = null;
|
||||
var storeImmediateOffsetBytes = 0;
|
||||
if (immediate)
|
||||
{
|
||||
storeImmediateOffsetBytes = checked((int)(offset * sizeof(uint)));
|
||||
}
|
||||
else if (offset == 0xFF && literal.HasValue)
|
||||
{
|
||||
storeImmediateOffsetBytes = unchecked((int)literal.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
storeDynamicOffsetRegister = offset;
|
||||
}
|
||||
|
||||
sources = [
|
||||
Gen5Operand.Scalar(scalarBase),
|
||||
.. Enumerable.Range(0, checked((int)storeCount)).Select(index =>
|
||||
Gen5Operand.Scalar(scalarDestination + (uint)index)),
|
||||
];
|
||||
if (storeDynamicOffsetRegister.HasValue)
|
||||
{
|
||||
sources = [.. sources, Gen5Operand.Scalar(storeDynamicOffsetRegister.Value)];
|
||||
}
|
||||
|
||||
destinations = [];
|
||||
control = new Gen5GlobalMemoryControl(
|
||||
storeCount,
|
||||
0,
|
||||
scalarDestination,
|
||||
0,
|
||||
scalarBase,
|
||||
storeImmediateOffsetBytes,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
storeDynamicOffsetRegister,
|
||||
true);
|
||||
break;
|
||||
}
|
||||
|
||||
var count = ScalarLoadDwordCount(opcode);
|
||||
uint? dynamicOffsetRegister = null;
|
||||
var immediateOffsetBytes = 0;
|
||||
|
|
@ -2075,7 +2282,7 @@ public static partial class Gen5ShaderTranslator
|
|||
Gen5Operand.Source(word & 0x1FF, literal),
|
||||
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||
];
|
||||
if ((opcode is "VMadMkF32" or "VFmaMkF32") && literal.HasValue)
|
||||
if ((opcode is "VMadMkF32" or "VFmaMkF32" or "VFmaMkF16") && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
|
|
@ -2084,7 +2291,7 @@ public static partial class Gen5ShaderTranslator
|
|||
sources[1],
|
||||
];
|
||||
}
|
||||
else if ((opcode is "VMadAkF32" or "VFmaAkF32") && literal.HasValue)
|
||||
else if ((opcode is "VMadAkF32" or "VFmaAkF32" or "VFmaAkF16") && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
|
|
@ -2277,6 +2484,17 @@ public static partial class Gen5ShaderTranslator
|
|||
Gen5Operand.Vector(vectorData0),
|
||||
Gen5Operand.Vector(vectorData1),
|
||||
],
|
||||
"DsMskorB32" => [
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorData0),
|
||||
Gen5Operand.Vector(vectorData1),
|
||||
],
|
||||
// 64-bit LDS atomics take ADDR plus a DATA0 register pair.
|
||||
"DsAddU64" or "DsOrB64" => [
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorData0),
|
||||
Gen5Operand.Vector(vectorData0 + 1),
|
||||
],
|
||||
// GFX10 DS_MIN/MAX_F32 use DATA0 as the replacement value and
|
||||
// DATA1 as the floating-point compare operand.
|
||||
"DsMinF32" or "DsMaxF32" => [
|
||||
|
|
@ -2687,6 +2905,14 @@ public static partial class Gen5ShaderTranslator
|
|||
_ => 0,
|
||||
};
|
||||
|
||||
private static uint ScalarMemoryDwordCount(string opcode) => opcode switch
|
||||
{
|
||||
"ScratchStoreDword" => 1,
|
||||
"ScratchStoreDwordx2" => 2,
|
||||
"ScratchStoreDwordx4" => 4,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static int SignExtend(uint value, int bits)
|
||||
{
|
||||
var shift = 32 - bits;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// A runtime V# whose four dwords are read from a static scalar buffer (the SRT) at an
|
||||
// offset driven by a canonical unsigned induction loop. The loop's guard bounds the set
|
||||
// of descriptors the access can ever select, so the backend can enumerate that set into
|
||||
// a flat table of native candidates and dispatch with a run-time selector.
|
||||
//
|
||||
// The plan is deliberately a pure analysis result: it names the guest SRT source and the
|
||||
// offset/index expressions, and records the proven byte range. It does not invent a
|
||||
// descriptor, and it never treats the safety extent of the SRT as a semantic proof of
|
||||
// which entries are valid descriptors; that proof is the loop guard, and each candidate
|
||||
// is decoded and validated again at materialisation.
|
||||
public sealed class BufferCandidateTablePlan
|
||||
{
|
||||
// A hard, generic ceiling on the number of native candidates one table may bind.
|
||||
// Exceeding it is an explicit unsupported case, never a silent truncation.
|
||||
public const int DefaultCap = 64;
|
||||
|
||||
public const uint DescriptorByteSize = 16;
|
||||
|
||||
// The descriptor source of the static guest SRT buffer the V# dwords are read from.
|
||||
public uint SourceSrtResource { get; set; } = DescriptorConstants.NoIndex;
|
||||
|
||||
// The guest SRT handle itself, kept for diagnostics and equivalence.
|
||||
public ScalarValue SrtHandle { get; init; } = null!;
|
||||
|
||||
// The full dynamic byte offset fed to the scalar-buffer read: base + index * stride.
|
||||
public ScalarValue OffsetExpression { get; init; } = null!;
|
||||
|
||||
// The induction variable whose loop guard bounds the candidate set.
|
||||
public ScalarValue IndexExpression { get; init; } = null!;
|
||||
|
||||
// The induction's first value and per-iteration increment.
|
||||
public ScalarValue Initial { get; init; } = null!;
|
||||
public uint Step { get; init; }
|
||||
|
||||
// The guard's exclusive/inclusive limit; null when the bound is a compile-time constant.
|
||||
public ScalarValue? Limit { get; init; }
|
||||
public bool LimitInclusive { get; init; }
|
||||
|
||||
// The byte distance between two consecutive candidates and the constant base offset.
|
||||
public uint Stride { get; init; }
|
||||
public uint BaseOffset { get; init; }
|
||||
|
||||
// The proven candidate byte span: MinOffset is the first candidate, MaxOffset is one
|
||||
// past the final candidate's descriptor, so MaxOffset - MinOffset is the accessed span.
|
||||
public uint MinOffset { get; init; }
|
||||
public uint MaxOffset { get; init; }
|
||||
|
||||
// The statically proven candidate count, or -1 when the guard limit is run-time.
|
||||
public int Count { get; init; } = -1;
|
||||
|
||||
// The SRT's declared byte size when it is a compile-time constant, else uint.MaxValue.
|
||||
// This is a memory-safety bound, not the set of valid descriptors.
|
||||
public uint SrtByteExtent { get; init; } = uint.MaxValue;
|
||||
|
||||
// The memory access records this table serves.
|
||||
public IReadOnlyList<int> MemoryIndices { get; init; } = [];
|
||||
|
||||
public int Cap { get; init; } = DefaultCap;
|
||||
|
||||
public bool IsStaticallyBounded => Count >= 0;
|
||||
|
||||
// The byte distance between two consecutive candidates: the induction step times the
|
||||
// descriptor stride.
|
||||
public uint CandidateSpacing => unchecked(Step * Stride);
|
||||
|
||||
// The byte offset of candidate index, valid only for a statically bounded table.
|
||||
public uint StaticCandidateOffset(int index) => unchecked(MinOffset + (uint)index * CandidateSpacing);
|
||||
|
||||
// The candidate count for a run-time limit, clamped to the cap. Returns -1 when the
|
||||
// limit is below the initial value (an empty loop) or overflows.
|
||||
public bool TryResolveCount(uint limit, out int count)
|
||||
{
|
||||
count = 0;
|
||||
if (!Initial.IsConstant)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var first = Initial.ConstantU32;
|
||||
var bound = LimitInclusive ? (ulong)limit + 1ul : limit;
|
||||
if (bound <= first)
|
||||
{
|
||||
count = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
var span = bound - first;
|
||||
var iterations = (span + Step - 1ul) / Step;
|
||||
if (iterations > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
count = (int)iterations;
|
||||
return true;
|
||||
}
|
||||
|
||||
public BufferCandidateTablePlan WithMemoryIndex(int memoryIndex) => new()
|
||||
{
|
||||
SourceSrtResource = SourceSrtResource,
|
||||
SrtHandle = SrtHandle,
|
||||
OffsetExpression = OffsetExpression,
|
||||
IndexExpression = IndexExpression,
|
||||
Initial = Initial,
|
||||
Step = Step,
|
||||
Limit = Limit,
|
||||
LimitInclusive = LimitInclusive,
|
||||
Stride = Stride,
|
||||
BaseOffset = BaseOffset,
|
||||
MinOffset = MinOffset,
|
||||
MaxOffset = MaxOffset,
|
||||
Count = Count,
|
||||
SrtByteExtent = SrtByteExtent,
|
||||
MemoryIndices = [.. MemoryIndices, memoryIndex],
|
||||
Cap = Cap,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// Recognises a runtime V# whose four dwords come from one static scalar buffer (the SRT)
|
||||
// at a dynamic offset driven by a canonical unsigned induction loop, and proves that the
|
||||
// loop's guard bounds the descriptors the access can select. The result is a pure
|
||||
// description of the candidate set; no descriptor is invented and no memory bound is
|
||||
// promoted into a semantic proof.
|
||||
internal static class BufferCandidateTablePlanner
|
||||
{
|
||||
public static bool TryPlan(ShaderResourcePlan plan, ScalarValue handle, int memoryIndex, out BufferCandidateTablePlan table)
|
||||
{
|
||||
table = null!;
|
||||
if (handle.Kind != ScalarValueKind.BufferHandle || handle.Operands.Length != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Every dword must be the same-width scalar-buffer read at a shared dynamic offset,
|
||||
// with the component immediates 0,4,8,12 that one dwordx4 read produces.
|
||||
ScalarValue? srt = null;
|
||||
ScalarValue? dynamicOffset = null;
|
||||
for (var word = 0; word < 4; word++)
|
||||
{
|
||||
var read = handle.Operands[word];
|
||||
if (read.Kind != ScalarValueKind.ScalarBufferWord || read.MemoryIndex < 0 || read.MemoryIndex >= plan.Memory.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var memory = plan.Memory[read.MemoryIndex];
|
||||
if (memory.Kind != MemoryResourceKind.ScalarBuffer || memory.DataBits != 32 || memory.DataDwords != 1 ||
|
||||
memory.Offset != (uint)word * sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (srt is null)
|
||||
{
|
||||
srt = read.Operands[0];
|
||||
}
|
||||
else if (!plan.Graph.Equivalent(srt, read.Operands[0]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dynamicOffset is null)
|
||||
{
|
||||
dynamicOffset = read.Operands[1];
|
||||
}
|
||||
else if (!plan.Graph.Equivalent(dynamicOffset, read.Operands[1]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// offset = base + index * stride
|
||||
var offset = dynamicOffset!;
|
||||
var baseOffset = 0u;
|
||||
if (offset.Kind == ScalarValueKind.Operation && offset.Operation == ScalarOperation.IAdd32)
|
||||
{
|
||||
if (offset.Operands[0].IsConstant)
|
||||
{
|
||||
baseOffset = offset.Operands[0].ConstantU32;
|
||||
offset = offset.Operands[1];
|
||||
}
|
||||
else if (offset.Operands[1].IsConstant)
|
||||
{
|
||||
baseOffset = offset.Operands[1].ConstantU32;
|
||||
offset = offset.Operands[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (offset.Kind != ScalarValueKind.Operation || offset.Operation != ScalarOperation.IMul32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ScalarValue index;
|
||||
uint stride;
|
||||
if (offset.Operands[0].IsConstant)
|
||||
{
|
||||
stride = offset.Operands[0].ConstantU32;
|
||||
index = offset.Operands[1];
|
||||
}
|
||||
else if (offset.Operands[1].IsConstant)
|
||||
{
|
||||
stride = offset.Operands[1].ConstantU32;
|
||||
index = offset.Operands[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stride < BufferCandidateTablePlan.DescriptorByteSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryProveInductionBound(plan, index, out var initial, out var step, out var limit, out var inclusive) || limit is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initial.IsConstant)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var srtExtent = SrtByteExtent(plan, srt!);
|
||||
var minOffset = unchecked(baseOffset + initial.ConstantU32 * stride);
|
||||
var candidate = new BufferCandidateTablePlan
|
||||
{
|
||||
SrtHandle = srt!,
|
||||
OffsetExpression = dynamicOffset!,
|
||||
IndexExpression = index,
|
||||
Initial = initial,
|
||||
Step = step,
|
||||
Limit = limit.IsConstant ? null : limit,
|
||||
LimitInclusive = inclusive,
|
||||
Stride = stride,
|
||||
BaseOffset = baseOffset,
|
||||
MinOffset = minOffset,
|
||||
SrtByteExtent = srtExtent,
|
||||
MemoryIndices = [memoryIndex],
|
||||
};
|
||||
|
||||
int count;
|
||||
uint maxOffset;
|
||||
if (limit.IsConstant)
|
||||
{
|
||||
if (!candidate.TryResolveCount(limit.ConstantU32, out count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count <= 0 || count > candidate.Cap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
maxOffset = unchecked(candidate.StaticCandidateOffset(count - 1) + BufferCandidateTablePlan.DescriptorByteSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!plan.ValidateRuntimeValue(limit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// A run-time limit is capped and memory-checked at materialisation; the static
|
||||
// start of the range still has to stay inside the declared SRT extent.
|
||||
count = -1;
|
||||
maxOffset = uint.MaxValue;
|
||||
}
|
||||
|
||||
if (maxOffset < minOffset || (maxOffset != uint.MaxValue && maxOffset > srtExtent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
table = new BufferCandidateTablePlan
|
||||
{
|
||||
SrtHandle = srt!,
|
||||
OffsetExpression = dynamicOffset!,
|
||||
IndexExpression = index,
|
||||
Initial = initial,
|
||||
Step = step,
|
||||
Limit = limit.IsConstant ? null : limit,
|
||||
LimitInclusive = inclusive,
|
||||
Stride = stride,
|
||||
BaseOffset = baseOffset,
|
||||
MinOffset = minOffset,
|
||||
MaxOffset = maxOffset,
|
||||
Count = count,
|
||||
SrtByteExtent = srtExtent,
|
||||
MemoryIndices = [memoryIndex],
|
||||
Cap = candidate.Cap,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// A canonical unsigned induction: the value is a phi with a constant initial and an
|
||||
// add-recurrence, and a branch guarded by an unsigned comparison against the phi
|
||||
// lives at the loop header or one of its predecessors.
|
||||
private static bool TryProveInductionBound(
|
||||
ShaderResourcePlan plan,
|
||||
ScalarValue index,
|
||||
out ScalarValue initial,
|
||||
out uint step,
|
||||
out ScalarValue? limit,
|
||||
out bool inclusive)
|
||||
{
|
||||
initial = null!;
|
||||
step = 0;
|
||||
limit = null;
|
||||
inclusive = false;
|
||||
if (index.Kind != ScalarValueKind.Phi)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ScalarValue? found = null;
|
||||
var initialCount = 0;
|
||||
foreach (var operand in index.Operands)
|
||||
{
|
||||
if (operand.Kind == ScalarValueKind.Operation && operand.Operation == ScalarOperation.IAdd32)
|
||||
{
|
||||
if (ReferenceEquals(operand.Operands[0], index) && operand.Operands[1].IsConstant)
|
||||
{
|
||||
step = operand.Operands[1].ConstantU32;
|
||||
}
|
||||
else if (ReferenceEquals(operand.Operands[1], index) && operand.Operands[0].IsConstant)
|
||||
{
|
||||
step = operand.Operands[0].ConstantU32;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (!ReferenceEquals(operand, index))
|
||||
{
|
||||
found = operand;
|
||||
initialCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (step == 0 || initialCount != 1 || found is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
initial = found;
|
||||
|
||||
var flow = plan.Graph.ControlFlow;
|
||||
var guardBlock = (int?)null;
|
||||
foreach (var pair in plan.Graph.BranchConditions)
|
||||
{
|
||||
var condition = pair.Value;
|
||||
if (condition.Kind != ScalarValueKind.Operation)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ScalarValue? bound;
|
||||
bool boundInclusive;
|
||||
switch (condition.Operation)
|
||||
{
|
||||
case ScalarOperation.ULessThan32 when MatchesIndex(plan, condition.Operands[0], index, step):
|
||||
bound = condition.Operands[1];
|
||||
boundInclusive = false;
|
||||
break;
|
||||
case ScalarOperation.ULessThanEqual32 when MatchesIndex(plan, condition.Operands[0], index, step):
|
||||
bound = condition.Operands[1];
|
||||
boundInclusive = true;
|
||||
break;
|
||||
case ScalarOperation.UGreaterThan32 when MatchesIndex(plan, condition.Operands[1], index, step):
|
||||
bound = condition.Operands[0];
|
||||
boundInclusive = false;
|
||||
break;
|
||||
case ScalarOperation.UGreaterThanEqual32 when MatchesIndex(plan, condition.Operands[1], index, step):
|
||||
bound = condition.Operands[0];
|
||||
boundInclusive = true;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!bound.IsConstant && !plan.ValidateRuntimeValue(bound))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var block = BlockOf(plan, pair.Key);
|
||||
if (block < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The guard must live at the loop header that owns the phi or one of the
|
||||
// blocks feeding it, so it really controls the recurrence.
|
||||
var header = index.PhiBlock;
|
||||
if (block != header && !flow.Predecessors[header].Contains(block))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (limit is not null && !plan.Graph.Equivalent(limit, bound))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
limit = bound;
|
||||
inclusive = boundInclusive;
|
||||
guardBlock = block;
|
||||
}
|
||||
|
||||
return limit is not null && guardBlock is not null;
|
||||
}
|
||||
|
||||
// The guard compares either the induction value itself or its next value; both bound
|
||||
// the same executed set, so a one-step add of the phi is accepted.
|
||||
private static bool MatchesIndex(ShaderResourcePlan plan, ScalarValue operand, ScalarValue index, uint step)
|
||||
{
|
||||
if (plan.Graph.Equivalent(operand, index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operand.Kind != ScalarValueKind.Operation || operand.Operation != ScalarOperation.IAdd32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plan.Graph.Equivalent(operand.Operands[0], index) && operand.Operands[1].IsConstant)
|
||||
{
|
||||
return operand.Operands[1].ConstantU32 == step;
|
||||
}
|
||||
|
||||
if (plan.Graph.Equivalent(operand.Operands[1], index) && operand.Operands[0].IsConstant)
|
||||
{
|
||||
return operand.Operands[0].ConstantU32 == step;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int BlockOf(ShaderResourcePlan plan, uint pc)
|
||||
{
|
||||
var blocks = plan.Graph.ControlFlow.Blocks;
|
||||
for (var index = 0; index < blocks.Count; index++)
|
||||
{
|
||||
if (pc >= blocks[index].StartPc && pc < blocks[index].EndPc)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The SRT's declared byte size when both its stride and record count are constants.
|
||||
// This is a memory-safety bound, never evidence that any entry is a valid descriptor.
|
||||
private static uint SrtByteExtent(ShaderResourcePlan plan, ScalarValue srt)
|
||||
{
|
||||
if (srt.Operands.Length != 4)
|
||||
{
|
||||
return uint.MaxValue;
|
||||
}
|
||||
|
||||
var strideWord = plan.Graph.ResolveInvariantPhi(srt.Operands[1]) ?? srt.Operands[1];
|
||||
var recordsWord = plan.Graph.ResolveInvariantPhi(srt.Operands[2]) ?? srt.Operands[2];
|
||||
if (!strideWord.IsConstant || !recordsWord.IsConstant)
|
||||
{
|
||||
return uint.MaxValue;
|
||||
}
|
||||
|
||||
var stride = (strideWord.ConstantU32 >> 16) & 0x3FFFu;
|
||||
var records = recordsWord.ConstantU32;
|
||||
var size = stride == 0 ? records : (ulong)stride * records;
|
||||
return size > uint.MaxValue ? uint.MaxValue : (uint)size;
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,17 @@ public static class DeviceAddressRangePlanner
|
|||
continue;
|
||||
}
|
||||
|
||||
// A FLAT address built from an aperture base is an LDS or scratch
|
||||
// pointer; the backend routes it per lane, so it owns no device range.
|
||||
if (memory.Kind == MemoryResourceKind.Flat)
|
||||
{
|
||||
memory.AddressSpace = ClassifyFlatAddress(plan, handle.Operands[1], plan.Accesses[index]!.Active);
|
||||
if (memory.AddressSpace != FlatAddressSpace.Global)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var slot = handles.FindIndex(existing => plan.Graph.Equivalent(existing, handle));
|
||||
if (slot < 0)
|
||||
{
|
||||
|
|
@ -79,6 +90,132 @@ public static class DeviceAddressRangePlanner
|
|||
return ranges;
|
||||
}
|
||||
|
||||
// A FLAT address is local when its high dword provably lies in an aperture.
|
||||
// Two shapes occur: the high dword is clamped between constants inside the
|
||||
// apertures (v_med3_u32 with the hard-coded PS5 bounds), or it derives from an
|
||||
// aperture operand, which shaders often merge into the upper half of a
|
||||
// computed dword with SDWA WORD1.
|
||||
private static FlatAddressSpace ClassifyFlatAddress(ShaderResourcePlan plan, ScalarValue high, ScalarValue? active)
|
||||
{
|
||||
var range = UnsignedRange(plan, high, active, []);
|
||||
var (low, highBound) = IsEmpty(range) ? (0u, uint.MaxValue) : range;
|
||||
var lowShared = Gen5InlineConstants.IsSharedApertureHigh(low);
|
||||
var lowPrivate = Gen5InlineConstants.IsPrivateApertureHigh(low);
|
||||
var highShared = Gen5InlineConstants.IsSharedApertureHigh(highBound);
|
||||
var highPrivate = Gen5InlineConstants.IsPrivateApertureHigh(highBound);
|
||||
// The two apertures are adjacent windows, so bounds inside them keep every
|
||||
// value in between inside them too.
|
||||
if ((lowShared || lowPrivate) && (highShared || highPrivate))
|
||||
{
|
||||
return (lowShared || highShared, lowPrivate || highPrivate) switch
|
||||
{
|
||||
(true, true) => FlatAddressSpace.SharedOrPrivate,
|
||||
(true, false) => FlatAddressSpace.Shared,
|
||||
_ => FlatAddressSpace.Private,
|
||||
};
|
||||
}
|
||||
|
||||
return ClassifyByApertureOperand(high);
|
||||
}
|
||||
|
||||
// Conservative unsigned bounds of a 32-bit value as the access sees it. Phi, Select,
|
||||
// UMin and UMax yield one of their inputs, so a cycle adds nothing (Empty); a select
|
||||
// on the access's own EXEC mask contributes only its active arm.
|
||||
// The mask is the same node when EXEC was not written in between, even when its
|
||||
// value is unknown (an undefined node is never structurally equivalent).
|
||||
private static readonly (uint Low, uint High) Empty = (uint.MaxValue, 0u);
|
||||
|
||||
private static bool IsEmpty((uint Low, uint High) range) => range.Low > range.High;
|
||||
|
||||
private static (uint Low, uint High) Union((uint Low, uint High) left, (uint Low, uint High) right) =>
|
||||
IsEmpty(left) ? right : IsEmpty(right) ? left : (Math.Min(left.Low, right.Low), Math.Max(left.High, right.High));
|
||||
|
||||
private static (uint Low, uint High) UnsignedRange(ShaderResourcePlan plan, ScalarValue value, ScalarValue? active,
|
||||
HashSet<ScalarValue> visiting)
|
||||
{
|
||||
if (value.Kind == ScalarValueKind.Constant && value.Type == ScalarValueType.U32)
|
||||
return (value.ConstantU32, value.ConstantU32);
|
||||
if (value.Kind == ScalarValueKind.MemoryAperture)
|
||||
{
|
||||
var aperture = (uint)(Gen5InlineConstants.DecodeAperture64((uint)value.Payload) >> 32);
|
||||
return (aperture, aperture);
|
||||
}
|
||||
|
||||
if (!visiting.Add(value))
|
||||
return Empty;
|
||||
try
|
||||
{
|
||||
switch (value.Kind)
|
||||
{
|
||||
case ScalarValueKind.Select when value.Operands.Length == 3 &&
|
||||
active is not null && (ReferenceEquals(value.Operands[0], active) || plan.Graph.Equivalent(value.Operands[0], active)):
|
||||
return UnsignedRange(plan, value.Operands[1], active, visiting);
|
||||
case ScalarValueKind.Select when value.Operands.Length == 3:
|
||||
return Union(UnsignedRange(plan, value.Operands[1], active, visiting),
|
||||
UnsignedRange(plan, value.Operands[2], active, visiting));
|
||||
case ScalarValueKind.Phi:
|
||||
{
|
||||
var range = Empty;
|
||||
foreach (var operand in value.Operands)
|
||||
range = Union(range, UnsignedRange(plan, operand, active, visiting));
|
||||
return range;
|
||||
}
|
||||
case ScalarValueKind.Operation when value.Operation is ScalarOperation.UMin32 or ScalarOperation.UMax32:
|
||||
{
|
||||
var left = UnsignedRange(plan, value.Operands[0], active, visiting);
|
||||
var right = UnsignedRange(plan, value.Operands[1], active, visiting);
|
||||
if (IsEmpty(left) || IsEmpty(right))
|
||||
return IsEmpty(left) ? right : left;
|
||||
return value.Operation == ScalarOperation.UMin32
|
||||
? (Math.Min(left.Low, right.Low), Math.Min(left.High, right.High))
|
||||
: (Math.Max(left.Low, right.Low), Math.Max(left.High, right.High));
|
||||
}
|
||||
default:
|
||||
return (0, uint.MaxValue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
visiting.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static FlatAddressSpace ClassifyByApertureOperand(ScalarValue high)
|
||||
{
|
||||
var shared = false;
|
||||
var @private = false;
|
||||
var pending = new Stack<ScalarValue>();
|
||||
var visited = new HashSet<ScalarValue>();
|
||||
pending.Push(high);
|
||||
while (pending.TryPop(out var value))
|
||||
{
|
||||
if (!visited.Add(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.Kind == ScalarValueKind.MemoryAperture)
|
||||
{
|
||||
if (Gen5InlineConstants.IsSharedAperture((uint)value.Payload))
|
||||
shared = true;
|
||||
else
|
||||
@private = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var operand in value.Operands)
|
||||
pending.Push(operand);
|
||||
}
|
||||
|
||||
return (shared, @private) switch
|
||||
{
|
||||
(true, true) => FlatAddressSpace.SharedOrPrivate,
|
||||
(true, false) => FlatAddressSpace.Shared,
|
||||
(false, true) => FlatAddressSpace.Private,
|
||||
_ => FlatAddressSpace.Global,
|
||||
};
|
||||
}
|
||||
|
||||
// An access is bounded when its only run-time term is the record's immediate offset:
|
||||
// a scalar read, or a global access whose vector offset is a constant zero.
|
||||
private static bool IsBoundedOffset(ShaderResourcePlan plan, int memoryIndex)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// Where a guest V#'s dwords come from. A static descriptor is evaluated ahead of
|
||||
// dispatch into the dense buffer table; a runtime descriptor is fetched by the
|
||||
// shader itself from guest memory (a descriptor array indexed at run time).
|
||||
public enum BufferDescriptorProvenance : byte
|
||||
{
|
||||
Static,
|
||||
Runtime,
|
||||
}
|
||||
|
||||
// How the Vulkan backend lowers a guest buffer descriptor. This is backend
|
||||
// policy, deliberately kept out of the guest descriptor representation: the
|
||||
// same guest V# could be lowered differently depending on the access, the
|
||||
// device capabilities or cost, without changing what the guest descriptor is.
|
||||
public enum BufferLoweringStrategy : byte
|
||||
{
|
||||
// A compile-time V# bound as a native storage-buffer descriptor.
|
||||
NativeBinding,
|
||||
|
||||
// A runtime V# addressed through the physical-storage-buffer page table.
|
||||
// Base, stride and byte bounds are read from the descriptor's raw words at
|
||||
// run time, so an untyped/raw access needs no compile-time value at all.
|
||||
PhysicalStorageBuffer,
|
||||
|
||||
// A runtime V# enumerated into a bounded set of native candidate bindings
|
||||
// and selected with an OpSwitch. Required when the access's format/type
|
||||
// cannot be reconstructed from the raw words alone.
|
||||
BoundedCandidateTable,
|
||||
}
|
||||
|
||||
// The guest buffer view (V#) as the hardware sees it, independent of how the
|
||||
// host binds it. Static descriptors reference a dense buffer already
|
||||
// materialised by the host; runtime descriptors are loaded by the shader.
|
||||
public sealed class GuestBufferDescriptor
|
||||
{
|
||||
public BufferDescriptorProvenance Provenance { get; init; }
|
||||
|
||||
// A static descriptor's index in ShaderResourceInfo.Buffers.
|
||||
public uint StaticResource { get; init; } = DescriptorConstants.NoIndex;
|
||||
|
||||
// Chooses the backend lowering for one access through this descriptor.
|
||||
// A static descriptor always binds natively. A runtime descriptor is
|
||||
// addressed through the page table when the access is a raw dword/subword
|
||||
// access, and otherwise needs candidate enumeration because Vulkan fixes
|
||||
// the resource's format/type at pipeline creation.
|
||||
public BufferLoweringStrategy ChooseStrategy(bool typed, bool formatted, bool atomic) =>
|
||||
Provenance == BufferDescriptorProvenance.Static
|
||||
? BufferLoweringStrategy.NativeBinding
|
||||
: typed || formatted || atomic
|
||||
? BufferLoweringStrategy.BoundedCandidateTable
|
||||
: BufferLoweringStrategy.PhysicalStorageBuffer;
|
||||
}
|
||||
|
|
@ -34,9 +34,9 @@ public static class GuestImageFormat
|
|||
[
|
||||
(1, ImageNumericClass.Float), (5, ImageNumericClass.Uint), (7, ImageNumericClass.Float), (8, ImageNumericClass.Float),
|
||||
(11, ImageNumericClass.Uint), (12, ImageNumericClass.Sint), (13, ImageNumericClass.Float), (14, ImageNumericClass.Float),
|
||||
(15, ImageNumericClass.Float), (18, ImageNumericClass.Uint), (19, ImageNumericClass.Sint), (20, ImageNumericClass.Uint),
|
||||
(15, ImageNumericClass.Float), (16, ImageNumericClass.Float), (18, ImageNumericClass.Uint), (19, ImageNumericClass.Sint), (20, ImageNumericClass.Uint),
|
||||
(21, ImageNumericClass.Sint), (22, ImageNumericClass.Float), (23, ImageNumericClass.Float), (24, ImageNumericClass.Float),
|
||||
(27, ImageNumericClass.Uint), (28, ImageNumericClass.Sint), (29, ImageNumericClass.Float), (34, ImageNumericClass.Uint),
|
||||
(27, ImageNumericClass.Uint), (28, ImageNumericClass.Sint), (29, ImageNumericClass.Float), (30, ImageNumericClass.Float), (34, ImageNumericClass.Uint),
|
||||
(36, ImageNumericClass.Float), (50, ImageNumericClass.Float), (54, ImageNumericClass.Uint), (56, ImageNumericClass.Float),
|
||||
(57, ImageNumericClass.Float), (60, ImageNumericClass.Uint), (61, ImageNumericClass.Sint), (62, ImageNumericClass.Uint),
|
||||
(63, ImageNumericClass.Sint), (64, ImageNumericClass.Float), (65, ImageNumericClass.Float), (66, ImageNumericClass.Float),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,20 @@ using System.Collections.Generic;
|
|||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// Where a FLAT access can land: LDS or scratch when its high dword lies in the shared
|
||||
// or private aperture, otherwise global memory.
|
||||
public enum FlatAddressSpace : byte
|
||||
{
|
||||
// No aperture reaches the address: an ordinary device address.
|
||||
Global,
|
||||
// The address is built from SHARED_BASE/SHARED_LIMIT.
|
||||
Shared,
|
||||
// The address is built from PRIVATE_BASE/PRIVATE_LIMIT.
|
||||
Private,
|
||||
// Both apertures reach the address; the aperture is chosen per lane.
|
||||
SharedOrPrivate,
|
||||
}
|
||||
|
||||
public enum MemoryResourceKind : byte
|
||||
{
|
||||
None,
|
||||
|
|
@ -90,10 +104,20 @@ public sealed class MemoryAccessInfo
|
|||
public bool IndexEnabled { get; init; }
|
||||
public bool OffsetEnabled { get; init; }
|
||||
public bool PlanningOnly { get; set; }
|
||||
// A FLAT access whose address derives from an LDS/scratch aperture. Set by the
|
||||
// device-address planner; such an access is routed per lane like the hardware
|
||||
// and has no device-address range of its own.
|
||||
public FlatAddressSpace AddressSpace { get; set; }
|
||||
// A scalar buffer load whose descriptor only exists on the device: the shader reads
|
||||
// through the descriptor in its registers instead of a host-bound buffer.
|
||||
public bool DeviceDescriptor { get; set; }
|
||||
|
||||
// The guest V# behind a buffer access, filled in by the resource tracker.
|
||||
// Null until then. Its provenance tells the backend which lowering
|
||||
// strategy to pick; the access shape (Typed/Formatted/Access) completes
|
||||
// the choice via GuestBufferDescriptor.ChooseStrategy.
|
||||
public GuestBufferDescriptor? BufferDescriptor { get; set; }
|
||||
|
||||
public bool IsAddressKind =>
|
||||
Kind is MemoryResourceKind.ScalarAddress or MemoryResourceKind.Flat or
|
||||
MemoryResourceKind.Global or MemoryResourceKind.Scratch;
|
||||
|
|
@ -110,7 +134,7 @@ public sealed class MemoryAccessInfo
|
|||
ImageHasMip == other.ImageHasMip && ImageR128 == other.ImageR128 && Glc == other.Glc && Slc == other.Slc &&
|
||||
IndexEnabled == other.IndexEnabled && OffsetEnabled == other.OffsetEnabled &&
|
||||
Resource == other.Resource && Sampler == other.Sampler && PlanningOnly == other.PlanningOnly &&
|
||||
DeviceDescriptor == other.DeviceDescriptor;
|
||||
DeviceDescriptor == other.DeviceDescriptor && AddressSpace == other.AddressSpace;
|
||||
}
|
||||
|
||||
// Every memory access of a program, indexed by program counter and component.
|
||||
|
|
@ -271,9 +295,8 @@ public sealed class MemoryAccessTable
|
|||
var opcode = instruction.Opcode;
|
||||
if (opcode is "ImageBvhIntersectRay" or "ImageBvh64IntersectRay")
|
||||
{
|
||||
// The current backends return a deterministic miss for raw GFX10
|
||||
// BVH traversal. Do not materialize its descriptor as a texture;
|
||||
// BVH descriptors do not use the regular image descriptor layout.
|
||||
// A BVH T# is not an image descriptor: the backend reads the nodes
|
||||
// through device addresses, so nothing is materialized here.
|
||||
return new MemoryAccessInfo
|
||||
{
|
||||
Pc = instruction.Pc,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// Copies guest bytes only when they are already resident on the CPU side. It never
|
||||
// synchronizes: a range the GPU may still own, or that a clean read would refuse,
|
||||
// returns false so the caller falls back to a full materialization.
|
||||
public delegate bool ResidentGuestBytesReader(ulong address, Span<byte> destination, bool clean);
|
||||
|
||||
// Materialization is a pure function of the plan, the draw's user data, the shader
|
||||
// base, the compute state and the guest words it reads. This cache records exactly
|
||||
// those words and reuses the result while every one of them is unchanged, so a draw
|
||||
// that re-binds the same resources skips the descriptor walk. Any failed read, any
|
||||
// GPU-owned range and any byte difference make the entry miss; nothing is guessed.
|
||||
public sealed class ResourceMaterializationCache
|
||||
{
|
||||
// Two generations approximate LRU: a full young generation becomes the old one,
|
||||
// and an old entry that is used again is promoted back.
|
||||
private readonly int _generationCapacity;
|
||||
private Dictionary<ulong, Entry> _young = new();
|
||||
private Dictionary<ulong, Entry> _old = new();
|
||||
private byte[] _scratch = new byte[256];
|
||||
|
||||
public ResourceMaterializationCache(int generationCapacity = 16384)
|
||||
{
|
||||
_generationCapacity = Math.Max(1, generationCapacity);
|
||||
}
|
||||
|
||||
private static long _totalHits;
|
||||
private static long _totalMisses;
|
||||
private static long _totalUncacheable;
|
||||
|
||||
public long Hits { get; private set; }
|
||||
public long Misses { get; private set; }
|
||||
public long Uncacheable { get; private set; }
|
||||
|
||||
// Process-wide counters since the previous call, for the periodic render report.
|
||||
public static string TakeReport()
|
||||
{
|
||||
var hits = Interlocked.Exchange(ref _totalHits, 0);
|
||||
var misses = Interlocked.Exchange(ref _totalMisses, 0);
|
||||
var uncacheable = Interlocked.Exchange(ref _totalUncacheable, 0);
|
||||
var total = hits + misses;
|
||||
return FormattableString.Invariant(
|
||||
$"[PERF][RESOURCE_CACHE] hits={hits} misses={misses} uncacheable={uncacheable} hit_rate={(total == 0 ? 0 : hits * 100.0 / total):F1}%");
|
||||
}
|
||||
|
||||
public bool Materialize(
|
||||
ShaderResourcePlan plan,
|
||||
ResourceRuntimeInputs inputs,
|
||||
ResidentGuestBytesReader residentReader,
|
||||
ref ResourceSnapshot snapshot,
|
||||
ref ResourceSpecialization specialization,
|
||||
out ResourceMaterializationFailure failure)
|
||||
{
|
||||
var key = KeyOf(plan, inputs);
|
||||
if (TryFind(key, plan, inputs, out var cached) && Validate(cached, residentReader))
|
||||
{
|
||||
Hits++;
|
||||
Interlocked.Increment(ref _totalHits);
|
||||
snapshot = cached.Snapshot;
|
||||
specialization = cached.Specialization;
|
||||
failure = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
Misses++;
|
||||
Interlocked.Increment(ref _totalMisses);
|
||||
var recorder = new ReadRecorder();
|
||||
var recording = new ResourceRuntimeInputs
|
||||
{
|
||||
UserData = inputs.UserData,
|
||||
ShaderBase = inputs.ShaderBase,
|
||||
ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false),
|
||||
ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true),
|
||||
ComputeState = inputs.ComputeState,
|
||||
};
|
||||
if (!ResourceMaterializer.Materialize(plan, recording, ref snapshot, ref specialization, out failure))
|
||||
return false;
|
||||
|
||||
if (recorder.Failed)
|
||||
{
|
||||
Uncacheable++;
|
||||
Interlocked.Increment(ref _totalUncacheable);
|
||||
return true;
|
||||
}
|
||||
|
||||
Store(key, recorder.Build(plan, inputs, snapshot, specialization));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong KeyOf(ShaderResourcePlan plan, ResourceRuntimeInputs inputs)
|
||||
{
|
||||
var hash = new HashCode();
|
||||
hash.Add(RuntimeHelpers.GetHashCode(plan));
|
||||
hash.Add(inputs.ShaderBase);
|
||||
hash.Add(inputs.ComputeState);
|
||||
var userData = inputs.UserData;
|
||||
hash.Add(userData.Count);
|
||||
for (var index = 0; index < userData.Count; index++)
|
||||
hash.Add(userData[index]);
|
||||
var low = (uint)hash.ToHashCode();
|
||||
// A second, independent mix keeps accidental collisions out of the 64-bit key.
|
||||
var high = 0x9E3779B9u;
|
||||
for (var index = 0; index < userData.Count; index++)
|
||||
high = (high ^ userData[index]) * 0x01000193u;
|
||||
return ((ulong)high << 32) | low;
|
||||
}
|
||||
|
||||
private bool TryFind(ulong key, ShaderResourcePlan plan, ResourceRuntimeInputs inputs, out Entry entry)
|
||||
{
|
||||
if (_young.TryGetValue(key, out entry!))
|
||||
return entry.Matches(plan, inputs);
|
||||
if (!_old.TryGetValue(key, out entry!) || !entry.Matches(plan, inputs))
|
||||
return false;
|
||||
_old.Remove(key);
|
||||
Store(key, entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Store(ulong key, Entry entry)
|
||||
{
|
||||
if (_young.Count >= _generationCapacity && !_young.ContainsKey(key))
|
||||
{
|
||||
_old = _young;
|
||||
_young = new Dictionary<ulong, Entry>(_generationCapacity);
|
||||
}
|
||||
|
||||
_young[key] = entry;
|
||||
}
|
||||
|
||||
private bool Validate(Entry entry, ResidentGuestBytesReader residentReader)
|
||||
{
|
||||
for (var index = 0; index < entry.RangeAddresses.Length; index++)
|
||||
{
|
||||
var length = entry.RangeLengths[index];
|
||||
if (_scratch.Length < length)
|
||||
_scratch = new byte[Math.Max(length, _scratch.Length * 2)];
|
||||
var current = _scratch.AsSpan(0, length);
|
||||
if (!residentReader(entry.RangeAddresses[index], current, entry.RangeClean[index]) ||
|
||||
!current.SequenceEqual(entry.Bytes.AsSpan(entry.RangeOffsets[index], length)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public required ShaderResourcePlan Plan { get; init; }
|
||||
public required uint[] UserData { get; init; }
|
||||
public required ulong ShaderBase { get; init; }
|
||||
public required ComputeSelectorState? ComputeState { get; init; }
|
||||
public required ulong[] RangeAddresses { get; init; }
|
||||
public required int[] RangeOffsets { get; init; }
|
||||
public required int[] RangeLengths { get; init; }
|
||||
public required bool[] RangeClean { get; init; }
|
||||
public required byte[] Bytes { get; init; }
|
||||
public required ResourceSnapshot Snapshot { get; init; }
|
||||
public required ResourceSpecialization Specialization { get; init; }
|
||||
|
||||
public bool Matches(ShaderResourcePlan plan, ResourceRuntimeInputs inputs)
|
||||
{
|
||||
if (!ReferenceEquals(Plan, plan) || ShaderBase != inputs.ShaderBase ||
|
||||
!Nullable.Equals(ComputeState, inputs.ComputeState) || UserData.Length != inputs.UserData.Count)
|
||||
return false;
|
||||
for (var index = 0; index < UserData.Length; index++)
|
||||
if (UserData[index] != inputs.UserData[index])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ReadRecorder
|
||||
{
|
||||
private readonly List<(ulong Address, uint Word, bool Clean)> _reads = new();
|
||||
|
||||
public bool Failed { get; private set; }
|
||||
|
||||
public GuestWordReader? Wrap(GuestWordReader? inner, bool clean)
|
||||
{
|
||||
if (inner is null)
|
||||
return null;
|
||||
return (ulong address, out uint word) =>
|
||||
{
|
||||
if (!inner(address, out word))
|
||||
{
|
||||
Failed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
_reads.Add((address, word, clean));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
public Entry Build(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, ResourceSnapshot snapshot,
|
||||
ResourceSpecialization specialization)
|
||||
{
|
||||
_reads.Sort((left, right) => left.Address.CompareTo(right.Address));
|
||||
var addresses = new List<ulong>();
|
||||
var offsets = new List<int>();
|
||||
var lengths = new List<int>();
|
||||
var clean = new List<bool>();
|
||||
var bytes = new List<byte>(_reads.Count * sizeof(uint));
|
||||
ulong end = 0;
|
||||
var previous = ulong.MaxValue;
|
||||
foreach (var (address, word, wordClean) in _reads)
|
||||
{
|
||||
if (address == previous)
|
||||
{
|
||||
clean[^1] |= wordClean;
|
||||
continue;
|
||||
}
|
||||
|
||||
previous = address;
|
||||
if (addresses.Count == 0 || address != end)
|
||||
{
|
||||
addresses.Add(address);
|
||||
offsets.Add(bytes.Count);
|
||||
lengths.Add(0);
|
||||
clean.Add(false);
|
||||
}
|
||||
|
||||
lengths[^1] += sizeof(uint);
|
||||
clean[^1] |= wordClean;
|
||||
bytes.Add((byte)word);
|
||||
bytes.Add((byte)(word >> 8));
|
||||
bytes.Add((byte)(word >> 16));
|
||||
bytes.Add((byte)(word >> 24));
|
||||
end = address + sizeof(uint);
|
||||
}
|
||||
|
||||
var userData = new uint[inputs.UserData.Count];
|
||||
for (var index = 0; index < userData.Length; index++)
|
||||
userData[index] = inputs.UserData[index];
|
||||
return new Entry
|
||||
{
|
||||
Plan = plan,
|
||||
UserData = userData,
|
||||
ShaderBase = inputs.ShaderBase,
|
||||
ComputeState = inputs.ComputeState,
|
||||
RangeAddresses = [.. addresses],
|
||||
RangeOffsets = [.. offsets],
|
||||
RangeLengths = [.. lengths],
|
||||
RangeClean = [.. clean],
|
||||
Bytes = [.. bytes],
|
||||
Snapshot = snapshot,
|
||||
Specialization = specialization,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,17 @@ public static class ResourceMaterializer
|
|||
public uint[] FlattenedTable = [];
|
||||
public uint[] UserData = [];
|
||||
public List<IndirectImageTable> IndirectImages = [];
|
||||
public List<BufferCandidateTable> BufferCandidateTables = [];
|
||||
}
|
||||
|
||||
// One bounded runtime V# table resolved for this draw: the distinct candidate descriptors
|
||||
// and the run-time offset -> candidate mapping the emitter searches.
|
||||
private sealed class BufferCandidateTable
|
||||
{
|
||||
public int MemoryIndex;
|
||||
public List<uint> Keys = [];
|
||||
public List<uint> Candidates = [];
|
||||
public List<DescriptorWords> Descriptors = [];
|
||||
}
|
||||
|
||||
public static bool Materialize(
|
||||
|
|
@ -142,7 +153,10 @@ public static class ResourceMaterializer
|
|||
if (indirect.DirectCandidates is { } directCandidates)
|
||||
{
|
||||
if (!RuntimeValueEvaluator.EvaluateSources(plan, directCandidates.Select(candidate => candidate.Source).ToArray(),
|
||||
cleanInputs, [], evaluateTable: false, out var descriptors, out _)) return false;
|
||||
cleanInputs, [], evaluateTable: false, out var descriptors, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var directTable = new IndirectImageTable { Resource = (uint)imageIndex };
|
||||
for (var candidateIndex = 0; candidateIndex < descriptors.Count; candidateIndex++)
|
||||
{
|
||||
|
|
@ -163,6 +177,21 @@ public static class ResourceMaterializer
|
|||
if (directTable.Descriptors.Count > 1) snapshot.IndirectImages.Add(directTable);
|
||||
continue;
|
||||
}
|
||||
if (indirect.WaveIndexed is { } waveIndexed)
|
||||
{
|
||||
if (!RuntimeValueEvaluator.EvaluateSources(plan, [indirect.HeapSource], cleanInputs, [], evaluateTable: false,
|
||||
out var waveSources, out _))
|
||||
return false;
|
||||
if (!MaterializeWaveIndexedImage(indirect, waveIndexed, waveSources[0], image, image.R128, inputs, out var waveTable, out failure))
|
||||
return false;
|
||||
snapshot.Images[imageIndex] = waveTable.Descriptors[(int)waveTable.Candidates[0]].Dwords;
|
||||
if (waveTable.Descriptors.Count > 1)
|
||||
{
|
||||
waveTable.Resource = (uint)imageIndex;
|
||||
snapshot.IndirectImages.Add(waveTable);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (indirect.Dense)
|
||||
{
|
||||
if (!RuntimeValueEvaluator.EvaluateSources(plan, [indirect.HeapSource], cleanInputs, [], evaluateTable: false,
|
||||
|
|
@ -211,10 +240,114 @@ public static class ResourceMaterializer
|
|||
snapshot.Samplers = new uint[plan.Info.Samplers.Count][];
|
||||
for (var index = 0; index < snapshot.Samplers.Length; index++)
|
||||
snapshot.Samplers[index] = values[cursor++].Dwords;
|
||||
|
||||
// Bounded runtime V# tables: the whole table is read and validated before it is
|
||||
// published, so a single unreadable candidate leaves the previous snapshot intact.
|
||||
foreach (var candidatePlan in plan.BufferCandidateTables)
|
||||
{
|
||||
if (candidatePlan.SourceSrtResource == DescriptorConstants.NoIndex ||
|
||||
!RuntimeValueEvaluator.EvaluateSources(plan, [candidatePlan.SourceSrtResource], inputs, [], evaluateTable: false,
|
||||
out var srtSources, out _) || srtSources.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MaterializeBufferCandidateTable(plan, candidatePlan, srtSources[0], inputs, out var candidateTable))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot.BufferCandidateTables.Add(candidateTable);
|
||||
}
|
||||
|
||||
snapshot.UserData = inputs.UserData.ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reads every candidate the loop guard can select from the guest SRT, validates it and
|
||||
// records the run-time probe key (its base-address low dword) -> candidate mapping.
|
||||
// Distinct keys are required so the mapping is unambiguous; identical descriptors share
|
||||
// one candidate.
|
||||
private static bool MaterializeBufferCandidateTable(
|
||||
ShaderResourcePlan plan,
|
||||
BufferCandidateTablePlan table,
|
||||
DescriptorWords srt,
|
||||
ResourceRuntimeInputs inputs,
|
||||
out BufferCandidateTable result)
|
||||
{
|
||||
result = new BufferCandidateTable { MemoryIndex = table.MemoryIndices.Count != 0 ? table.MemoryIndices[0] : -1 };
|
||||
if (srt.DwordCount != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int count;
|
||||
if (table.IsStaticallyBounded)
|
||||
{
|
||||
count = table.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (table.Limit is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var scratch = RuntimeEvaluationScratch.Rent();
|
||||
var evaluator = new RuntimeValueEvaluator(scratch, plan, inputs);
|
||||
if (!evaluator.Evaluate(table.Limit, out var limit) || !table.TryResolveCount(limit, out count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (count <= 0 || count > table.Cap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var keyToCandidate = new Dictionary<uint, uint>();
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
var offset = unchecked(table.MinOffset + (uint)index * table.CandidateSpacing);
|
||||
var words = new uint[4];
|
||||
for (uint dword = 0; dword < 4; dword++)
|
||||
{
|
||||
if (!ReadScalarBufferWord(srt.Dwords, offset, dword * sizeof(uint), inputs, out words[dword]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// A descriptor whose reserved bits are set is unbound and reads as zero.
|
||||
if ((words[3] >> 30) != 0)
|
||||
{
|
||||
Array.Clear(words);
|
||||
}
|
||||
|
||||
var descriptor = new DescriptorWords(words);
|
||||
if (keyToCandidate.TryGetValue(words[0], out var existing))
|
||||
{
|
||||
// Two different descriptors sharing one probe key cannot be told apart at
|
||||
// run time; reject precisely instead of guessing.
|
||||
if (!result.Descriptors[(int)existing].SameAs(descriptor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = (uint)result.Descriptors.Count;
|
||||
result.Descriptors.Add(descriptor);
|
||||
keyToCandidate[words[0]] = candidate;
|
||||
}
|
||||
|
||||
result.Keys = [.. keyToCandidate.Keys];
|
||||
result.Candidates = result.Keys.Select(key => keyToCandidate[key]).ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string DiagnoseSnapshotEvaluationFailure(
|
||||
ShaderResourcePlan plan,
|
||||
ResourceRuntimeInputs inputs,
|
||||
|
|
@ -569,6 +702,72 @@ public static class ResourceMaterializer
|
|||
out failure);
|
||||
}
|
||||
|
||||
private static bool MaterializeWaveIndexedImage(
|
||||
IndirectImageSelector indirect,
|
||||
WaveIndexedImageSelector wave,
|
||||
DescriptorWords heap,
|
||||
ImageResource image,
|
||||
bool r128,
|
||||
ResourceRuntimeInputs inputs,
|
||||
out IndirectImageTable result,
|
||||
out ResourceMaterializationFailure failure)
|
||||
{
|
||||
failure = ResourceMaterializationFailure.Other;
|
||||
result = new IndirectImageTable();
|
||||
if (heap.DwordCount != 2 || inputs.ReadCleanMemory is null || wave.IndexStride == 0)
|
||||
return false;
|
||||
|
||||
var baseAddress = (((ulong)heap.Dwords[1] << 32) | heap.Dwords[0]) & AddressMask;
|
||||
if (!TryReadCleanWord(baseAddress, wave.MaskOffset, inputs, out var activeMask))
|
||||
return false;
|
||||
|
||||
var keys = new SortedSet<uint>();
|
||||
for (uint bit = 0; bit < 32; bit++)
|
||||
{
|
||||
if ((activeMask & (1u << (int)bit)) == 0)
|
||||
continue;
|
||||
var indexOffset = (ulong)wave.IndexTableOffset + (ulong)bit * wave.IndexStride;
|
||||
if (!TryReadCleanWord(baseAddress, indexOffset, inputs, out var key))
|
||||
return false;
|
||||
// VCmpxLeI32 0, key suppresses signed-negative indices before ReadFirstLane.
|
||||
if ((key & 0x8000_0000u) == 0)
|
||||
keys.Add(key);
|
||||
}
|
||||
|
||||
var probed = new List<uint[]>(Math.Max(1, keys.Count));
|
||||
var offsets = new List<uint>(Math.Max(1, keys.Count));
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var candidate = new uint[8];
|
||||
var descriptorOffset = (ulong)indirect.TableOffset + ((ulong)key << 5);
|
||||
for (uint dword = 0; dword < candidate.Length; dword++)
|
||||
{
|
||||
if (!TryReadCleanWord(baseAddress, descriptorOffset + dword * sizeof(uint), inputs, out candidate[dword]))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128) || !ReservedImageBitsClear(candidate))
|
||||
Array.Clear(candidate);
|
||||
probed.Add(candidate);
|
||||
offsets.Add(unchecked(indirect.DynamicOffsetBase + (key << 5)));
|
||||
}
|
||||
|
||||
if (probed.Count == 0)
|
||||
{
|
||||
probed.Add(new uint[8]);
|
||||
offsets.Add(0);
|
||||
}
|
||||
|
||||
return FinishIndirectImage(probed, offsets, out result, out failure);
|
||||
}
|
||||
|
||||
private static bool TryReadCleanWord(ulong baseAddress, ulong offset, ResourceRuntimeInputs inputs, out uint word)
|
||||
{
|
||||
word = 0;
|
||||
return offset <= AddressMask && baseAddress <= AddressMask - offset &&
|
||||
inputs.ReadCleanMemory is not null && inputs.ReadCleanMemory(baseAddress + offset, out word);
|
||||
}
|
||||
|
||||
private static bool FinishIndirectImage(
|
||||
IReadOnlyList<uint[]> probed,
|
||||
IEnumerable<uint> keys,
|
||||
|
|
@ -687,6 +886,11 @@ public static class ResourceMaterializer
|
|||
mappingWordCount = checked(mappingWordCount + 1 + table.Keys.Count * 2);
|
||||
}
|
||||
|
||||
foreach (var table in snapshot.BufferCandidateTables)
|
||||
{
|
||||
mappingWordCount = checked(mappingWordCount + 1 + table.Keys.Count * 2);
|
||||
}
|
||||
|
||||
// Each draw owns these arrays. Only indirect candidates require a larger table.
|
||||
Array.Resize(ref snapshot.Images, imageCount);
|
||||
var mappingCursor = snapshot.FlattenedTable.Length;
|
||||
|
|
@ -794,7 +998,10 @@ public static class ResourceMaterializer
|
|||
{
|
||||
images[index] = image with
|
||||
{
|
||||
NumericClass = baseImage.Atomic ? ImageNumericClass.Uint : ImageNumericClass.Float,
|
||||
// A null-bound atomic keeps its declared numeric class
|
||||
// (Uint or, since float image atomics were added, Float)
|
||||
// instead of assuming every atomic image is Uint.
|
||||
NumericClass = baseImage.Atomic ? baseImage.NumericClass : ImageNumericClass.Float,
|
||||
Dimension = ImageDimension.Dim2D,
|
||||
Cube = false,
|
||||
};
|
||||
|
|
@ -810,7 +1017,14 @@ public static class ResourceMaterializer
|
|||
}
|
||||
|
||||
var format = GuestImageFormat.FormatOf(descriptor);
|
||||
if (baseImage.Atomic && format != GuestImageFormat.Format32Uint)
|
||||
// Integer image atomics (Add/Smax/And/...) only ever operate on a
|
||||
// 32-bit uint UAV. The float image atomics (Fmin/Fmax/Fcmpswap,
|
||||
// MIMG op 0x1D-0x1F) are lowered as a compare-and-swap loop on the
|
||||
// raw bit pattern (Gen5SpirvTranslator) and legitimately target a
|
||||
// 32-bit float-format UAV instead - reject only formats that are
|
||||
// neither.
|
||||
if (baseImage.Atomic && format != GuestImageFormat.Format32Uint &&
|
||||
GuestImageFormat.SampledNumericClass(format) != ImageNumericClass.Float)
|
||||
{
|
||||
return Fail($"atomic image descriptor {index} uses unsupported format {format}");
|
||||
}
|
||||
|
|
@ -827,7 +1041,8 @@ public static class ResourceMaterializer
|
|||
return Fail($"storage image descriptor {index} uses unsupported format {format}");
|
||||
}
|
||||
|
||||
if (rawSintStorage)
|
||||
// Atomics use the uint view; float min/max compare-exchange its bits.
|
||||
if (rawSintStorage || (baseImage.Atomic && numericClass == ImageNumericClass.Float))
|
||||
{
|
||||
numericClass = ImageNumericClass.Uint;
|
||||
}
|
||||
|
|
@ -961,7 +1176,71 @@ public static class ResourceMaterializer
|
|||
}
|
||||
}
|
||||
|
||||
specialization = new ResourceSpecialization { Buffers = buffers, Images = images };
|
||||
// Bounded runtime V# candidates are appended after the plan's buffers; each draw
|
||||
// owns their words and the run-time key mapping sits in the flattened table.
|
||||
var baseBufferCount = buffers.Count;
|
||||
var bufferCursor = snapshot.Buffers.Length;
|
||||
var candidateTables = new List<BufferCandidateTableSpecialization>(snapshot.BufferCandidateTables.Count);
|
||||
foreach (var table in snapshot.BufferCandidateTables)
|
||||
{
|
||||
if (table.Descriptors.Count == 0 || table.Keys.Count == 0)
|
||||
{
|
||||
return Fail("buffer candidate table has no candidates");
|
||||
}
|
||||
|
||||
Array.Resize(ref snapshot.Buffers, bufferCursor + table.Descriptors.Count);
|
||||
for (var candidate = 0; candidate < table.Descriptors.Count; candidate++)
|
||||
{
|
||||
var words = table.Descriptors[candidate].Dwords;
|
||||
if ((words[3] >> 30) != 0)
|
||||
{
|
||||
Array.Clear(words);
|
||||
}
|
||||
|
||||
snapshot.Buffers[bufferCursor + candidate] = words;
|
||||
var stride = (words[1] >> 16) & 0x3FFF;
|
||||
var swizzleEnabled = (words[1] >> 31) != 0;
|
||||
var indexStride = (words[3] >> 21) & 0x3;
|
||||
var addThreadId = (words[3] >> 23) & 0x1;
|
||||
var packedStride = stride | ((swizzleEnabled ? 1u : 0u) << 14) | (indexStride << 16) | (addThreadId << 20);
|
||||
var swizzle = stride != 0 && ((packedStride >> 14) & 1) != 0;
|
||||
if (stride == 0)
|
||||
{
|
||||
packedStride &= ~((1u << 14) | (3u << 16));
|
||||
}
|
||||
else if (!swizzle)
|
||||
{
|
||||
packedStride &= ~(3u << 16);
|
||||
}
|
||||
|
||||
buffers.Add(new BufferSpecialization(packedStride, (words[3] >> 12) & 0x7F, words[3] & 0xFFF));
|
||||
}
|
||||
|
||||
var mappingOffset = (uint)mappingCursor;
|
||||
var order = Enumerable.Range(0, table.Keys.Count).OrderBy(index => table.Keys[index]).ToArray();
|
||||
snapshot.FlattenedTable[(int)mappingOffset] = (uint)table.Keys.Count;
|
||||
for (var entry = 0; entry < order.Length; entry++)
|
||||
{
|
||||
var source = order[entry];
|
||||
var offset = (int)mappingOffset + 1 + entry * 2;
|
||||
snapshot.FlattenedTable[offset] = table.Keys[source];
|
||||
snapshot.FlattenedTable[offset + 1] = table.Candidates[source];
|
||||
}
|
||||
|
||||
mappingCursor += 1 + table.Keys.Count * 2;
|
||||
candidateTables.Add(new BufferCandidateTableSpecialization(
|
||||
(uint)bufferCursor, (uint)table.Descriptors.Count, mappingOffset,
|
||||
(uint)BitOperations.Log2((uint)table.Keys.Count) + 1));
|
||||
bufferCursor += table.Descriptors.Count;
|
||||
}
|
||||
|
||||
specialization = new ResourceSpecialization
|
||||
{
|
||||
BaseBufferCount = baseBufferCount,
|
||||
Buffers = buffers,
|
||||
Images = images,
|
||||
BufferCandidateTables = candidateTables,
|
||||
};
|
||||
specializedSnapshot = snapshot;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1026,11 +1305,11 @@ public static class ResourceMaterializer
|
|||
public static SpecializedResourceInfo ApplyTo(ShaderResourcePlan plan, ResourceSpecialization specialization)
|
||||
{
|
||||
var source = plan.Info;
|
||||
if (source.Buffers.Count != specialization.Buffers.Count || source.Images.Count > specialization.Images.Count)
|
||||
if (source.Buffers.Count != specialization.BaseBufferCount || source.Images.Count > specialization.Images.Count)
|
||||
{
|
||||
throw new ResourcePlanException(
|
||||
$"shader resource specialization does not match the plan: hash=0x{plan.Hash:X16} stage={plan.Stage} " +
|
||||
$"buffers={source.Buffers.Count}/{specialization.Buffers.Count} images={source.Images.Count}/{specialization.Images.Count}");
|
||||
$"buffers={source.Buffers.Count}/{specialization.BaseBufferCount} images={source.Images.Count}/{specialization.Images.Count}");
|
||||
}
|
||||
|
||||
var info = source.Clone();
|
||||
|
|
@ -1041,6 +1320,32 @@ public static class ResourceMaterializer
|
|||
info.Buffers[index].DescriptorSwizzle = specialization.Buffers[index].DescriptorSwizzle;
|
||||
}
|
||||
|
||||
// Candidate buffers follow the plan's buffers, one native binding each.
|
||||
for (var index = source.Buffers.Count; index < specialization.Buffers.Count; index++)
|
||||
{
|
||||
var specialized = specialization.Buffers[index];
|
||||
info.Buffers.Add(new BufferResource
|
||||
{
|
||||
Source = DescriptorConstants.NoIndex,
|
||||
Read = true,
|
||||
PackedStride = specialized.PackedStride,
|
||||
DescriptorFormat = specialized.DescriptorFormat,
|
||||
DescriptorSwizzle = specialized.DescriptorSwizzle,
|
||||
});
|
||||
}
|
||||
|
||||
for (var index = 0; index < specialization.BufferCandidateTables.Count; index++)
|
||||
{
|
||||
var table = specialization.BufferCandidateTables[index];
|
||||
info.BufferCandidateTables.Add(new BufferCandidateTableInfo
|
||||
{
|
||||
FirstCandidate = table.FirstCandidate,
|
||||
CandidateCount = table.CandidateCount,
|
||||
MappingOffset = table.MappingOffset,
|
||||
SearchIterations = table.SearchIterations,
|
||||
});
|
||||
}
|
||||
|
||||
for (var index = 0; index < specialization.Images.Count; index++)
|
||||
{
|
||||
var specialized = specialization.Images[index];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@ namespace SharpEmu.ShaderCompiler.Resources;
|
|||
|
||||
public readonly record struct BufferSpecialization(uint PackedStride, uint DescriptorFormat, uint DescriptorSwizzle);
|
||||
|
||||
// The module-affecting layout of one bounded runtime V# table. Candidate descriptors
|
||||
// themselves are appended to Buffers; this names their contiguous run and the flattened
|
||||
// key mapping, so the switch body does not have to embed per-draw addresses.
|
||||
public readonly record struct BufferCandidateTableSpecialization(
|
||||
uint FirstCandidate,
|
||||
uint CandidateCount,
|
||||
uint MappingOffset,
|
||||
uint SearchIterations);
|
||||
|
||||
public readonly record struct ImageSpecialization(
|
||||
ImageNumericClass NumericClass,
|
||||
ImageDimension Dimension,
|
||||
|
|
@ -24,17 +33,22 @@ public readonly record struct ImageSpecialization(
|
|||
// stay in the snapshot, so they never create a permutation.
|
||||
public sealed class ResourceSpecialization : IEquatable<ResourceSpecialization>
|
||||
{
|
||||
// The plan's base buffer count; Buffers beyond it are materialized candidates.
|
||||
public int BaseBufferCount { get; init; }
|
||||
public List<BufferSpecialization> Buffers { get; init; } = [];
|
||||
public List<ImageSpecialization> Images { get; init; } = [];
|
||||
public List<BufferCandidateTableSpecialization> BufferCandidateTables { get; init; } = [];
|
||||
|
||||
public bool Equals(ResourceSpecialization? other) =>
|
||||
other is not null && Buffers.SequenceEqual(other.Buffers) && Images.SequenceEqual(other.Images);
|
||||
other is not null && BaseBufferCount == other.BaseBufferCount && Buffers.SequenceEqual(other.Buffers) &&
|
||||
Images.SequenceEqual(other.Images) && BufferCandidateTables.SequenceEqual(other.BufferCandidateTables);
|
||||
|
||||
public override bool Equals(object? obj) => Equals(obj as ResourceSpecialization);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
hash.Add(BaseBufferCount);
|
||||
foreach (var buffer in Buffers)
|
||||
{
|
||||
hash.Add(buffer);
|
||||
|
|
@ -45,14 +59,26 @@ public sealed class ResourceSpecialization : IEquatable<ResourceSpecialization>
|
|||
hash.Add(image);
|
||||
}
|
||||
|
||||
foreach (var table in BufferCandidateTables)
|
||||
{
|
||||
hash.Add(table);
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
|
||||
public ResourceSpecialization Clone() => new() { Buffers = [.. Buffers], Images = [.. Images] };
|
||||
public ResourceSpecialization Clone() => new()
|
||||
{
|
||||
BaseBufferCount = BaseBufferCount,
|
||||
Buffers = [.. Buffers],
|
||||
Images = [.. Images],
|
||||
BufferCandidateTables = [.. BufferCandidateTables],
|
||||
};
|
||||
|
||||
// The specialization of a plan before any draw: raw buffers and the tracked image classes.
|
||||
public static ResourceSpecialization Default(ShaderResourceInfo info) => new()
|
||||
{
|
||||
BaseBufferCount = info.Buffers.Count,
|
||||
Buffers = info.Buffers.Select(_ => new BufferSpecialization(0, DescriptorConstants.InvalidFormat, DescriptorConstants.IdentityDestinationSelect)).ToList(),
|
||||
Images = info.Images.Select(image => new ImageSpecialization(
|
||||
image.NumericClass == ImageNumericClass.Unsupported ? (image.Atomic ? ImageNumericClass.Uint : ImageNumericClass.Float) : image.NumericClass,
|
||||
|
|
|
|||
|
|
@ -113,8 +113,11 @@ public sealed partial class ResourceTracker
|
|||
|
||||
var key = scaled.Operands[0];
|
||||
var bound = DenseKeyBound(key);
|
||||
if (bound == 0)
|
||||
var waveIndexed = TryCreateWaveIndexedImageSelector(key, reads);
|
||||
if (bound == 0 && waveIndexed is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var read in reads)
|
||||
if (!UsesOnly(read, [handle]))
|
||||
|
|
@ -135,6 +138,7 @@ public sealed partial class ResourceTracker
|
|||
TableOffset = tableOffset,
|
||||
DynamicOffsetBase = unchecked(tableOffset - tableImmediate),
|
||||
KeyBound = bound,
|
||||
WaveIndexed = waveIndexed,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -174,4 +178,253 @@ public sealed partial class ResourceTracker
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Post-process kernels can select a descriptor per active bit in a scalar mask:
|
||||
// mask -> s_ff1 -> v_mov -> (bit * stride + table) -> global_load ->
|
||||
// v_readfirstlane -> key << 5 -> s_load_dwordx8. The global load itself is
|
||||
// lane-varying, so it deliberately stays undefined in the scalar graph. The host
|
||||
// can nevertheless resolve the same finite key set from the mask and index table.
|
||||
private WaveIndexedImageSelector? TryCreateWaveIndexedImageSelector(
|
||||
ScalarValue key,
|
||||
IReadOnlyList<ScalarValue> reads)
|
||||
{
|
||||
if (key.Kind != ScalarValueKind.FirstLane || key.Operands.Length != 2 || reads.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var instructions = _graph.Program.Instructions;
|
||||
var firstLaneIndex = FindInstructionIndex(instructions, (uint)key.Payload);
|
||||
if (firstLaneIndex < 0 || instructions[firstLaneIndex] is not { Opcode: "VReadfirstlaneB32", Sources.Count: 1, Destinations.Count: 1 } firstLane ||
|
||||
firstLane.Sources[0] is not { Kind: Gen5OperandKind.VectorRegister } vectorSource ||
|
||||
firstLane.Destinations[0] is not { Kind: Gen5OperandKind.ScalarRegister } scalarDestination)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var globalIndex = FindLastDefinition(instructions, firstLaneIndex, vectorSource);
|
||||
if (globalIndex < 0 || instructions[globalIndex].Control is not Gen5GlobalMemoryControl
|
||||
{
|
||||
DwordCount: 1,
|
||||
UsesFlatAddress: false,
|
||||
OffsetBytes: 0,
|
||||
} global || global.DestinationVectorRegister != vectorSource.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var descriptorMemoryIndex = reads[0].MemoryIndex;
|
||||
if (descriptorMemoryIndex < 0 || descriptorMemoryIndex >= _plan.Memory.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var descriptorLoadIndex = FindInstructionIndex(instructions, _plan.Memory[descriptorMemoryIndex].Pc);
|
||||
if (descriptorLoadIndex < 0 || instructions[descriptorLoadIndex] is not
|
||||
{
|
||||
Control: Gen5ScalarMemoryControl
|
||||
{
|
||||
DestinationCount: >= 8,
|
||||
DynamicOffsetRegister: { } dynamicOffsetRegister,
|
||||
},
|
||||
} descriptorLoad || dynamicOffsetRegister != scalarDestination.Value ||
|
||||
descriptorLoad.Sources.Count == 0 || descriptorLoad.Sources[0] != Gen5Operand.Scalar(global.ScalarAddress))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var address = Gen5Operand.Vector(global.VectorAddress);
|
||||
var addressDefinitionIndex = FindLastDefinition(instructions, globalIndex, address);
|
||||
if (addressDefinitionIndex < 0 || !TryGetAddedConstant(instructions[addressDefinitionIndex], out var indexDataOffset))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var heapAddress = Gen5Operand.Scalar(global.ScalarAddress);
|
||||
if (TryGetSelfAddedConstant(instructions[addressDefinitionIndex], address, out _) &&
|
||||
TryGetWaveIndexedStride(instructions, addressDefinitionIndex, address, heapAddress,
|
||||
out var maskRegister, out var bitRegister, out var maskOffset, out var indexStride) &&
|
||||
indexDataOffset != 0 && indexStride != 0 && instructions.Any(instruction => instruction.Opcode == "SBitset0B32" &&
|
||||
instruction.Destinations.Contains(maskRegister) && instruction.Sources.Contains(bitRegister)))
|
||||
{
|
||||
return new(maskOffset, indexDataOffset, indexStride);
|
||||
}
|
||||
|
||||
// Some game kernels reuse the vector address register while preparing the
|
||||
// lane mask. The address arithmetic no longer has a single SSA-looking
|
||||
// chain, but the scalar data dependency is still exact: one clean mask word,
|
||||
// its FF1 result, the matching bit clear, and a scalar multiply that gives
|
||||
// the global index stride. This is the same finite runtime-table model; it
|
||||
// merely avoids making register allocation part of the proof.
|
||||
return TryGetWaveIndexedMaskStride(instructions, addressDefinitionIndex, heapAddress, indexDataOffset);
|
||||
}
|
||||
|
||||
private static WaveIndexedImageSelector? TryGetWaveIndexedMaskStride(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
int before,
|
||||
Gen5Operand heapAddress,
|
||||
uint indexTableOffset)
|
||||
{
|
||||
if (indexTableOffset == 0)
|
||||
return null;
|
||||
|
||||
for (var loadIndex = before - 1; loadIndex >= 0; loadIndex--)
|
||||
{
|
||||
if (instructions[loadIndex] is not
|
||||
{
|
||||
Control: Gen5ScalarMemoryControl { DestinationCount: 1, ImmediateOffsetBytes: >= 0, DynamicOffsetRegister: null } maskControl,
|
||||
Sources.Count: >= 1,
|
||||
Destinations.Count: 1,
|
||||
} maskLoad || maskLoad.Sources[0] != heapAddress ||
|
||||
maskLoad.Destinations[0] is not { Kind: Gen5OperandKind.ScalarRegister } maskRegister)
|
||||
continue;
|
||||
|
||||
for (var scanIndex = loadIndex + 1; scanIndex < before; scanIndex++)
|
||||
{
|
||||
if (instructions[scanIndex] is not { Opcode: "SFF1I32B32", Sources.Count: 1, Destinations.Count: 1 } scan ||
|
||||
scan.Sources[0] != maskRegister || scan.Destinations[0] is not { Kind: Gen5OperandKind.ScalarRegister } bitRegister ||
|
||||
!instructions.Skip(scanIndex + 1).Take(before - scanIndex - 1)
|
||||
.Any(instruction => instruction.Opcode == "SBitset0B32" && instruction.Destinations.Contains(maskRegister) &&
|
||||
instruction.Sources.Contains(bitRegister)))
|
||||
continue;
|
||||
|
||||
for (var multiplyIndex = scanIndex + 1; multiplyIndex < before; multiplyIndex++)
|
||||
{
|
||||
var multiply = instructions[multiplyIndex];
|
||||
if (multiply.Opcode != "SMulI32" || multiply.Sources.Count != 2 || !multiply.Sources.Contains(bitRegister))
|
||||
continue;
|
||||
var strideOperand = multiply.Sources[0] == bitRegister ? multiply.Sources[1] : multiply.Sources[0];
|
||||
if (!TryGetConstant(strideOperand, out var stride) || stride == 0)
|
||||
continue;
|
||||
return new((uint)maskControl.ImmediateOffsetBytes, indexTableOffset, stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryGetWaveIndexedStride(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
int addressAddIndex,
|
||||
Gen5Operand address,
|
||||
Gen5Operand heapAddress,
|
||||
out Gen5Operand maskRegister,
|
||||
out Gen5Operand bitRegister,
|
||||
out uint maskOffset,
|
||||
out uint stride)
|
||||
{
|
||||
maskRegister = default;
|
||||
bitRegister = default;
|
||||
maskOffset = 0;
|
||||
stride = 0;
|
||||
var strideAddIndex = FindLastDefinition(instructions, addressAddIndex, address);
|
||||
if (strideAddIndex < 0 || instructions[strideAddIndex] is not { Opcode: "VLshlAddU32", Sources.Count: 3 } strideAdd ||
|
||||
strideAdd.Sources[0] != address || strideAdd.Sources[2] != address || !TryGetConstant(strideAdd.Sources[1], out var outerShift) || outerShift >= 31)
|
||||
return false;
|
||||
|
||||
var innerShiftIndex = FindLastDefinition(instructions, strideAddIndex, address);
|
||||
if (innerShiftIndex < 0 || instructions[innerShiftIndex] is not { Opcode: "VLshlrevB32", Sources.Count: 2 } innerShift ||
|
||||
!TryGetConstant(innerShift.Sources[0], out var innerAmount) || innerAmount >= 31 ||
|
||||
innerShift.Sources[1] is not { Kind: Gen5OperandKind.VectorRegister } bitVector)
|
||||
return false;
|
||||
|
||||
var moveIndex = FindLastDefinition(instructions, innerShiftIndex, bitVector);
|
||||
if (moveIndex < 0 || instructions[moveIndex] is not { Opcode: "VMovB32", Sources.Count: 1 } move ||
|
||||
move.Sources[0] is not { Kind: Gen5OperandKind.ScalarRegister } bitScalar)
|
||||
return false;
|
||||
|
||||
var bitScanIndex = FindLastDefinition(instructions, moveIndex, bitScalar);
|
||||
if (bitScanIndex < 0 || instructions[bitScanIndex] is not { Opcode: "SFF1I32B32", Sources.Count: 1 } bitScan ||
|
||||
bitScan.Sources[0] is not { Kind: Gen5OperandKind.ScalarRegister } mask)
|
||||
return false;
|
||||
|
||||
var maskLoadIndex = FindLastDefinition(instructions, bitScanIndex, mask);
|
||||
if (maskLoadIndex < 0 || instructions[maskLoadIndex] is not
|
||||
{
|
||||
Control: Gen5ScalarMemoryControl { DestinationCount: 1, ImmediateOffsetBytes: >= 0, DynamicOffsetRegister: null } maskControl,
|
||||
Sources.Count: >= 1,
|
||||
} maskLoad || maskLoad.Sources[0] != heapAddress)
|
||||
return false;
|
||||
|
||||
var combinedStride = (ulong)(1u << (int)innerAmount) * ((1u << (int)outerShift) + 1u);
|
||||
if (combinedStride == 0 || combinedStride > uint.MaxValue)
|
||||
return false;
|
||||
|
||||
maskRegister = mask;
|
||||
bitRegister = bitScalar;
|
||||
maskOffset = (uint)maskControl.ImmediateOffsetBytes;
|
||||
stride = (uint)combinedStride;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindLastDefinition(IReadOnlyList<Gen5ShaderInstruction> instructions, int before, Gen5Operand destination)
|
||||
{
|
||||
for (var index = before - 1; index >= 0; index--)
|
||||
{
|
||||
if (instructions[index].Destinations.Contains(destination))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int FindInstructionIndex(IReadOnlyList<Gen5ShaderInstruction> instructions, uint pc)
|
||||
{
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
if (instructions[index].Pc == pc)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool TryGetSelfAddedConstant(Gen5ShaderInstruction instruction, Gen5Operand destination, out uint constant)
|
||||
{
|
||||
constant = 0;
|
||||
if (instruction.Opcode is not ("VAddI32" or "VAddU32") || instruction.Sources.Count != 2 ||
|
||||
!instruction.Destinations.Contains(destination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var left = instruction.Sources[0];
|
||||
var right = instruction.Sources[1];
|
||||
if (left == destination)
|
||||
{
|
||||
return TryGetConstant(right, out constant);
|
||||
}
|
||||
|
||||
return right == destination && TryGetConstant(left, out constant);
|
||||
}
|
||||
|
||||
private static bool TryGetAddedConstant(Gen5ShaderInstruction instruction, out uint constant)
|
||||
{
|
||||
constant = 0;
|
||||
if (instruction.Opcode is not ("VAddI32" or "VAddU32") || instruction.Sources.Count != 2)
|
||||
return false;
|
||||
return TryGetConstant(instruction.Sources[0], out constant) || TryGetConstant(instruction.Sources[1], out constant);
|
||||
}
|
||||
|
||||
private static bool TryGetConstant(Gen5Operand operand, out uint constant)
|
||||
{
|
||||
if (operand.Kind == Gen5OperandKind.LiteralConstant)
|
||||
{
|
||||
constant = operand.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operand.Kind == Gen5OperandKind.EncodedConstant && Gen5InlineConstants.TryDecode(operand.Value, out constant))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
constant = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public sealed partial class ResourceTracker
|
|||
private readonly List<DescriptorSource> _sources = [];
|
||||
private readonly List<(int Index, uint Resource, uint Sampler, bool HasSampler)> _memoryPatches = [];
|
||||
private readonly List<IndirectImagePlan> _indirectImages = [];
|
||||
private readonly List<BufferCandidateTablePlan> _bufferCandidateTables = [];
|
||||
private readonly Dictionary<ScalarValue, List<ScalarValue>> _uses;
|
||||
private readonly Dictionary<int, List<ScalarValue>> _readsByMemory = [];
|
||||
|
||||
|
|
@ -38,7 +39,8 @@ public sealed partial class ResourceTracker
|
|||
IReadOnlyList<DescriptorSource> Sources,
|
||||
ShaderResourceInfo Info,
|
||||
IReadOnlyList<IndirectImageAccess> IndirectImages,
|
||||
IReadOnlySet<ScalarValue> IndirectReads);
|
||||
IReadOnlySet<ScalarValue> IndirectReads,
|
||||
IReadOnlyList<BufferCandidateTablePlan> BufferCandidateTables);
|
||||
|
||||
private ResourceTracker(ShaderResourcePlan plan)
|
||||
{
|
||||
|
|
@ -134,12 +136,80 @@ public sealed partial class ResourceTracker
|
|||
}
|
||||
}
|
||||
|
||||
return new Result(_sources, _info, indirectAccesses, indirectReads);
|
||||
return new Result(_sources, _info, indirectAccesses, indirectReads, _bufferCandidateTables);
|
||||
}
|
||||
|
||||
private ResourcePlanException Failure(uint pc, string reason) =>
|
||||
new($"shader resource tracking: hash=0x{_plan.Hash:X16} stage={_plan.Stage} pc=0x{pc:X8} {reason}");
|
||||
|
||||
/// <summary>
|
||||
/// Names the instructions that produced the undefined leaves <paramref name="value"/>
|
||||
/// rests on, so a rejected descriptor dword points at an opcode.
|
||||
/// </summary>
|
||||
private string DescribeUndefinedLeaves(ScalarValue value)
|
||||
{
|
||||
var origins = new List<string>();
|
||||
var seen = new HashSet<ScalarValue>();
|
||||
var pending = new Stack<ScalarValue>();
|
||||
pending.Push(value);
|
||||
while (pending.Count != 0 && origins.Count < 8)
|
||||
{
|
||||
var current = pending.Pop();
|
||||
if (!seen.Add(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.Kind == ScalarValueKind.Undefined &&
|
||||
_graph.TryGetUndefinedOrigin(current, out var origin))
|
||||
{
|
||||
var described = $"{origin.Opcode}@0x{origin.Pc:X}";
|
||||
if (!origins.Contains(described))
|
||||
{
|
||||
origins.Add(described);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var operand in current.Operands)
|
||||
{
|
||||
pending.Push(operand);
|
||||
}
|
||||
}
|
||||
|
||||
return origins.Count == 0 ? string.Empty : $" (undefined from: {string.Join(", ", origins)})";
|
||||
}
|
||||
|
||||
private bool HasUndefinedOrigin(ScalarValue value, string opcodePrefix)
|
||||
{
|
||||
var seen = new HashSet<ScalarValue>();
|
||||
var pending = new Stack<ScalarValue>();
|
||||
pending.Push(value);
|
||||
while (pending.Count != 0)
|
||||
{
|
||||
var current = pending.Pop();
|
||||
if (!seen.Add(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.Kind == ScalarValueKind.Undefined &&
|
||||
_graph.TryGetUndefinedOrigin(current, out var origin) &&
|
||||
origin.Opcode.StartsWith(opcodePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var operand in current.Operands)
|
||||
{
|
||||
pending.Push(operand);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- descriptor sources ----
|
||||
|
||||
// Copies a handle's dwords into a source. A sampler that no clamp axis sets to
|
||||
|
|
@ -221,12 +291,20 @@ public sealed partial class ResourceTracker
|
|||
}
|
||||
}
|
||||
|
||||
private bool ValidateSource(DescriptorSource source, out uint badDword)
|
||||
private bool ValidateSource(DescriptorSource source, out uint badDword) => ValidateSource(source, out badDword, out _);
|
||||
|
||||
private bool ValidateSource(DescriptorSource source, out uint badDword, out bool controlDependent)
|
||||
{
|
||||
controlDependent = false;
|
||||
for (badDword = 0; badDword < source.DwordCount; badDword++)
|
||||
{
|
||||
var dword = source.Dwords[badDword];
|
||||
if (dword.Type != ScalarValueType.U32 || !_plan.ValidateRuntimeValue(dword))
|
||||
if (dword.Type != ScalarValueType.U32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_plan.ValidateRuntimeValue(dword, out controlDependent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -261,6 +339,42 @@ public sealed partial class ResourceTracker
|
|||
return (uint)(_sources.Count - 1);
|
||||
}
|
||||
|
||||
// Records a proven bounded SRT candidate table, reusing an equivalent table so several
|
||||
// accesses share one native candidate layout. Returns false when the SRT root itself is
|
||||
// not a valid runtime source, in which case the access stays on its old lowering.
|
||||
private bool InternBufferCandidateTable(BufferCandidateTablePlan table, uint pc, int memoryIndex)
|
||||
{
|
||||
table.SourceSrtResource = InternRuntimeSource(table.SrtHandle, pc);
|
||||
if (table.SourceSrtResource == DescriptorConstants.NoIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var existing = 0; existing < _bufferCandidateTables.Count; existing++)
|
||||
{
|
||||
var current = _bufferCandidateTables[existing];
|
||||
if (!_graph.Equivalent(current.SrtHandle, table.SrtHandle) ||
|
||||
!_graph.Equivalent(current.OffsetExpression, table.OffsetExpression))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_bufferCandidateTables[existing] = current.WithMemoryIndex(memoryIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
_bufferCandidateTables.Add(table);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A fully-resolvable descriptor source for the static SRT root a candidate table reads
|
||||
// through. Its dwords stay runtime values; this only records them for the materialiser.
|
||||
private uint InternRuntimeSource(ScalarValue handle, uint pc)
|
||||
{
|
||||
var source = MakeSource(handle, 4, false, false, pc);
|
||||
return ValidateSource(source, out _) ? InternSource(source) : DescriptorConstants.NoIndex;
|
||||
}
|
||||
|
||||
private bool IsHostBufferHandle(ScalarValue? handle) =>
|
||||
handle is { Kind: ScalarValueKind.BufferHandle, Operands.Length: 4 } &&
|
||||
handle.Operands.All(dword => dword.Type == ScalarValueType.U32 && _plan.ValidateRuntimeValue(dword));
|
||||
|
|
@ -298,7 +412,15 @@ public sealed partial class ResourceTracker
|
|||
return false;
|
||||
}
|
||||
|
||||
private uint GetHandleSource(ScalarValue? handle, ScalarValueKind expected, uint width, uint pc, bool sampler = false, bool sampleAdjust = false)
|
||||
private uint GetHandleSource(
|
||||
ScalarValue? handle,
|
||||
ScalarValueKind expected,
|
||||
uint width,
|
||||
uint pc,
|
||||
bool sampler = false,
|
||||
bool sampleAdjust = false,
|
||||
string? memoryOpcode = null,
|
||||
MemoryAccess memoryAccess = MemoryAccess.Read)
|
||||
{
|
||||
if (handle is null || handle.Kind != expected)
|
||||
{
|
||||
|
|
@ -306,23 +428,248 @@ public sealed partial class ResourceTracker
|
|||
}
|
||||
|
||||
var source = MakeSource(handle, width, sampler, sampleAdjust, pc);
|
||||
if (expected == ScalarValueKind.ImageHandle)
|
||||
var nonContiguousImage = expected == ScalarValueKind.ImageHandle && !IsContiguousScalarBufferRecord(source);
|
||||
|
||||
// Image descriptors loaded straight from a scalar buffer at a dynamically-uniform
|
||||
// offset (e.g. a bindless material heap entry read via S_BUFFER_LOAD, without going
|
||||
// through the explicit TryMakeIndirectImage/TryMakeDirectImage heap-record shapes)
|
||||
// used to be rejected outright here. But RuntimeValueValidator/RuntimeValueEvaluator
|
||||
// already handle ScalarBufferWord dwords generically (the same mechanism buffer
|
||||
// descriptors rely on via MaterializationSources), so let ValidateSource below be the
|
||||
// single source of truth instead of a narrower, ImageHandle-specific blanket ban.
|
||||
var badDword = 0u;
|
||||
var controlDependent = false;
|
||||
if (nonContiguousImage || !ValidateSource(source, out badDword, out controlDependent))
|
||||
{
|
||||
for (uint dword = 0; dword < source.DwordCount; dword++)
|
||||
// A bindless image/sampler descriptor whose dwords resolve through a
|
||||
// control-dependent phi (e.g. a hash-table/linear-probe material lookup, as seen
|
||||
// in Ghost of Yotei) has no single compile-time source: real support needs
|
||||
// GPU-side dynamic descriptor indexing, which this resource tracker doesn't
|
||||
// implement. Rather than fail shader recompilation outright, degrade to a null
|
||||
// descriptor for that one access and let it read as a null/black texture,
|
||||
// mirroring KytyPS5's fallback for the same case (feat/shader-control-dependent-
|
||||
// descriptor). Buffer/sampler-adjacent handles or any other validation failure
|
||||
// still hard-fail, since those aren't safe to silently zero.
|
||||
var dynamicImageFallback = expected is (ScalarValueKind.ImageHandle or ScalarValueKind.SamplerHandle) &&
|
||||
(controlDependent || HasUndefinedOrigin(source.Dwords[badDword], "BufferLoadFormat") ||
|
||||
(nonContiguousImage && source.Dwords.Any(dword => HasUndefinedOrigin(dword, "SAndB32"))));
|
||||
if (dynamicImageFallback)
|
||||
{
|
||||
if (source.Dwords[dword].Kind == ScalarValueKind.ScalarBufferWord)
|
||||
source = new DescriptorSource
|
||||
{
|
||||
throw Failure(pc, $"{expected} dword {dword} is not a valid runtime value");
|
||||
}
|
||||
Dwords = Enumerable.Repeat(_graph.Constant(0u), (int)source.DwordCount).ToArray(),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw Failure(
|
||||
pc,
|
||||
$"{memoryOpcode ?? "memory"} ({memoryAccess}) {expected} dword {badDword} is not a valid runtime value" +
|
||||
DescribeUndefinedLeaves(source.Dwords[badDword]) +
|
||||
$" (value: {DescribeValueShape(source.Dwords[badDword])})");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ValidateSource(source, out var badDword))
|
||||
return InternSource(source);
|
||||
}
|
||||
|
||||
private string DescribeValueShape(ScalarValue value, int depth = 0)
|
||||
{
|
||||
if (depth >= 4)
|
||||
{
|
||||
throw Failure(pc, $"{expected} dword {badDword} is not a valid runtime value");
|
||||
return $"{value.Kind}#{value.Id}";
|
||||
}
|
||||
|
||||
return InternSource(source);
|
||||
if (value.IsConstant)
|
||||
{
|
||||
return value.Type == ScalarValueType.U64
|
||||
? $"0x{value.ConstantU64:X}"
|
||||
: value.Type == ScalarValueType.Bool ? (value.ConstantBool ? "true" : "false") : $"0x{value.ConstantU32:X}";
|
||||
}
|
||||
|
||||
if (value.Kind == ScalarValueKind.Operation)
|
||||
{
|
||||
return $"{value.Operation}#{value.Id}({string.Join(',', value.Operands.Select(operand => DescribeValueShape(operand, depth + 1)))})";
|
||||
}
|
||||
|
||||
if (value.Kind == ScalarValueKind.Undefined && _graph.TryGetUndefinedOrigin(value, out var origin))
|
||||
{
|
||||
return $"Undefined#{value.Id}({origin.Opcode}@0x{origin.Pc:X})";
|
||||
}
|
||||
|
||||
return $"{value.Kind}#{value.Id}";
|
||||
}
|
||||
|
||||
// An image descriptor read from a scalar buffer must be one contiguous record.
|
||||
private bool IsContiguousScalarBufferRecord(DescriptorSource source)
|
||||
{
|
||||
if (!source.Dwords.Any(dword => dword.Kind == ScalarValueKind.ScalarBufferWord))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var first = ScalarReadMemory(source.Dwords[0], out _);
|
||||
for (var dword = 0; dword < source.Dwords.Length; dword++)
|
||||
{
|
||||
var read = source.Dwords[dword];
|
||||
var memory = ScalarReadMemory(read, out _);
|
||||
if (first is null || memory is null ||
|
||||
memory.Offset != first.Offset + (uint)dword * sizeof(uint) ||
|
||||
!_graph.Equivalent(read.Operands[0], source.Dwords[0].Operands[0]) ||
|
||||
!_graph.Equivalent(read.Operands[1], source.Dwords[0].Operands[1]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// A runtime V# is one whose four dwords cannot be resolved at plan time, but
|
||||
// whose non-constant leaves are all reads from guest memory. This covers a
|
||||
// descriptor read straight from a device address and one read from the SRT
|
||||
// buffer at a dynamic offset, including the phi that merges an SRT read
|
||||
// across a loop. A handle that is fully resolvable (constants and flattened
|
||||
// table words only) is not a runtime descriptor and keeps the native binding.
|
||||
private bool IsRuntimeDescriptorHandle(ScalarValue? handle, bool allowComputedWords = false)
|
||||
{
|
||||
if (handle is null || handle.Kind != ScalarValueKind.BufferHandle ||
|
||||
handle.Operands.Length != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Descriptor words loaded by the shader can point at a memory entry that
|
||||
// is intentionally omitted from the host-side table (for example a
|
||||
// vertex-fetch record created after fixed-function lowering). The
|
||||
// device-address lowering does not need to evaluate that word on the
|
||||
// host, so keep the descriptor runtime when the word is still a real
|
||||
// scalar memory read. Other values retain the stricter derivability
|
||||
// check, which keeps divergent first-lane handles rejected.
|
||||
if (!handle.Operands.All(IsRuntimeDescriptorDword))
|
||||
{
|
||||
// Some vertex-fetch shaders combine one scalar address load with
|
||||
// descriptor words produced in the vector path. Those words are
|
||||
// not host-evaluable, but the shader still owns the complete
|
||||
// descriptor in its registers. Once a scalar memory word is part
|
||||
// of the handle, lower the whole descriptor through device
|
||||
// addresses instead of trying to bind a partial host source.
|
||||
return allowComputedWords && handle.Operands.Any(operand => HasRuntimeReadKind(operand, ScalarValueKind.ScalarAddressWord));
|
||||
}
|
||||
|
||||
return handle.Operands.Any(operand => HasRuntimeRead(operand));
|
||||
}
|
||||
|
||||
private bool IsRuntimeDescriptorDword(ScalarValue value) => IsRuntimeDescriptorDword(value, []);
|
||||
|
||||
private bool IsRuntimeDescriptorDword(ScalarValue value, HashSet<ScalarValue> visiting)
|
||||
{
|
||||
if (!visiting.Add(value))
|
||||
{
|
||||
return value.Kind == ScalarValueKind.Phi;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (value.Kind is ScalarValueKind.ScalarAddressWord or ScalarValueKind.ScalarBufferWord)
|
||||
{
|
||||
// The backend consumes this as a device-address word. It may
|
||||
// not have a corresponding host materialization record.
|
||||
return value.MemoryIndex >= 0;
|
||||
}
|
||||
|
||||
return IsRuntimeDerivable(value, visiting);
|
||||
}
|
||||
finally
|
||||
{
|
||||
visiting.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
// True when a value is known at plan time (constant, user data, shader base,
|
||||
// a flattened resource-table word) or is ultimately a guest-memory read.
|
||||
// A phi, select or operation qualifies when every operand does, so a
|
||||
// loop-carried descriptor merge stays a runtime descriptor. A cycle through
|
||||
// a phi is accepted: its non-cyclic inputs are checked on the path that
|
||||
// reached it, the same way the read planner treats cyclic phis.
|
||||
private bool IsRuntimeDerivable(ScalarValue value) => IsRuntimeDerivable(value, []);
|
||||
|
||||
private bool IsRuntimeDerivable(ScalarValue value, HashSet<ScalarValue> visiting)
|
||||
{
|
||||
if (!visiting.Add(value))
|
||||
{
|
||||
return value.Kind == ScalarValueKind.Phi;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (value.Kind)
|
||||
{
|
||||
case ScalarValueKind.Constant:
|
||||
case ScalarValueKind.ResourceTableWord:
|
||||
case ScalarValueKind.UserData:
|
||||
case ScalarValueKind.ShaderBase:
|
||||
case ScalarValueKind.MemoryAperture:
|
||||
return true;
|
||||
case ScalarValueKind.ScalarAddressWord:
|
||||
case ScalarValueKind.ScalarBufferWord:
|
||||
return value.MemoryIndex < _graph.Memory.Count;
|
||||
case ScalarValueKind.Phi:
|
||||
case ScalarValueKind.Select:
|
||||
case ScalarValueKind.Operation:
|
||||
case ScalarValueKind.FirstLane:
|
||||
return value.Operands.All(operand => IsRuntimeDerivable(operand, visiting));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
visiting.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasRuntimeRead(ScalarValue value) => HasRuntimeRead(value, []);
|
||||
|
||||
private static bool HasRuntimeReadKind(ScalarValue value, ScalarValueKind kind) => HasRuntimeReadKind(value, kind, []);
|
||||
|
||||
private static bool HasRuntimeReadKind(ScalarValue value, ScalarValueKind kind, HashSet<ScalarValue> visiting)
|
||||
{
|
||||
if (!visiting.Add(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return value.Kind == kind || value.Operands.Any(operand => HasRuntimeReadKind(operand, kind, visiting));
|
||||
}
|
||||
finally
|
||||
{
|
||||
visiting.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasRuntimeRead(ScalarValue value, HashSet<ScalarValue> visiting)
|
||||
{
|
||||
if (!visiting.Add(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (value.Kind is ScalarValueKind.ScalarAddressWord or ScalarValueKind.ScalarBufferWord)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return value.Operands.Any(operand => HasRuntimeRead(operand, visiting));
|
||||
}
|
||||
finally
|
||||
{
|
||||
visiting.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- dense tables ----
|
||||
|
|
@ -490,6 +837,14 @@ public sealed partial class ResourceTracker
|
|||
var isBuffer = memory.Kind is MemoryResourceKind.Buffer or MemoryResourceKind.ScalarBuffer;
|
||||
var isAddress = memory.Kind is MemoryResourceKind.ScalarAddress or MemoryResourceKind.Flat or MemoryResourceKind.Global or MemoryResourceKind.Scratch;
|
||||
var isImage = memory.Kind == MemoryResourceKind.Image;
|
||||
if (memory.Opcode is "ImageBvhIntersectRay" or "ImageBvh64IntersectRay")
|
||||
{
|
||||
// BVH nodes are read through the device-address page table: traversal
|
||||
// reaches bottom-level trees whose bases only exist in guest memory.
|
||||
_info.UsesDeviceAddresses = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBuffer && !isAddress && !isImage)
|
||||
{
|
||||
return;
|
||||
|
|
@ -497,6 +852,12 @@ public sealed partial class ResourceTracker
|
|||
|
||||
if (access is null)
|
||||
{
|
||||
if (HasIndirectPcTransferBefore(memory.Pc))
|
||||
{
|
||||
memory.PlanningOnly = true;
|
||||
return;
|
||||
}
|
||||
|
||||
throw Failure(memory.Pc, "memory operation has no resource handle");
|
||||
}
|
||||
|
||||
|
|
@ -507,6 +868,85 @@ public sealed partial class ResourceTracker
|
|||
|
||||
if (isBuffer)
|
||||
{
|
||||
// A front GS/HS shader can carry a second code object after
|
||||
// S_SETPC_B64. Until AGC supplies its continuation descriptor,
|
||||
// linear decoding sees that unreachable tail with undefined SGPRs.
|
||||
// Do not turn that dead instruction into a descriptor failure; the
|
||||
// live path has already transferred control through the PC pair.
|
||||
if (access.Handle is { Kind: ScalarValueKind.BufferHandle } unreachableHandle &&
|
||||
unreachableHandle.Operands.Any(value => value.IsUndefined) &&
|
||||
HasIndirectPcTransferBefore(memory.Pc))
|
||||
{
|
||||
memory.PlanningOnly = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// A buffer access can carry a descriptor assembled in vector lanes. Its
|
||||
// record count/stride is not a host-evaluable scalar (the A6FE Little
|
||||
// Nightmares path is one example), but the Vulkan physical-storage-buffer
|
||||
// lowering can read the four descriptor words directly from shader
|
||||
// registers. Keep this narrow to vector-derived accesses; ordinary
|
||||
// malformed handles must still fail validation below.
|
||||
if (memory.Access is MemoryAccess.Read or MemoryAccess.Write &&
|
||||
access.Handle is { Kind: ScalarValueKind.BufferHandle } writeHandle)
|
||||
{
|
||||
var writeSource = MakeSource(writeHandle, 4, false, false, memory.Pc);
|
||||
if (writeSource.Dwords.Any(dword =>
|
||||
HasUndefinedOrigin(dword, "VAdd3U32") ||
|
||||
HasUndefinedOrigin(dword, "VCvtU32F32") ||
|
||||
HasUndefinedOrigin(dword, "VRcpIflagF32")))
|
||||
{
|
||||
memory.BufferDescriptor = new GuestBufferDescriptor
|
||||
{
|
||||
Provenance = BufferDescriptorProvenance.Runtime,
|
||||
};
|
||||
_info.UsesDeviceAddresses = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// A V# whose dwords are all read from a runtime scalar-memory address
|
||||
// (e.g. a descriptor-array entry indexed by a loop counter) cannot be
|
||||
// bound ahead of time. Record it as a runtime guest descriptor so the
|
||||
// backend can choose a lowering strategy for each access.
|
||||
// A vector access through a V# the draw can evaluate keeps the explicit binding.
|
||||
var allowComputedFormatDescriptor = memory.Formatted &&
|
||||
memory.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal);
|
||||
if (IsRuntimeDescriptorHandle(access.Handle, allowComputedFormatDescriptor) &&
|
||||
(memory.Kind != MemoryResourceKind.Buffer ||
|
||||
!ValidateSource(MakeSource(access.Handle!, 4, false, false, memory.Pc), out _)))
|
||||
{
|
||||
// A formatted vector access can enumerate its bounded candidates; a scalar
|
||||
// buffer read only needs the descriptor's base, which its SGPRs already
|
||||
// hold, so it is addressed through the device-address page table.
|
||||
if (memory.Kind == MemoryResourceKind.Buffer &&
|
||||
BufferCandidateTablePlanner.TryPlan(_plan, access.Handle!, index, out var table) &&
|
||||
InternBufferCandidateTable(table, memory.Pc, index))
|
||||
{
|
||||
memory.BufferDescriptor = new GuestBufferDescriptor
|
||||
{
|
||||
Provenance = BufferDescriptorProvenance.Runtime,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Without a bounded candidate table, a V# loaded from scalar-buffer
|
||||
// data is read through its descriptor registers on the device.
|
||||
if (memory.Kind == MemoryResourceKind.Buffer && IsDeviceLoadedBufferHandle(access.Handle))
|
||||
{
|
||||
memory.DeviceDescriptor = true;
|
||||
_info.UsesDeviceAddresses = true;
|
||||
return;
|
||||
}
|
||||
|
||||
memory.BufferDescriptor = new GuestBufferDescriptor
|
||||
{
|
||||
Provenance = BufferDescriptorProvenance.Runtime,
|
||||
};
|
||||
_info.UsesDeviceAddresses = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Scalar loads can address buffers that have no host descriptor binding, while
|
||||
// vector buffer descriptors loaded from scalar-buffer data must stay device-side.
|
||||
if ((memory.Kind == MemoryResourceKind.ScalarBuffer && !IsHostBufferHandle(access.Handle)) ||
|
||||
|
|
@ -517,13 +957,35 @@ public sealed partial class ResourceTracker
|
|||
return;
|
||||
}
|
||||
|
||||
var source = GetHandleSource(access.Handle, ScalarValueKind.BufferHandle, 4, memory.Pc);
|
||||
var source = GetHandleSource(
|
||||
access.Handle,
|
||||
ScalarValueKind.BufferHandle,
|
||||
4,
|
||||
memory.Pc,
|
||||
memoryOpcode: memory.Opcode,
|
||||
memoryAccess: memory.Access);
|
||||
var resource = AddBuffer(source, memory, memory.Pc);
|
||||
if (resource == DescriptorConstants.NoIndex)
|
||||
{
|
||||
throw Failure(memory.Pc, "buffer resource limit exceeded");
|
||||
// Past the binding budget the access reads its V# from its SGPRs when it
|
||||
// runs, as the hardware does, and goes through the device-address table.
|
||||
// That path has no typed (tbuffer) or formatted store lowering.
|
||||
if (memory.Opcode.StartsWith("TBuffer", StringComparison.Ordinal) ||
|
||||
(memory.Formatted && memory.Access != MemoryAccess.Read))
|
||||
{
|
||||
throw Failure(memory.Pc, "buffer resource limit exceeded");
|
||||
}
|
||||
|
||||
memory.DeviceDescriptor = true;
|
||||
_info.UsesDeviceAddresses = true;
|
||||
return;
|
||||
}
|
||||
|
||||
memory.BufferDescriptor = new GuestBufferDescriptor
|
||||
{
|
||||
Provenance = BufferDescriptorProvenance.Static,
|
||||
StaticResource = resource,
|
||||
};
|
||||
AddMemoryPatch(index, resource, 0, false, memory.Pc);
|
||||
return;
|
||||
}
|
||||
|
|
@ -551,6 +1013,22 @@ public sealed partial class ResourceTracker
|
|||
return;
|
||||
}
|
||||
|
||||
// A fused shader resumes through S_SETPC_B64. The continuation can
|
||||
// legitimately consume the image descriptor carried in the incoming
|
||||
// SGPR state, while the standalone resource graph only sees the first
|
||||
// code object and therefore has no provenance for those dwords. Do
|
||||
// not reject the whole program for that cross-object bookkeeping gap;
|
||||
// leave the access unplanned so the continuation path can supply its
|
||||
// descriptor at runtime.
|
||||
if (isImage &&
|
||||
access.Handle is { Kind: ScalarValueKind.ImageHandle } continuationImage &&
|
||||
continuationImage.Operands.Any(value => value.IsUndefined) &&
|
||||
HasIndirectPcTransferBefore(memory.Pc))
|
||||
{
|
||||
memory.PlanningOnly = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (memory.ImageClass == ImageResourceClass.None)
|
||||
{
|
||||
throw Failure(memory.Pc, "image operation has invalid resource kind");
|
||||
|
|
@ -633,6 +1111,35 @@ public sealed partial class ResourceTracker
|
|||
private bool IsIndirectPlanningMemory(int index) =>
|
||||
_indirectImages.Any(plan => plan.SuppressMemoryReads && plan.Memory.Contains(index));
|
||||
|
||||
private bool HasIndirectPcTransferBefore(uint pc)
|
||||
{
|
||||
var previousEnd = 0u;
|
||||
foreach (var instruction in _graph.Program.Instructions)
|
||||
{
|
||||
if (instruction.Pc >= pc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (instruction.Opcode is "SSetpcB64" or "SRfeB64")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fused shaders replace the S_SETPC marker with a NOP and append
|
||||
// the continuation at its real address. The PC discontinuity is
|
||||
// the remaining reliable boundary when the marker is gone.
|
||||
if (previousEnd != 0 && instruction.Pc > previousEnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
previousEnd = instruction.Pc + (uint)(instruction.Words.Count * sizeof(uint));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void PlanIndirectImages()
|
||||
{
|
||||
for (var index = 0; index < _plan.Memory.Count; index++)
|
||||
|
|
@ -662,7 +1169,8 @@ public sealed partial class ResourceTracker
|
|||
}
|
||||
|
||||
var memory = _plan.Memory[index];
|
||||
return memory.Kind == MemoryResourceKind.ScalarBuffer && memory.DataBits == 32 && memory.DataDwords == 1 ? memory : null;
|
||||
return memory.Kind is MemoryResourceKind.ScalarBuffer or MemoryResourceKind.Buffer &&
|
||||
memory.DataBits == 32 && memory.DataDwords == 1 ? memory : null;
|
||||
}
|
||||
|
||||
private bool MemoryIndexBelongsTo(int index, ScalarValue owner) =>
|
||||
|
|
|
|||
|
|
@ -116,6 +116,9 @@ public sealed class RuntimeValueEvaluator
|
|||
{
|
||||
case ScalarValueKind.Undefined:
|
||||
return false;
|
||||
case ScalarValueKind.MemoryAperture:
|
||||
result = Gen5InlineConstants.DecodeAperture64((uint)value.Payload) >> 32;
|
||||
return true;
|
||||
case ScalarValueKind.UserData:
|
||||
{
|
||||
var register = value.UserDataRegister;
|
||||
|
|
@ -235,7 +238,9 @@ public sealed class RuntimeValueEvaluator
|
|||
var size = stride == 0 ? (ulong)(uint)records : (ulong)stride * (uint)records;
|
||||
if (aligned > size || size - aligned < sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
// An unbound (empty) V# reads as zero; overrunning a bound buffer stays a failure.
|
||||
result = 0;
|
||||
return (uint)records == 0;
|
||||
}
|
||||
|
||||
address = ((baseAddress & ~3ul) + byteOffset) & ~3ul;
|
||||
|
|
@ -354,7 +359,10 @@ public sealed class RuntimeValueEvaluator
|
|||
{
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
if (!evaluator.Evaluate(source.Dwords[index], out words[index])) return false;
|
||||
if (!evaluator.Evaluate(source.Dwords[index], out words[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ public sealed class RuntimeValueValidator
|
|||
private readonly ScalarValue? _activeMask;
|
||||
private readonly HashSet<ScalarValue> _visiting = [];
|
||||
|
||||
// Set when a failure traces back to a phi that isn't loop-invariant (e.g. a
|
||||
// hash-table/linear-probe bindless lookup): the value genuinely has no single
|
||||
// compile-time source, as opposed to using an operation or memory shape we just
|
||||
// don't recognize. Callers use this to decide whether degrading to a null
|
||||
// descriptor is reasonable instead of failing outright (KytyPS5 does the same
|
||||
// for image/sampler handles specifically).
|
||||
public bool ControlDependent { get; private set; }
|
||||
|
||||
public RuntimeValueValidator(ScalarValueGraph graph, uint userDataBase, uint userDataCount, int tableReadCount, ScalarValue? activeMask = null)
|
||||
{
|
||||
_graph = graph;
|
||||
|
|
@ -58,7 +66,8 @@ public sealed class RuntimeValueValidator
|
|||
|
||||
try
|
||||
{
|
||||
return ValidateNode(value);
|
||||
var result = ValidateNode(value);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -81,15 +90,35 @@ public sealed class RuntimeValueValidator
|
|||
case ScalarValueKind.UserData:
|
||||
return value.UserDataRegister >= _userDataBase && value.UserDataRegister - _userDataBase < _userDataCount;
|
||||
case ScalarValueKind.ShaderBase:
|
||||
case ScalarValueKind.MemoryAperture:
|
||||
return true;
|
||||
case ScalarValueKind.Phi:
|
||||
{
|
||||
var invariant = _graph.ResolveInvariantPhi(value);
|
||||
return invariant is not null && Validate(invariant);
|
||||
if (invariant is null)
|
||||
{
|
||||
ControlDependent = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return Validate(invariant);
|
||||
}
|
||||
case ScalarValueKind.FirstLane:
|
||||
return value.Operands.Length == 2 &&
|
||||
new RuntimeValueValidator(_graph, _userDataBase, _userDataCount, _tableReadCount, value.Operands[1]).Validate(value.Operands[0]);
|
||||
{
|
||||
if (value.Operands.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nested = new RuntimeValueValidator(_graph, _userDataBase, _userDataCount, _tableReadCount, value.Operands[1]);
|
||||
var ok = nested.Validate(value.Operands[0]);
|
||||
if (nested.ControlDependent)
|
||||
{
|
||||
ControlDependent = true;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
case ScalarValueKind.ResourceTableWord:
|
||||
return value.Payload < (ulong)_tableReadCount;
|
||||
case ScalarValueKind.ScalarAddressWord:
|
||||
|
|
@ -168,6 +197,6 @@ public sealed class RuntimeValueValidator
|
|||
|
||||
var kind = _graph.Memory[value.MemoryIndex].Kind;
|
||||
return (value.Kind == ScalarValueKind.ScalarAddressWord && kind == MemoryResourceKind.ScalarAddress) ||
|
||||
(value.Kind == ScalarValueKind.ScalarBufferWord && kind == MemoryResourceKind.ScalarBuffer);
|
||||
(value.Kind == ScalarValueKind.ScalarBufferWord && kind is MemoryResourceKind.ScalarBuffer or MemoryResourceKind.Buffer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ public enum ScalarValueKind : byte
|
|||
ScalarAddressWord,
|
||||
ScalarBufferWord,
|
||||
ResourceTableWord,
|
||||
// The high dword of a GFX10 LDS/scratch aperture (payload: the inline operand).
|
||||
MemoryAperture,
|
||||
}
|
||||
|
||||
// Operations a value node can apply to its operands. The validator accepts only the
|
||||
|
|
@ -154,6 +156,9 @@ public sealed class ScalarValue
|
|||
public static ScalarValue ConstantOf(bool value) =>
|
||||
new(ScalarValueKind.Constant, ScalarValueType.Bool, ScalarOperation.None, []) { Payload = value ? 1u : 0u };
|
||||
|
||||
public static ScalarValue MemoryAperture(uint operand) =>
|
||||
new(ScalarValueKind.MemoryAperture, ScalarValueType.U32, ScalarOperation.None, []) { Payload = operand };
|
||||
|
||||
public static ScalarValue Undefined(ScalarValueType type) =>
|
||||
new(ScalarValueKind.Undefined, type, ScalarOperation.None, []);
|
||||
|
||||
|
|
@ -197,6 +202,7 @@ public sealed class ScalarValue
|
|||
{
|
||||
ScalarValueKind.Constant => Type == ScalarValueType.U64 ? $"0x{Payload:X}ul" : Type == ScalarValueType.Bool ? (Payload != 0 ? "true" : "false") : $"0x{(uint)Payload:X}",
|
||||
ScalarValueKind.UserData => $"UserData(s{Payload})",
|
||||
ScalarValueKind.MemoryAperture => $"Aperture({Payload})",
|
||||
ScalarValueKind.Operation => $"{Operation}({string.Join(", ", Operands.Select(operand => operand.ToString()))})",
|
||||
ScalarValueKind.Phi => $"Phi#{Id}(block {PhiBlock})",
|
||||
_ => $"{Kind}#{Id}",
|
||||
|
|
@ -232,7 +238,7 @@ public static class ScalarValueEquivalence
|
|||
return false;
|
||||
}
|
||||
|
||||
if (left.Kind is ScalarValueKind.Constant or ScalarValueKind.UserData or ScalarValueKind.ResourceTableWord)
|
||||
if (left.Kind is ScalarValueKind.Constant or ScalarValueKind.UserData or ScalarValueKind.ResourceTableWord or ScalarValueKind.MemoryAperture)
|
||||
{
|
||||
return left.Payload == right.Payload;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,6 +297,8 @@ public sealed partial class ScalarValueGraph
|
|||
|
||||
private void Apply(Gen5ShaderInstruction instruction, RegisterState state)
|
||||
{
|
||||
// Lets ScalarValueGraph.Undefined record which instruction gave up.
|
||||
_graph.BuilderInstruction = (instruction.Pc, instruction.Opcode);
|
||||
switch (instruction.Encoding)
|
||||
{
|
||||
case Gen5ShaderEncoding.Sop1:
|
||||
|
|
@ -357,6 +359,13 @@ public sealed partial class ScalarValueGraph
|
|||
case "SMovB32":
|
||||
state.WriteScalar(destinationRegister, Read(instruction.Sources[0], state));
|
||||
return;
|
||||
case "SBitreplicateB64B32":
|
||||
{
|
||||
var replicated = Read(instruction.Sources[0], state);
|
||||
state.WriteScalar(destinationRegister, replicated);
|
||||
state.WriteScalar(destinationRegister + 1, replicated);
|
||||
return;
|
||||
}
|
||||
case "SMovkI32":
|
||||
state.WriteScalar(destinationRegister, _graph.Constant(unchecked((uint)(short)instruction.Sources[0].Value)));
|
||||
return;
|
||||
|
|
@ -1029,9 +1038,12 @@ public sealed partial class ScalarValueGraph
|
|||
return;
|
||||
}
|
||||
|
||||
var hasModifiers = instruction.Control is Gen5Vop3Control { AbsoluteMask: not 0 } or Gen5Vop3Control { NegateMask: not 0 } or
|
||||
Gen5Vop3Control { Clamp: true } or Gen5Vop3Control { OutputModifier: not 0 } or Gen5Vop3Control { OperandSelect: not 0 } or
|
||||
Gen5SdwaControl or Gen5DppControl or Gen5Dpp8Control or Gen5Vop3pControl;
|
||||
var hasModifiers = (instruction.Control is Gen5Vop3Control { AbsoluteMask: not 0 } or Gen5Vop3Control { NegateMask: not 0 } or
|
||||
Gen5Vop3Control { Clamp: true } or Gen5Vop3Control { OutputModifier: not 0 } or Gen5Vop3Control { OperandSelect: not 0 }) ||
|
||||
(instruction.Control is Gen5SdwaControl sdwa &&
|
||||
(sdwa.AbsoluteMask != 0 || sdwa.NegateMask != 0 || sdwa.OutputModifier != 0 || sdwa.Clamp ||
|
||||
sdwa.DestinationSelect == 7 || sdwa.Source0Select == 7 || sdwa.Source1Select == 7 || sdwa.DestinationUnused == 3)) ||
|
||||
instruction.Control is Gen5DppControl or Gen5Dpp8Control or Gen5Vop3pControl;
|
||||
var value = hasModifiers ? _graph.Undefined(ScalarValueType.U32) : VectorResult(instruction, state);
|
||||
if (instruction.Control is Gen5Vop3Control { ScalarDestination: { } carryDestination } && !hasModifiers &&
|
||||
opcode is "VAddCoU32" or "VSubCoU32" or "VSubrevCoU32" or "VAddCoCiU32" or "VSubCoCiU32" or "VSubrevCoCiU32" or "VMadU64U32")
|
||||
|
|
@ -1049,7 +1061,10 @@ public sealed partial class ScalarValueGraph
|
|||
{
|
||||
if (destination.Kind == Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
state.WriteVector(destination.Value, value);
|
||||
var result = instruction.Control is Gen5SdwaControl destinationSdwa
|
||||
? ApplySdwaDestination(destinationSdwa, value, state.ReadVector(destination.Value))
|
||||
: value;
|
||||
state.WriteVector(destination.Value, result);
|
||||
// Multi-dword results are not modelled; the second dword is undefined.
|
||||
value = _graph.Undefined(ScalarValueType.U32);
|
||||
}
|
||||
|
|
@ -1064,7 +1079,9 @@ public sealed partial class ScalarValueGraph
|
|||
{
|
||||
var opcode = instruction.Opcode;
|
||||
var sources = instruction.Sources;
|
||||
ScalarValue Source(int index) => index < sources.Count ? ReadVectorOperand(sources[index], state) : _graph.Undefined(ScalarValueType.U32);
|
||||
ScalarValue Source(int index) => index < sources.Count
|
||||
? ApplySdwaSource(instruction.Control as Gen5SdwaControl, index, ReadVectorOperand(sources[index], state))
|
||||
: _graph.Undefined(ScalarValueType.U32);
|
||||
ScalarValue Shift(ScalarValue value, ScalarValue count, ScalarOperation operation) =>
|
||||
Binary(operation, value, Binary(ScalarOperation.And32, count, _graph.Constant(31u)));
|
||||
ScalarValue Low24(ScalarValue value) =>
|
||||
|
|
@ -1105,6 +1122,26 @@ public sealed partial class ScalarValueGraph
|
|||
state.CarryOut = Bool(ScalarOperation.LogicalAnd, state.Exec, NotZero(Binary(ScalarOperation.Or32, Extract(first, 1), Extract(second, 1))));
|
||||
return Extract(second, 0);
|
||||
}
|
||||
case "VSubCoCiU32":
|
||||
case "VSubrevCoCiU32":
|
||||
{
|
||||
var left = opcode == "VSubCoCiU32" ? Source(0) : Source(1);
|
||||
var right = opcode == "VSubCoCiU32" ? Source(1) : Source(0);
|
||||
var borrowIn = _graph.Select(
|
||||
instruction.Sources.Count > 2 ? MaskOf(sources[2], state) : state.Vcc,
|
||||
_graph.Constant(1u),
|
||||
_graph.Constant(0u));
|
||||
var partial = Binary(ScalarOperation.ISub32, left, right);
|
||||
var result = Binary(ScalarOperation.ISub32, partial, borrowIn);
|
||||
state.CarryOut = Bool(
|
||||
ScalarOperation.LogicalAnd,
|
||||
state.Exec,
|
||||
Bool(
|
||||
ScalarOperation.LogicalOr,
|
||||
Bool(ScalarOperation.UGreaterThan32, right, left),
|
||||
Bool(ScalarOperation.UGreaterThan32, borrowIn, partial)));
|
||||
return result;
|
||||
}
|
||||
case "VMulLoU32":
|
||||
case "VMulLoI32":
|
||||
return Binary(ScalarOperation.IMul32, Source(0), Source(1));
|
||||
|
|
@ -1165,6 +1202,15 @@ public sealed partial class ScalarValueGraph
|
|||
return Binary(ScalarOperation.UMax32, Source(0), Source(1));
|
||||
case "VMinI32":
|
||||
return Binary(ScalarOperation.SMin32, Source(0), Source(1));
|
||||
case "VMed3U32":
|
||||
// median(a, b, c) = max(min(a, b), min(max(a, b), c))
|
||||
return Binary(ScalarOperation.UMax32,
|
||||
Binary(ScalarOperation.UMin32, Source(0), Source(1)),
|
||||
Binary(ScalarOperation.UMin32, Binary(ScalarOperation.UMax32, Source(0), Source(1)), Source(2)));
|
||||
case "VMed3I32":
|
||||
return Binary(ScalarOperation.SMax32,
|
||||
Binary(ScalarOperation.SMin32, Source(0), Source(1)),
|
||||
Binary(ScalarOperation.SMin32, Binary(ScalarOperation.SMax32, Source(0), Source(1)), Source(2)));
|
||||
case "VMaxI32":
|
||||
return Binary(ScalarOperation.SMax32, Source(0), Source(1));
|
||||
case "VBfeU32":
|
||||
|
|
@ -1238,6 +1284,68 @@ public sealed partial class ScalarValueGraph
|
|||
}
|
||||
}
|
||||
|
||||
private ScalarValue ApplySdwaSource(Gen5SdwaControl? control, int sourceIndex, ScalarValue value)
|
||||
{
|
||||
if (control is null || sourceIndex > 1)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var selector = sourceIndex == 0 ? control.Source0Select : control.Source1Select;
|
||||
if (selector == 6)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (selector > 5)
|
||||
{
|
||||
return _graph.Undefined(ScalarValueType.U32);
|
||||
}
|
||||
|
||||
var width = selector <= 3 ? 8u : 16u;
|
||||
var shift = selector <= 3 ? selector * 8u : (selector - 4u) * 16u;
|
||||
var extracted = Binary(ScalarOperation.And32,
|
||||
Binary(ScalarOperation.ShiftRightLogical32, value, _graph.Constant(shift)),
|
||||
_graph.Constant(width == 8 ? 0xFFu : 0xFFFFu));
|
||||
|
||||
return (sourceIndex == 0 ? control.Source0SignExtend : control.Source1SignExtend)
|
||||
? _graph.Operation(ScalarOperation.BitFieldSExtract, ScalarValueType.U32, extracted,
|
||||
_graph.Constant(0u), _graph.Constant(width))
|
||||
: extracted;
|
||||
}
|
||||
|
||||
private ScalarValue ApplySdwaDestination(Gen5SdwaControl control, ScalarValue value, ScalarValue previous)
|
||||
{
|
||||
if (control.DestinationSelect == 6)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (control.DestinationSelect > 5 || control.DestinationUnused == 3)
|
||||
{
|
||||
return _graph.Undefined(ScalarValueType.U32);
|
||||
}
|
||||
|
||||
var width = control.DestinationSelect <= 3 ? 8u : 16u;
|
||||
var shift = control.DestinationSelect <= 3 ? control.DestinationSelect * 8u : (control.DestinationSelect - 4u) * 16u;
|
||||
var lowMask = width == 8 ? 0xFFu : 0xFFFFu;
|
||||
var fieldMask = lowMask << (int)shift;
|
||||
var positioned = Binary(ScalarOperation.ShiftLeft32,
|
||||
Binary(ScalarOperation.And32, value, _graph.Constant(lowMask)), _graph.Constant(shift));
|
||||
|
||||
return control.DestinationUnused switch
|
||||
{
|
||||
0 => positioned,
|
||||
1 => Binary(ScalarOperation.Or32, positioned,
|
||||
_graph.Select(NotZero(Binary(ScalarOperation.And32, positioned,
|
||||
_graph.Constant(1u << (int)(shift + width - 1)))),
|
||||
_graph.Constant(uint.MaxValue << (int)(shift + width)), _graph.Constant(0u))),
|
||||
2 => Binary(ScalarOperation.Or32,
|
||||
Binary(ScalarOperation.And32, previous, _graph.Constant(~fieldMask)), positioned),
|
||||
_ => _graph.Undefined(ScalarValueType.U32),
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyVectorCompare(Gen5ShaderInstruction instruction, RegisterState state)
|
||||
{
|
||||
var opcode = instruction.Opcode;
|
||||
|
|
@ -1245,6 +1353,27 @@ public sealed partial class ScalarValueGraph
|
|||
var right = instruction.Sources.Count > 1 ? ReadVectorOperand(instruction.Sources[1], state) : _graph.Undefined(ScalarValueType.U32);
|
||||
var updatesExecutionMask = opcode.StartsWith("VCmpx", StringComparison.Ordinal);
|
||||
var suffix = opcode[(updatesExecutionMask ? "VCmpx".Length : "VCmp".Length)..];
|
||||
|
||||
// Integer 16-bit compares consume the low word of their operands. Signed forms
|
||||
// sign-extend it before comparison; floating-point F16 compares use their own path.
|
||||
if (suffix.EndsWith("16", StringComparison.Ordinal) && !suffix.EndsWith("F16", StringComparison.Ordinal))
|
||||
{
|
||||
var signed16 = suffix.EndsWith("I16", StringComparison.Ordinal);
|
||||
ScalarValue Narrow16(ScalarValue value)
|
||||
{
|
||||
var low = Binary(ScalarOperation.And32, value, _graph.Constant(0xffffu));
|
||||
return signed16
|
||||
? Binary(
|
||||
ScalarOperation.ShiftRightArithmetic32,
|
||||
Binary(ScalarOperation.ShiftLeft32, low, _graph.Constant(16u)),
|
||||
_graph.Constant(16u))
|
||||
: low;
|
||||
}
|
||||
|
||||
left = Narrow16(left);
|
||||
right = Narrow16(right);
|
||||
}
|
||||
|
||||
var result = instruction.Control is Gen5SdwaControl or Gen5Vop3Control { AbsoluteMask: not 0 } or Gen5Vop3Control { NegateMask: not 0 }
|
||||
? _graph.Undefined(ScalarValueType.Bool)
|
||||
: suffix switch
|
||||
|
|
@ -1259,6 +1388,16 @@ public sealed partial class ScalarValueGraph
|
|||
"GtI32" => Bool(ScalarOperation.SGreaterThan32, left, right),
|
||||
"LeI32" => Bool(ScalarOperation.SLessThanEqual32, left, right),
|
||||
"GeI32" => Bool(ScalarOperation.SGreaterThanEqual32, left, right),
|
||||
"EqU16" or "EqI16" => Bool(ScalarOperation.IEqual32, left, right),
|
||||
"NeU16" or "NeI16" => Bool(ScalarOperation.INotEqual32, left, right),
|
||||
"LtU16" => Bool(ScalarOperation.ULessThan32, left, right),
|
||||
"LeU16" => Bool(ScalarOperation.ULessThanEqual32, left, right),
|
||||
"GtU16" => Bool(ScalarOperation.UGreaterThan32, left, right),
|
||||
"GeU16" => Bool(ScalarOperation.UGreaterThanEqual32, left, right),
|
||||
"LtI16" => Bool(ScalarOperation.SLessThan32, left, right),
|
||||
"LeI16" => Bool(ScalarOperation.SLessThanEqual32, left, right),
|
||||
"GtI16" => Bool(ScalarOperation.SGreaterThan32, left, right),
|
||||
"GeI16" => Bool(ScalarOperation.SGreaterThanEqual32, left, right),
|
||||
"LeF32" => Bool(ScalarOperation.FLessThanEqual, left, right),
|
||||
"GeF32" => Bool(ScalarOperation.FGreaterThanEqual, left, right),
|
||||
"LtF32" => Bool(ScalarOperation.FLessThan, left, right),
|
||||
|
|
@ -1341,9 +1480,10 @@ public sealed partial class ScalarValueGraph
|
|||
|
||||
private void ApplyMemory(Gen5ShaderInstruction instruction, RegisterState state)
|
||||
{
|
||||
ScalarValue? vectorRead = null;
|
||||
if (_recording && _graph.Memory.TryGetIndex(instruction.Pc, 0, out var memoryIndex))
|
||||
{
|
||||
_graph.Accesses[memoryIndex] = instruction.Control switch
|
||||
var binding = instruction.Control switch
|
||||
{
|
||||
Gen5BufferMemoryControl buffer => new MemoryAccessBinding(
|
||||
_graph.Handle(ScalarValueKind.BufferHandle, state.Read(buffer.ScalarResource), state.Read(buffer.ScalarResource + 1),
|
||||
|
|
@ -1360,9 +1500,26 @@ public sealed partial class ScalarValueGraph
|
|||
AddressHandleOf(global, state),
|
||||
null,
|
||||
null,
|
||||
global.UsesFlatAddress ? null : state.ReadVector(global.VectorAddress)),
|
||||
global.UsesFlatAddress ? null : state.ReadVector(global.VectorAddress),
|
||||
state.Exec),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (binding is not null && instruction.Control is Gen5BufferMemoryControl bufferControl &&
|
||||
!instruction.Opcode.Contains("Store", StringComparison.Ordinal) &&
|
||||
instruction.Destinations.FirstOrDefault(destination => destination.Kind == Gen5OperandKind.VectorRegister) is { } vectorDestination)
|
||||
{
|
||||
var handle = binding.Handle!;
|
||||
var offset = state.ReadVector(bufferControl.VectorAddress);
|
||||
// A lane-varying address loads a lane-varying value.
|
||||
if (!offset.IsUndefined)
|
||||
{
|
||||
vectorRead = _graph.MemoryRead(ScalarValueKind.ScalarBufferWord, handle, offset, memoryIndex);
|
||||
binding = binding with { Read = vectorRead };
|
||||
}
|
||||
}
|
||||
|
||||
_graph.Accesses[memoryIndex] = binding;
|
||||
}
|
||||
|
||||
foreach (var destination in instruction.Destinations)
|
||||
|
|
@ -1370,7 +1527,7 @@ public sealed partial class ScalarValueGraph
|
|||
if (destination.Kind == Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
state.ClearLanes(destination.Value);
|
||||
state.WriteVector(destination.Value, _graph.Undefined(ScalarValueType.U32));
|
||||
state.WriteVector(destination.Value, vectorRead ?? _graph.Undefined(ScalarValueType.U32));
|
||||
}
|
||||
else if (destination.Kind == Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
|
|
@ -1411,6 +1568,13 @@ public sealed partial class ScalarValueGraph
|
|||
return _graph.Select(state.Scc, _graph.Constant(1u), _graph.Constant(0u));
|
||||
}
|
||||
|
||||
// An aperture keeps its identity: a FLAT address built from it
|
||||
// targets LDS or scratch, not the device-address page table.
|
||||
if (Gen5InlineConstants.IsAperture(operand.Value))
|
||||
{
|
||||
return _graph.MemoryAperture(operand.Value);
|
||||
}
|
||||
|
||||
return Gen5InlineConstants.TryDecode(operand.Value, out var value)
|
||||
? _graph.Constant(value)
|
||||
: _graph.Undefined(ScalarValueType.U32);
|
||||
|
|
@ -1429,6 +1593,14 @@ public sealed partial class ScalarValueGraph
|
|||
return (state.Read(operand.Value), state.Read(operand.Value + 1));
|
||||
}
|
||||
|
||||
if (operand.Kind == Gen5OperandKind.EncodedConstant && Gen5InlineConstants.IsAperture(operand.Value))
|
||||
{
|
||||
// A 64-bit aperture read: the offset dword is a constant, the high
|
||||
// dword carries the aperture.
|
||||
return (_graph.Constant((uint)Gen5InlineConstants.DecodeAperture64(operand.Value)),
|
||||
_graph.MemoryAperture(operand.Value));
|
||||
}
|
||||
|
||||
var low = Read(operand, state);
|
||||
var negative = operand.Kind == Gen5OperandKind.EncodedConstant && operand.Value is >= 193 and <= 208;
|
||||
return (low, _graph.Constant(negative ? uint.MaxValue : 0u));
|
||||
|
|
@ -1549,7 +1721,11 @@ public sealed partial class ScalarValueGraph
|
|||
ThreadBits.Remove(register - 1);
|
||||
if (register == ExecLow)
|
||||
{
|
||||
Exec = _graph.Undefined(ScalarValueType.Bool);
|
||||
// An all-or-nothing constant keeps the mask known, so s_cbranch_execz stays decidable.
|
||||
Exec = value.IsConstant && Scalars[ExecHigh] is { IsConstant: true, ConstantU32: 0 } &&
|
||||
value.ConstantU32 is 0 or FullWaveMask
|
||||
? _graph.Constant(value.ConstantU32 != 0)
|
||||
: _graph.Undefined(ScalarValueType.Bool);
|
||||
}
|
||||
else if (register == VccLow)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ namespace SharpEmu.ShaderCompiler.Resources;
|
|||
|
||||
// The graph values one memory access reads its descriptor from: the handles, the read
|
||||
// node of a scalar load, and the uniform vector offset of a global access.
|
||||
public sealed record MemoryAccessBinding(ScalarValue? Handle, ScalarValue? SamplerHandle, ScalarValue? Read, ScalarValue? Offset = null);
|
||||
// Active is the EXEC mask the access runs under: a lane outside it performs no access,
|
||||
// so a lane-masked register write under the same mask contributes only its new value.
|
||||
public sealed record MemoryAccessBinding(ScalarValue? Handle, ScalarValue? SamplerHandle, ScalarValue? Read, ScalarValue? Offset = null,
|
||||
ScalarValue? Active = null);
|
||||
|
||||
// The uniform value graph of one program: every value a descriptor can be assembled
|
||||
// from, symbolic in user data and in the shader base.
|
||||
|
|
@ -61,12 +64,48 @@ public sealed partial class ScalarValueGraph
|
|||
// constants fold, so revisiting a block reproduces its values exactly.
|
||||
internal ScalarValue Constant(uint value) => Intern($"c32:{value}", () => ScalarValue.ConstantOf(value));
|
||||
|
||||
internal ScalarValue MemoryAperture(uint operand) => Intern($"aperture:{operand}", () => ScalarValue.MemoryAperture(operand));
|
||||
|
||||
internal ScalarValue Constant(ulong value) => Intern($"c64:{value}", () => ScalarValue.ConstantOf(value));
|
||||
|
||||
internal ScalarValue Constant(bool value) => Intern($"c1:{value}", () => ScalarValue.ConstantOf(value));
|
||||
|
||||
// One undefined node per type keeps every value built over it stable across visits.
|
||||
internal ScalarValue Undefined(ScalarValueType type) => Intern($"undef:{type}", () => ScalarValue.Undefined(type));
|
||||
/// <summary>
|
||||
/// The instruction the builder is translating, for diagnostics only.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Undefined nodes are interned per type, so a single node stands for every
|
||||
/// instruction the builder could not model and carries no origin of its own.
|
||||
/// A resource plan that fails on an undefined dword therefore names no
|
||||
/// culprit, and the builder has about twenty places that produce one. This
|
||||
/// records which instructions actually gave up, so the failure points at an
|
||||
/// opcode instead of a list of candidates.
|
||||
/// </remarks>
|
||||
internal (uint Pc, string Opcode) BuilderInstruction { get; set; }
|
||||
|
||||
private readonly Dictionary<ScalarValue, (uint Pc, string Opcode)> _undefinedOrigins = [];
|
||||
|
||||
/// <summary>
|
||||
/// The instruction an undefined value came from, when origins are tracked.
|
||||
/// </summary>
|
||||
internal bool TryGetUndefinedOrigin(ScalarValue value, out (uint Pc, string Opcode) origin) =>
|
||||
_undefinedOrigins.TryGetValue(value, out origin);
|
||||
|
||||
// Undefined nodes are interned per instruction. Revisiting a block reaches
|
||||
// the same instruction, so graph values stay stable while resource planning
|
||||
// can distinguish a known safe fallback from an unrelated malformed input.
|
||||
internal ScalarValue Undefined(ScalarValueType type)
|
||||
{
|
||||
if (BuilderInstruction.Opcode is null)
|
||||
{
|
||||
return Intern($"undef:{type}", () => ScalarValue.Undefined(type));
|
||||
}
|
||||
|
||||
var instruction = BuilderInstruction;
|
||||
var value = Intern($"undef:{type}:{instruction.Pc:X}", () => ScalarValue.Undefined(type));
|
||||
_undefinedOrigins.TryAdd(value, instruction);
|
||||
return value;
|
||||
}
|
||||
|
||||
internal ScalarValue UserData(uint register) => Intern($"ud:{register}", () => ScalarValue.UserData(register));
|
||||
|
||||
|
|
@ -115,11 +154,25 @@ public sealed partial class ScalarValueGraph
|
|||
|
||||
internal ScalarValue Operation(ScalarOperation operation, ScalarValueType type, params ScalarValue[] operands)
|
||||
{
|
||||
// Some ALU identities produce a deterministic value even when the other
|
||||
// operand has no tracked provenance. This matters for descriptor setup code
|
||||
// that masks an unused or hardware-defined register before using it. Keep
|
||||
// these identities ahead of the general undefined propagation below.
|
||||
if (TryFoldWithUndefined(operation, type, operands, out var undefinedFolded))
|
||||
{
|
||||
return undefinedFolded;
|
||||
}
|
||||
|
||||
foreach (var operand in operands)
|
||||
{
|
||||
if (operand.IsUndefined)
|
||||
{
|
||||
return Undefined(type);
|
||||
// Keep the operation shape when one input lacks provenance. The
|
||||
// runtime validator will still reject the undefined leaf, but
|
||||
// preserving the node lets later symbolic identities eliminate
|
||||
// only the bits that are provably independent of that input and
|
||||
// keeps diagnostics attached to the real instruction graph.
|
||||
return Intern($"op:{operation}:{type}:{Ids(operands)}", () => ScalarValue.MakeOperation(operation, type, operands));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +199,53 @@ public sealed partial class ScalarValueGraph
|
|||
return Intern($"op:{operation}:{type}:{Ids(operands)}", () => ScalarValue.MakeOperation(operation, type, operands));
|
||||
}
|
||||
|
||||
private bool TryFoldWithUndefined(ScalarOperation operation, ScalarValueType type, ScalarValue[] operands, out ScalarValue folded)
|
||||
{
|
||||
folded = null!;
|
||||
|
||||
if (operands.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (operation == ScalarOperation.And32 &&
|
||||
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.U32 && operand.ConstantU32 == 0))
|
||||
{
|
||||
folded = Constant(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation == ScalarOperation.And64 &&
|
||||
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.U64 && operand.ConstantU64 == 0))
|
||||
{
|
||||
folded = Constant(0ul);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation == ScalarOperation.Or32 &&
|
||||
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.U32 && operand.ConstantU32 == uint.MaxValue))
|
||||
{
|
||||
folded = Constant(uint.MaxValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation == ScalarOperation.LogicalAnd &&
|
||||
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.Bool && !operand.ConstantBool))
|
||||
{
|
||||
folded = Constant(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation == ScalarOperation.LogicalOr &&
|
||||
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.Bool && operand.ConstantBool))
|
||||
{
|
||||
folded = Constant(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryFold(ScalarOperation operation, ScalarValueType type, ScalarValue[] operands, out ScalarValue folded)
|
||||
{
|
||||
folded = null!;
|
||||
|
|
|
|||
|
|
@ -141,6 +141,18 @@ public sealed record StageInput(StageInputKind Kind, uint Location, uint Compone
|
|||
|
||||
public sealed record StageOutput(StageOutputKind Kind, uint Index, uint Location, string DebugName);
|
||||
|
||||
// One bounded runtime V# table lowered to a contiguous run of native buffer candidates,
|
||||
// selected through a key mapping in the flattened table.
|
||||
public sealed class BufferCandidateTableInfo
|
||||
{
|
||||
public uint FirstCandidate { get; set; }
|
||||
public uint CandidateCount { get; set; }
|
||||
public uint MappingOffset { get; set; }
|
||||
public uint SearchIterations { get; set; }
|
||||
|
||||
public BufferCandidateTableInfo Clone() => (BufferCandidateTableInfo)MemberwiseClone();
|
||||
}
|
||||
|
||||
// The dense resource tables of a program plus the facts the pipeline layout needs.
|
||||
public sealed class ShaderResourceInfo
|
||||
{
|
||||
|
|
@ -154,6 +166,7 @@ public sealed class ShaderResourceInfo
|
|||
public List<ImageResource> Images { get; set; } = [];
|
||||
public List<SamplerResource> Samplers { get; set; } = [];
|
||||
public List<SampledImagePair> SampledPairs { get; set; } = [];
|
||||
public List<BufferCandidateTableInfo> BufferCandidateTables { get; set; } = [];
|
||||
public List<StageInput> Inputs { get; set; } = [];
|
||||
public List<StageOutput> Outputs { get; set; } = [];
|
||||
public byte[] VertexFetchComponents { get; set; } = new byte[32];
|
||||
|
|
@ -168,6 +181,7 @@ public sealed class ShaderResourceInfo
|
|||
Images = Images.Select(image => image.Clone()).ToList(),
|
||||
Samplers = Samplers.Select(sampler => sampler.Clone()).ToList(),
|
||||
SampledPairs = SampledPairs.Select(pair => pair.Clone()).ToList(),
|
||||
BufferCandidateTables = BufferCandidateTables.Select(table => table.Clone()).ToList(),
|
||||
Inputs = [.. Inputs],
|
||||
Outputs = [.. Outputs],
|
||||
VertexFetchComponents = (byte[])VertexFetchComponents.Clone(),
|
||||
|
|
@ -192,12 +206,20 @@ public sealed record IndirectImageSelector(
|
|||
public uint TableOffset { get; init; }
|
||||
public uint DynamicOffsetBase { get; init; }
|
||||
public uint KeyBound { get; init; }
|
||||
public WaveIndexedImageSelector? WaveIndexed { get; init; }
|
||||
|
||||
// The key read's immediate offset. The hardware adds it after the 32-bit selector offset, without wrapping.
|
||||
public uint MaterialImmediate { get; init; }
|
||||
}
|
||||
|
||||
public sealed record DirectImageCandidate(uint Offset, uint Source);
|
||||
|
||||
// A wave-uniform descriptor selector. The guest derives each descriptor key from
|
||||
// a set bit in one scalar mask, through a compact global index table. Keeping this
|
||||
// shape explicit lets the host materialize only those keys, rather than treating a
|
||||
// lane value as an unknowable descriptor address.
|
||||
public sealed record WaveIndexedImageSelector(uint MaskOffset, uint IndexTableOffset, uint IndexStride);
|
||||
|
||||
// The graph values one descriptor is assembled from, up to eight dwords.
|
||||
public sealed class DescriptorSource
|
||||
{
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public sealed class ShaderResourcePlan
|
|||
public IReadOnlyList<ScalarValue> DynamicReads { get; private set; } = [];
|
||||
public IReadOnlyList<byte> CleanFlatSlots { get; private set; } = [];
|
||||
public IReadOnlyList<IndirectImageAccess> IndirectImages { get; private set; } = [];
|
||||
public IReadOnlyList<BufferCandidateTablePlan> BufferCandidateTables { get; private set; } = [];
|
||||
public bool RequiresSpecializationMemory { get; private set; }
|
||||
public ShaderResourceInfo Info { get; private set; } = new();
|
||||
|
||||
|
|
@ -77,7 +78,8 @@ public sealed class ShaderResourcePlan
|
|||
access.Handle is null ? null : Rewrite(access.Handle),
|
||||
access.SamplerHandle is null ? null : Rewrite(access.SamplerHandle),
|
||||
access.Read is null ? null : (reads.Replacements.ContainsKey(access.Read) ? RewriteRead(access.Read) : Rewrite(access.Read)),
|
||||
access.Offset is null ? null : Rewrite(access.Offset));
|
||||
access.Offset is null ? null : Rewrite(access.Offset),
|
||||
access.Active is null ? null : Rewrite(access.Active));
|
||||
}
|
||||
|
||||
// Diagnostics observe rewritten values before descriptor validation.
|
||||
|
|
@ -86,6 +88,7 @@ public sealed class ShaderResourcePlan
|
|||
plan.DescriptorSources = tracked.Sources;
|
||||
plan.Info = tracked.Info;
|
||||
plan.IndirectImages = tracked.IndirectImages;
|
||||
plan.BufferCandidateTables = tracked.BufferCandidateTables;
|
||||
plan.DynamicReads = plan.DynamicReads.Where(read => !tracked.IndirectReads.Contains(read)).ToList();
|
||||
|
||||
var materialization = new List<uint>();
|
||||
|
|
@ -129,6 +132,8 @@ public sealed class ShaderResourcePlan
|
|||
else if (indirect.Dense)
|
||||
{
|
||||
plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)indirect.HeapSource], cleanSlots);
|
||||
if (indirect.WaveIndexed is not null)
|
||||
plan.MarkHeapReadSlots(plan.DescriptorSources[(int)indirect.HeapSource], cleanSlots);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -168,6 +173,21 @@ public sealed class ShaderResourcePlan
|
|||
// The flattened table the host fills per draw: table reads, then the written ranges.
|
||||
public int FlattenedTableReservedCount => TableReads.Count + WrittenRangeCount * WrittenRangeDwordCount;
|
||||
|
||||
// A wave-indexed table picks its keys from a mask word read off the same heap.
|
||||
private void MarkHeapReadSlots(DescriptorSource heap, byte[] slots)
|
||||
{
|
||||
for (var slot = 0; slot < TableReads.Count && slot < slots.Length; slot++)
|
||||
{
|
||||
var value = TableReads[slot].Value;
|
||||
if (value.Kind == ScalarValueKind.ScalarAddressWord && value.Operands.Length != 0 &&
|
||||
value.Operands[0].Kind == ScalarValueKind.AddressHandle &&
|
||||
value.Operands[0].Operands.SequenceEqual(heap.Dwords))
|
||||
{
|
||||
slots[slot] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every flattened slot an indirect table's descriptor depends on must be read
|
||||
// through the clean reader, including the slots those reads depend on.
|
||||
private void MarkCleanFlatSlots(DescriptorSource source, byte[] slots)
|
||||
|
|
@ -201,6 +221,13 @@ public sealed class ShaderResourcePlan
|
|||
}
|
||||
}
|
||||
|
||||
public bool ValidateRuntimeValue(ScalarValue value) =>
|
||||
new RuntimeValueValidator(Graph, UserDataBase, UserDataCount, TableReads.Count).Validate(value);
|
||||
public bool ValidateRuntimeValue(ScalarValue value) => ValidateRuntimeValue(value, out _);
|
||||
|
||||
public bool ValidateRuntimeValue(ScalarValue value, out bool controlDependent)
|
||||
{
|
||||
var validator = new RuntimeValueValidator(Graph, UserDataBase, UserDataCount, TableReads.Count);
|
||||
var ok = validator.Validate(value);
|
||||
controlDependent = validator.ControlDependent;
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ public readonly record struct ShaderClipSpaceTransform(
|
|||
float HalfExtentX,
|
||||
float HalfExtentY);
|
||||
|
||||
// One bounded runtime V# table as the emitter sees it: a contiguous run of native buffer
|
||||
// candidates plus the flattened key mapping that selects among them.
|
||||
public readonly record struct BufferCandidateTableUse(
|
||||
uint FirstCandidate,
|
||||
uint CandidateCount,
|
||||
uint MappingOffset,
|
||||
uint SearchIterations);
|
||||
|
||||
// Everything an emitter needs to compile one permutation of a program: the decoded
|
||||
// program, its resource plan applied to one specialization, and the binding layout.
|
||||
public sealed class ShaderCompileRequest
|
||||
|
|
@ -97,11 +105,26 @@ public sealed class ShaderCompileRequest
|
|||
}
|
||||
|
||||
WrittenRangeSlotByMemoryIndex = writtenSlots;
|
||||
|
||||
var candidateTables = new Dictionary<int, BufferCandidateTableUse>();
|
||||
for (var index = 0; index < plan.BufferCandidateTables.Count && index < resources.Info.BufferCandidateTables.Count; index++)
|
||||
{
|
||||
var info = resources.Info.BufferCandidateTables[index];
|
||||
var use = new BufferCandidateTableUse(info.FirstCandidate, info.CandidateCount, info.MappingOffset, info.SearchIterations);
|
||||
foreach (var memoryIndex in plan.BufferCandidateTables[index].MemoryIndices)
|
||||
{
|
||||
candidateTables[memoryIndex] = use;
|
||||
}
|
||||
}
|
||||
|
||||
BufferCandidateTableByMemoryIndex = candidateTables;
|
||||
}
|
||||
|
||||
// The flattened table is bound when host reads, written ranges or indirect mappings fill it.
|
||||
// The flattened table is bound when host reads, written ranges, indirect mappings or
|
||||
// bounded buffer candidate mappings fill it.
|
||||
public static bool RequiresFlattenedTable(ShaderResourcePlan plan, SpecializedResourceInfo resources) =>
|
||||
plan.TableReads.Count != 0 || plan.WrittenRangeCount != 0 ||
|
||||
plan.BufferCandidateTables.Count != 0 ||
|
||||
resources.Info.Images.Any(image => image.IndirectSearchIterations != 0);
|
||||
|
||||
public Gen5ShaderProgram Program { get; }
|
||||
|
|
@ -127,12 +150,21 @@ public sealed class ShaderCompileRequest
|
|||
// Indirect image accesses: memory index → the memory index of the key read.
|
||||
public IReadOnlyDictionary<int, int> IndirectRootByMemoryIndex { get; }
|
||||
|
||||
// Bounded runtime V# accesses: memory index → the candidate run and its key mapping.
|
||||
public IReadOnlyDictionary<int, BufferCandidateTableUse> BufferCandidateTableByMemoryIndex { get; }
|
||||
|
||||
// Written device-address accesses: memory index → the flattened slot of their range.
|
||||
public IReadOnlyDictionary<int, uint> WrittenRangeSlotByMemoryIndex { get; }
|
||||
|
||||
public uint WaveSize { get; init; } = 32;
|
||||
public uint ScratchDwords { get; init; }
|
||||
public bool EnableGraphicsSubgroupOperations { get; init; } = true;
|
||||
|
||||
// The device supports 64-bit integer atomics on workgroup memory
|
||||
// (VkPhysicalDeviceFeatures.shaderSharedInt64Atomics). When set, the LDS
|
||||
// 64-bit atomics are emitted as real 64-bit atomics instead of a pair of
|
||||
// 32-bit ones, which is not atomic as a pair.
|
||||
public bool SupportsSharedInt64Atomics { get; init; }
|
||||
public Gen5ComputeSystemRegisters? ComputeSystemRegisters { get; init; }
|
||||
|
||||
public IReadOnlyList<Gen5PixelOutputBinding> PixelOutputs { get; init; } = [];
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public sealed class Gen5ShaderDecoderBoundaryTests
|
|||
private const uint Export = 0xF8000000;
|
||||
private const uint Nop = 0xBF800000;
|
||||
private const uint EndPgm = 0xBF810000;
|
||||
private const int MaximumInstructionCount = 16384;
|
||||
private const int MaximumInstructionCount = 1024 * 1024 / sizeof(uint);
|
||||
|
||||
[Fact]
|
||||
public void MissingAddress_IsRejectedWithoutReadingGuestMemory()
|
||||
|
|
@ -89,7 +89,7 @@ public sealed class Gen5ShaderDecoderBoundaryTests
|
|||
|
||||
Assert.False(decoded);
|
||||
Assert.Empty(program.Instructions);
|
||||
Assert.Equal("unterminated", error);
|
||||
Assert.StartsWith("unterminated", error, StringComparison.Ordinal);
|
||||
Assert.Equal(MaximumInstructionCount, memory.Reads.Count);
|
||||
Assert.All(memory.Reads, read => Assert.True(read.Succeeded));
|
||||
Assert.Equal(
|
||||
|
|
@ -117,6 +117,43 @@ public sealed class Gen5ShaderDecoderBoundaryTests
|
|||
Assert.Equal(words.Length, memory.Reads.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrailingBackwardBranch_StopsBeforeMetadataTrailer()
|
||||
{
|
||||
// Astro Bot: an out-of-line loop block after S_ENDPGM ends with an
|
||||
// S_BRANCH back into the loop, followed by the "sl00" metadata block.
|
||||
const uint CBranchScc0OverEndPgm = 0xBF840001u;
|
||||
const uint BranchBackToStart = 0xBF82FFFCu;
|
||||
const uint MetadataWord = 0x5D000040u;
|
||||
var memory = RecordingCpuMemory.FromWords(
|
||||
ShaderAddress,
|
||||
[CBranchScc0OverEndPgm, EndPgm, Nop, BranchBackToStart, MetadataWord]);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.True(decoded, error);
|
||||
Assert.Equal(4, program.Instructions.Count);
|
||||
Assert.Equal("SBranch", program.Instructions[^1].Opcode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebuggerBranchPastEndPgm_DoesNotDecodeDebuggerStub()
|
||||
{
|
||||
// Astro Bot: S_CBRANCH_CDBGSYS jumps to a debugger stub after
|
||||
// S_ENDPGM that loads descriptors from a devkit-only address.
|
||||
const uint CBranchCdbgsysOverEndPgm = 0xBF970001u;
|
||||
const uint StubMove = 0xBE8003FFu;
|
||||
var memory = RecordingCpuMemory.FromWords(
|
||||
ShaderAddress,
|
||||
[CBranchCdbgsysOverEndPgm, EndPgm, StubMove]);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.True(decoded, error);
|
||||
Assert.Equal(2, program.Instructions.Count);
|
||||
Assert.Equal("SEndpgm", program.Instructions[^1].Opcode);
|
||||
}
|
||||
|
||||
private static bool Decode(
|
||||
RecordingCpuMemory memory,
|
||||
ulong address,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,49 @@ public class AmprFileRegistryTests
|
|||
Assert.Equal(firstHost, resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AprResolve_UniquePath_ReturnsGuestPathHash()
|
||||
{
|
||||
AmprFileRegistry.ClearForTests();
|
||||
const string guestPath = "/app0/levels/m08_tutorial/a.cmsh";
|
||||
var host = Path.Combine(Path.GetTempPath(), "apr-unique.bin");
|
||||
|
||||
var id = AmprFileRegistry.RegisterAprResolvedPath(guestPath, host);
|
||||
|
||||
Assert.Equal(AmprFileRegistry.ComputeFileId(guestPath), id);
|
||||
Assert.Equal(id, AmprFileRegistry.RegisterAprResolvedPath(guestPath, host));
|
||||
Assert.True(AmprFileRegistry.TryGetHostPath(id, out var result));
|
||||
Assert.Equal(host, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void AprResolve_CollidingPaths_ReadTheirOwnFiles(bool indexFirst)
|
||||
{
|
||||
AmprFileRegistry.ClearForTests();
|
||||
const string firstPath = "$/assets/bce5a816.bin";
|
||||
const string secondPath = "$/assets/1b9e7058.bin";
|
||||
var firstHost = Path.Combine(Path.GetTempPath(), "apr-first.bin");
|
||||
var secondHost = Path.Combine(Path.GetTempPath(), "apr-second.bin");
|
||||
if (indexFirst)
|
||||
{
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests("assets/bce5a816.bin", firstHost);
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests("assets/1b9e7058.bin", secondHost);
|
||||
}
|
||||
|
||||
var firstId = AmprFileRegistry.RegisterAprResolvedPath(firstPath, firstHost);
|
||||
var secondId = AmprFileRegistry.RegisterAprResolvedPath(secondPath, secondHost);
|
||||
// A later alias publish of the partner must not re-poison either id.
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests("assets/1b9e7058.bin", secondHost);
|
||||
|
||||
Assert.NotEqual(firstId, secondId);
|
||||
Assert.True(AmprFileRegistry.TryGetHostPath(firstId, out var firstResult));
|
||||
Assert.True(AmprFileRegistry.TryGetHostPath(secondId, out var secondResult));
|
||||
Assert.Equal(firstHost, firstResult);
|
||||
Assert.Equal(secondHost, secondResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFileId_matches_utf8_fnv1a()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace SharpEmu.Libs.Tests.Fiber;
|
|||
/// validation and layout behaviour of <see cref="FiberExports"/>; they do not
|
||||
/// exercise a live guest thread scheduler.
|
||||
/// </summary>
|
||||
[Collection(FiberStateCollection.Name)]
|
||||
public sealed class FiberExportsTests
|
||||
{
|
||||
private const ulong Base = 0x3_0000_0000UL;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Fiber;
|
||||
|
||||
/// <summary>
|
||||
/// FiberExports keeps its continuations, stack ranges and thread states in
|
||||
/// static maps, and the tests reset them between cases. xUnit runs separate
|
||||
/// classes in parallel, so every fiber test class has to share one collection
|
||||
/// or a reset in one class wipes the state another is midway through using.
|
||||
/// </summary>
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class FiberStateCollection
|
||||
{
|
||||
public const string Name = "fiber-runtime-state";
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Fiber;
|
||||
|
||||
/// <summary>
|
||||
/// Continuation contract for a job-system style fiber loop: two fibers that
|
||||
/// hand control back and forth many times from the same resume point. Each
|
||||
/// resume has to restore exactly the state its own suspension captured, so a
|
||||
/// stale or cross-fiber restore shows up here instead of as a null dereference
|
||||
/// inside guest code several switches later.
|
||||
/// </summary>
|
||||
[Collection(FiberStateCollection.Name)]
|
||||
public sealed class FiberSwitchLoopTests : IDisposable
|
||||
{
|
||||
private const ulong Base = 0x3_0000_0000UL;
|
||||
private const int RegionSize = 0x8000;
|
||||
|
||||
private const ulong FiberA = Base + 0x0000;
|
||||
private const ulong FiberB = Base + 0x0100;
|
||||
private const ulong NameA = Base + 0x0300;
|
||||
private const ulong NameB = Base + 0x0340;
|
||||
private const ulong ArgSlotRoot = Base + 0x0380;
|
||||
private const ulong ArgSlotA = Base + 0x0388;
|
||||
private const ulong ArgSlotB = Base + 0x0390;
|
||||
private const ulong ContextA = Base + 0x1000;
|
||||
private const ulong ContextB = Base + 0x3000;
|
||||
private const ulong ContextSize = 0x1000;
|
||||
|
||||
private const ulong EntryA = 0x4_0000_1000UL;
|
||||
private const ulong EntryB = 0x4_0000_2000UL;
|
||||
private const ulong ArgOnInitializeA = 0xA11AUL;
|
||||
private const ulong ArgOnInitializeB = 0xB22BUL;
|
||||
|
||||
// The guest side of a job worker calls sceFiberSwitch from one place, so
|
||||
// every suspension of a given fiber reports the same return address and the
|
||||
// same resume stack. Only the payload registers differ between rounds.
|
||||
private const ulong ResumeRipA = 0x8_0084_1FD1UL;
|
||||
private const ulong ResumeRipB = 0x8_0084_1FD1UL;
|
||||
private const ulong ResumeRspA = 0x2_0858_FF10UL;
|
||||
private const ulong ResumeRspB = 0x2_085A_F3A0UL;
|
||||
private const ulong ReturnSlotA = ResumeRspA - 0x10UL;
|
||||
private const ulong ReturnSlotB = ResumeRspB - 0x10UL;
|
||||
|
||||
private const ulong RootReturnRip = 0x8_0084_0000UL;
|
||||
private const ulong RootResumeRsp = 0x2_0850_0000UL;
|
||||
private const ulong RootReturnSlot = RootResumeRsp - 0x10UL;
|
||||
|
||||
private const ulong ThreadHandle = 0x2E45_16AD_090UL;
|
||||
|
||||
private const uint StateRun = 1;
|
||||
private const uint StateIdle = 2;
|
||||
|
||||
private readonly IGuestThreadScheduler? _previousScheduler;
|
||||
private readonly ulong _previousThread;
|
||||
private readonly ulong _previousFiber;
|
||||
|
||||
public FiberSwitchLoopTests()
|
||||
{
|
||||
FiberExports.ResetRuntimeState();
|
||||
_previousScheduler = GuestThreadExecution.Scheduler;
|
||||
GuestThreadExecution.Scheduler = new ContextTransferScheduler();
|
||||
_previousThread = GuestThreadExecution.EnterGuestThread(ThreadHandle);
|
||||
_previousFiber = GuestThreadExecution.EnterFiber(0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GuestThreadExecution.EnterFiber(_previousFiber);
|
||||
GuestThreadExecution.RestoreGuestThread(_previousThread);
|
||||
GuestThreadExecution.Scheduler = _previousScheduler;
|
||||
FiberExports.ResetRuntimeState();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedSwitch_RestoresEachFiberOwnResumePoint()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
InitializeFiber(memory, context, FiberA, NameA, EntryA, ArgOnInitializeA, ContextA);
|
||||
InitializeFiber(memory, context, FiberB, NameB, EntryB, ArgOnInitializeB, ContextB);
|
||||
|
||||
// The thread enters the fiber world through sceFiberRun.
|
||||
GuestThreadExecution.EnterImportCallFrame(RootReturnRip, RootResumeRsp, RootReturnSlot);
|
||||
context[CpuRegister.Rdi] = FiberA;
|
||||
context[CpuRegister.Rsi] = 0x1000UL;
|
||||
context[CpuRegister.Rdx] = ArgSlotRoot;
|
||||
Assert.Equal(0, FiberExports.FiberRun(context));
|
||||
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentContextTransfer(out var toA));
|
||||
Assert.Equal(EntryA, toA.Rip);
|
||||
Assert.Equal(ArgOnInitializeA, toA.Rdi);
|
||||
Assert.Equal(0x1000UL, toA.Rsi);
|
||||
Assert.Equal(0UL, toA.Rax);
|
||||
Assert.Equal(FiberA, GuestThreadExecution.CurrentFiberAddress);
|
||||
Assert.Equal(StateRun, ReadUInt32(memory, FiberA + 4));
|
||||
|
||||
// Round 0 suspends A for the first time and starts B at its entry point.
|
||||
var sentinelsA = Sentinels(0xA0);
|
||||
Switch(memory, context, from: FiberA, to: FiberB, ResumeRipA, ResumeRspA, ReturnSlotA,
|
||||
ArgSlotA, argOnRun: 0xB000UL, sentinelsA);
|
||||
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentContextTransfer(out var toB));
|
||||
Assert.Equal(EntryB, toB.Rip);
|
||||
Assert.Equal(ArgOnInitializeB, toB.Rdi);
|
||||
Assert.Equal(0xB000UL, toB.Rsi);
|
||||
|
||||
var expectedA = new FiberExpectation(ResumeRipA, ResumeRspA, ReturnSlotA, sentinelsA);
|
||||
FiberExpectation? expectedB = null;
|
||||
|
||||
// Eight full round trips over the same pair of resume points.
|
||||
for (var round = 1; round <= 8; round++)
|
||||
{
|
||||
var sentinelsB = Sentinels((ulong)round << 32 | 0xB0);
|
||||
var argToA = 0xA000UL + (ulong)round;
|
||||
Switch(memory, context, from: FiberB, to: FiberA, ResumeRipB, ResumeRspB, ReturnSlotB,
|
||||
ArgSlotB, argToA, sentinelsB);
|
||||
expectedB = new FiberExpectation(ResumeRipB, ResumeRspB, ReturnSlotB, sentinelsB);
|
||||
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentContextTransfer(out var resumedA));
|
||||
AssertResume(resumedA, expectedA, $"round {round}: fiber A");
|
||||
Assert.Equal(argToA, ReadUInt64(memory, ArgSlotA));
|
||||
Assert.Equal(StateRun, ReadUInt32(memory, FiberA + 4));
|
||||
Assert.Equal(StateIdle, ReadUInt32(memory, FiberB + 4));
|
||||
|
||||
var nextSentinelsA = Sentinels((ulong)round << 32 | 0xA0);
|
||||
var argToB = 0xB000UL + (ulong)round;
|
||||
Switch(memory, context, from: FiberA, to: FiberB, ResumeRipA, ResumeRspA, ReturnSlotA,
|
||||
ArgSlotA, argToB, nextSentinelsA);
|
||||
expectedA = new FiberExpectation(ResumeRipA, ResumeRspA, ReturnSlotA, nextSentinelsA);
|
||||
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentContextTransfer(out var resumedB));
|
||||
AssertResume(resumedB, expectedB, $"round {round}: fiber B");
|
||||
Assert.Equal(argToB, ReadUInt64(memory, ArgSlotB));
|
||||
Assert.Equal(StateRun, ReadUInt32(memory, FiberB + 4));
|
||||
Assert.Equal(StateIdle, ReadUInt32(memory, FiberA + 4));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedSwitch_KeepsResumeStackInsideTheOwningFiberContext()
|
||||
{
|
||||
var memory = new FakeCpuMemory(Base, RegionSize);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
InitializeFiber(memory, context, FiberA, NameA, EntryA, ArgOnInitializeA, ContextA);
|
||||
InitializeFiber(memory, context, FiberB, NameB, EntryB, ArgOnInitializeB, ContextB);
|
||||
|
||||
GuestThreadExecution.EnterImportCallFrame(RootReturnRip, RootResumeRsp, RootReturnSlot);
|
||||
context[CpuRegister.Rdi] = FiberA;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
context[CpuRegister.Rdx] = ArgSlotRoot;
|
||||
Assert.Equal(0, FiberExports.FiberRun(context));
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentContextTransfer(out var toA));
|
||||
|
||||
// A fresh fiber must start on its own context block, not on the stack of
|
||||
// the thread that ran it.
|
||||
Assert.InRange(toA.Rsp, ContextA, ContextA + ContextSize);
|
||||
Assert.InRange(toA.ReturnSlotAddress, ContextA, ContextA + ContextSize);
|
||||
|
||||
Switch(memory, context, from: FiberA, to: FiberB, ResumeRipA, ResumeRspA, ReturnSlotA,
|
||||
ArgSlotA, argOnRun: 0, Sentinels(0xA0));
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentContextTransfer(out var toB));
|
||||
|
||||
Assert.InRange(toB.Rsp, ContextB, ContextB + ContextSize);
|
||||
Assert.InRange(toB.ReturnSlotAddress, ContextB, ContextB + ContextSize);
|
||||
Assert.NotEqual(toA.Rsp, toB.Rsp);
|
||||
}
|
||||
|
||||
private static void Switch(
|
||||
FakeCpuMemory memory,
|
||||
CpuContext context,
|
||||
ulong from,
|
||||
ulong to,
|
||||
ulong resumeRip,
|
||||
ulong resumeRsp,
|
||||
ulong returnSlot,
|
||||
ulong argOnReturnAddress,
|
||||
ulong argOnRun,
|
||||
IReadOnlyDictionary<CpuRegister, ulong> sentinels)
|
||||
{
|
||||
Assert.Equal(from, GuestThreadExecution.CurrentFiberAddress);
|
||||
|
||||
// The guest reaches sceFiberSwitch through the import trampoline, which
|
||||
// is what publishes the resume point for the calling fiber.
|
||||
GuestThreadExecution.EnterImportCallFrame(resumeRip, resumeRsp, returnSlot);
|
||||
foreach (var (register, value) in sentinels)
|
||||
{
|
||||
context[register] = value;
|
||||
}
|
||||
|
||||
context[CpuRegister.Rdi] = to;
|
||||
context[CpuRegister.Rsi] = argOnRun;
|
||||
context[CpuRegister.Rdx] = argOnReturnAddress;
|
||||
|
||||
Assert.Equal(0, FiberExports.FiberSwitch(context));
|
||||
Assert.Equal(to, GuestThreadExecution.CurrentFiberAddress);
|
||||
_ = memory;
|
||||
}
|
||||
|
||||
private static void AssertResume(GuestCpuContinuation actual, FiberExpectation? expected, string because)
|
||||
{
|
||||
Assert.NotNull(expected);
|
||||
Assert.Equal(expected.Rip, actual.Rip);
|
||||
Assert.Equal(expected.Rsp, actual.Rsp);
|
||||
Assert.Equal(expected.ReturnSlotAddress, actual.ReturnSlotAddress);
|
||||
// sceFiberSwitch returns SCE_OK to the fiber it resumes.
|
||||
Assert.Equal(0UL, actual.Rax);
|
||||
|
||||
foreach (var (register, value) in expected.Sentinels)
|
||||
{
|
||||
var restored = register switch
|
||||
{
|
||||
CpuRegister.Rbx => actual.Rbx,
|
||||
CpuRegister.Rbp => actual.Rbp,
|
||||
CpuRegister.R12 => actual.R12,
|
||||
CpuRegister.R13 => actual.R13,
|
||||
CpuRegister.R14 => actual.R14,
|
||||
CpuRegister.R15 => actual.R15,
|
||||
_ => throw new InvalidOperationException($"Unexpected sentinel register {register}."),
|
||||
};
|
||||
|
||||
Assert.True(value == restored, $"{because}: {register} was 0x{restored:X16}, expected 0x{value:X16}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<CpuRegister, ulong> Sentinels(ulong seed) =>
|
||||
new Dictionary<CpuRegister, ulong>
|
||||
{
|
||||
[CpuRegister.Rbx] = 0x1111_0000_0000_0000UL | seed,
|
||||
[CpuRegister.Rbp] = 0x2222_0000_0000_0000UL | seed,
|
||||
[CpuRegister.R12] = 0x3333_0000_0000_0000UL | seed,
|
||||
[CpuRegister.R13] = 0x4444_0000_0000_0000UL | seed,
|
||||
[CpuRegister.R14] = 0x5555_0000_0000_0000UL | seed,
|
||||
[CpuRegister.R15] = 0x6666_0000_0000_0000UL | seed,
|
||||
};
|
||||
|
||||
private static void InitializeFiber(
|
||||
FakeCpuMemory memory,
|
||||
CpuContext context,
|
||||
ulong fiber,
|
||||
ulong nameAddress,
|
||||
ulong entry,
|
||||
ulong argOnInitialize,
|
||||
ulong contextAddress)
|
||||
{
|
||||
memory.WriteCString(nameAddress, $"F{fiber:X}");
|
||||
context[CpuRegister.Rdi] = fiber;
|
||||
context[CpuRegister.Rsi] = nameAddress;
|
||||
context[CpuRegister.Rdx] = entry;
|
||||
context[CpuRegister.Rcx] = argOnInitialize;
|
||||
context[CpuRegister.R8] = contextAddress;
|
||||
context[CpuRegister.R9] = ContextSize;
|
||||
Assert.Equal(0, FiberExports.FiberInitialize(context));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private sealed record FiberExpectation(
|
||||
ulong Rip,
|
||||
ulong Rsp,
|
||||
ulong ReturnSlotAddress,
|
||||
IReadOnlyDictionary<CpuRegister, ulong> Sentinels);
|
||||
|
||||
private sealed class ContextTransferScheduler : IGuestThreadScheduler
|
||||
{
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context) { }
|
||||
public bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error) => throw new NotSupportedException();
|
||||
public bool TryJoinThread(CpuContext callerContext, ulong threadHandle, out ulong returnValue, out string? error) => throw new NotSupportedException();
|
||||
public void Pump(CpuContext callerContext, string reason) { }
|
||||
public int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue) => 0;
|
||||
public bool HasPendingGuestExceptionForCurrentThread() => false;
|
||||
public bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority) => throw new NotSupportedException();
|
||||
public bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask) => throw new NotSupportedException();
|
||||
public IReadOnlyList<GuestThreadSnapshot> SnapshotThreads() => Array.Empty<GuestThreadSnapshot>();
|
||||
public bool TryCallGuestFunction(CpuContext callerContext, ulong entryPoint, ulong arg0, ulong arg1,
|
||||
ulong stackAddress, ulong stackSize, string reason, out string? error) => throw new NotSupportedException();
|
||||
public bool TryCallGuestFunction(CpuContext callerContext, ulong entryPoint, ulong arg0, ulong arg1,
|
||||
ulong arg2, ulong stackAddress, ulong stackSize, string reason, out ulong returnValue,
|
||||
out string? error) => throw new NotSupportedException();
|
||||
public bool TryCallGuestFunction(CpuContext callerContext, ulong entryPoint, ulong arg0, ulong arg1,
|
||||
ulong arg2, ulong arg3, ulong stackAddress, ulong stackSize, string reason, out ulong returnValue,
|
||||
out string? error) => throw new NotSupportedException();
|
||||
public bool TryCallGuestContinuation(CpuContext callerContext, GuestCpuContinuation continuation, string reason,
|
||||
out string? error) => throw new NotSupportedException();
|
||||
public bool TryRaiseGuestException(CpuContext callerContext, ulong threadHandle, ulong handler, int exceptionType,
|
||||
out string? error) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -322,7 +322,10 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
runner.Run(StreamRunner.CustomPacket(Nop, 0, 0x6875_0781, StreamRunner.Low(Label), StreamRunner.High(Label), 0x9A, 0x04, 0x38));
|
||||
Assert.Equal(new[] { "prepare_flip 7 2 1 68", "eop FlipWithInterruptWriteBack32", "flush" }, runner.Host.Calls);
|
||||
Assert.Contains("flip event type is unknown", runner.RunExpectingFatal(StreamRunner.CustomPacket(Nop, 0, 0x6875_0781, 0, 0, 0, 0x28, 0x38)).Message);
|
||||
Assert.Contains("marker is unknown", runner.RunExpectingFatal(StreamRunner.CustomPacket(Nop, 0, 0x6875_0001)).Message);
|
||||
// Marker ids with no side effect (Demon's Souls emits 0x6EC and 0x80C) are consumed, not fatal.
|
||||
runner.Host.Calls.Clear();
|
||||
runner.Run(StreamRunner.CustomPacket(Nop, 0, 0x6875_06EC), StreamRunner.CustomPacket(Nop, 0, 0x6875_080C));
|
||||
Assert.Empty(runner.Host.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public sealed class GuestPixelFormatsTests
|
|||
{
|
||||
Assert.Equal(4u, GuestPixelFormats.BytesPerElement(GuestPixelFormat.Bits8_8_8_8UNorm));
|
||||
Assert.Equal(8u, GuestPixelFormats.BytesPerElement(GuestPixelFormat.Bits16_16_16_16Float));
|
||||
Assert.Equal(2u, GuestPixelFormats.BytesPerElement(GuestPixelFormat.Bits8_8UScaled));
|
||||
Assert.Equal(0u, GuestPixelFormats.BytesPerElement(GuestPixelFormat.Bits8SNorm));
|
||||
Assert.Equal(0u, GuestPixelFormats.BytesPerElement(GuestPixelFormat.Invalid));
|
||||
Assert.Equal(0u, GuestPixelFormats.BytesPerElement((GuestPixelFormat)200));
|
||||
|
|
@ -35,10 +36,13 @@ public sealed class GuestPixelFormatsTests
|
|||
Assert.Equal(TextureNumericClass.Uint, GuestPixelFormats.SampledNumericClass(GuestPixelFormat.Bits8_8_8_8UInt));
|
||||
Assert.Equal(TextureNumericClass.Sint, GuestPixelFormats.SampledNumericClass(GuestPixelFormat.Bits8_8_8_8SInt));
|
||||
Assert.Equal(TextureNumericClass.Float, GuestPixelFormats.SampledNumericClass(GuestPixelFormat.Bits8_8_8_8UNorm));
|
||||
Assert.Equal(TextureNumericClass.Float, GuestPixelFormats.SampledNumericClass(GuestPixelFormat.Bits8_8UScaled));
|
||||
Assert.Equal(TextureNumericClass.Unsupported, GuestPixelFormats.SampledNumericClass(GuestPixelFormat.Bits8SNorm));
|
||||
Assert.Equal(GuestPixelFormat.Bits32UInt, GuestPixelFormats.RemapTextureFormat(GuestPixelFormat.Bits11_11_10UInt));
|
||||
Assert.Equal(GuestPixelFormat.Bc7Srgb, GuestPixelFormats.RemapTextureFormat(GuestPixelFormat.Bc7Srgb));
|
||||
Assert.Equal(Format.R8G8B8A8Unorm, GuestPixelFormats.HostFormat(GuestPixelFormat.Bits8_8_8_8UNorm));
|
||||
Assert.Equal(Format.R8G8Uscaled, GuestPixelFormats.HostFormat(GuestPixelFormat.Bits8_8UScaled));
|
||||
Assert.Equal(Format.B10G11R11UfloatPack32, GuestPixelFormats.HostFormat(GuestPixelFormat.Bits11_11_10UNorm));
|
||||
Assert.Equal(Format.R8G8B8A8Srgb, GuestPixelFormats.HostFormat(GuestPixelFormat.Bits8_8_8_8Srgb));
|
||||
Assert.Equal(Format.BC1RgbaUnormBlock, GuestPixelFormats.HostFormat(GuestPixelFormat.Bc1UNorm));
|
||||
Assert.Equal(Format.Undefined, GuestPixelFormats.HostFormat(GuestPixelFormat.Invalid));
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public sealed class ImageDescriptionTests
|
|||
description.Resources = new SubresourceCount(levels, layers);
|
||||
description.Pitch = width;
|
||||
description.BytesPerBlock = 4;
|
||||
description.MipLayout[0] = new MipLevelLayout { Size = size, Pitch = width, Height = height };
|
||||
return description;
|
||||
}
|
||||
|
||||
|
|
@ -70,10 +71,24 @@ public sealed class ImageDescriptionTests
|
|||
volume.Type = GuestImageType.Color3D;
|
||||
volume.Extent = new Extent3D(32, 32, 8);
|
||||
volume.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 32768, Pitch = 32, Height = 32 };
|
||||
Assert.Equal(0, Color(Base, 32768, 32, 32, layers: 8).FindMatchingMipLevel(volume));
|
||||
Assert.Equal(-1, Color(Base, 32768, 32, 32, layers: 8).FindMatchingMipLevel(volume));
|
||||
Assert.Equal(-1, Color(Base, 32768, 32, 32, layers: 4).FindMatchingMipLevel(volume));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindMatchingArraySlice_UsesTheFullMipChainStrideForEachArrayLayer()
|
||||
{
|
||||
const ulong sliceStride = 21504;
|
||||
var container = Color(Base, 2 * sliceStride, 64, 64, levels: 3, layers: 2);
|
||||
container.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 2 * 16384, Pitch = 64, Height = 64 };
|
||||
container.MipLayout[1] = new MipLevelLayout { Offset = 16384, Size = 2 * 4096, Pitch = 32, Height = 32 };
|
||||
container.MipLayout[2] = new MipLevelLayout { Offset = 20480, Size = 2 * 1024, Pitch = 16, Height = 16 };
|
||||
|
||||
var secondLayerMip = Color(Base + sliceStride + 16384, 4096, 32, 32);
|
||||
Assert.Equal(1, secondLayerMip.FindMatchingMipLevel(container));
|
||||
Assert.Equal(1, secondLayerMip.FindMatchingArraySlice(container, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthRules_ResolveAttachmentAndTransferFormats()
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue