Compare commits

...
Author SHA1 Message Date
bigjjmoneymanandnmzik 0685bffb2f texture cache: convert D16<->D24/D32 depth copies instead of crashing (#1873)
gpu: convert guest D16 depth between native D16 and stenciled four-byte host depth

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-27 10:41:22 +01:00
nmzik a5b3673a39 graphics: draw patched-offset indirect draws from GPU-read records 2026-10-27 10:13:15 +01:00
nmzik 09753bf8ff memory: change defaults 2026-10-27 10:13:15 +01:00
nmzik 9d8c458535 gpu: keep reused smaller image tables resident and drop them a frame after their last draw 2026-10-27 10:13:15 +01:00
358333125d libFont: ship Noto Sans as the fallback font (#1917)
* cmake: download Noto Sans at configure time

Fetch the hinted Regular, Bold, Italic and Bold Italic of Noto Sans
v2.015 and their licence from the Noto project's font distribution
(notofonts.github.io) with FetchContent, like the other third-party
dependencies. The commit and each file's SHA-256 are pinned in
variables.

Only these four styles are needed, so they are fetched individually
instead of through the release archive (117 MB, every style and build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmake: ship Noto Sans beside the emulator

Copy the downloaded fonts to assets/fonts next to kyty_emulator on every
build, like the touch cursors and the trophy sound, and install them
there with the SIL Open Font License under licenses/noto-sans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* libFont: fall back to the shipped Noto Sans instead of Tracy's Roboto

Fonts stb_truetype cannot read, including every system font set opened
with sceFontOpenFontSet, used Roboto from Tracy's source folder, found
relative to the source tree or the working directory. Installed builds
have neither, so their system fonts drew every character as '?'.

Load Noto Sans Regular, Bold, Italic or Bold Italic from assets/fonts
next to the emulator instead, and log when a file is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* libFont: fetch Noto Sans with file(DOWNLOAD) and drop the style substitution

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-27 07:51:24 +01:00
nmzik fd6b28c4b2 build: target x86-64-v3 without floating-point contraction 2026-10-27 07:04:38 +01:00
Senaxx 6cee2dc0bd shader: inline the hot IR operand accessors 2026-10-27 07:04:38 +01:00
Senaxx a526a8b9c9 shader: hand IR uses over at once on replacement and skip use lists at program teardown 2026-10-27 07:04:38 +01:00
Senaxx 1ec69f8e7d gpu: make cull mode and front face dynamic state 2026-10-27 07:04:38 +01:00
nmzik 2463e3aaf0 gpu: write end-of-pipe labels beside GPU-written bytes to the backing store 2026-10-27 07:04:38 +01:00
nmzikandstrxno 556538bed5 gpu: write D16 demotion results with one 16-bit store
Co-authored-by: strxno <rokas.pettersen@gmail.com>
2026-10-27 07:04:38 +01:00
Mr. Big BritchesandClaude Opus 5.5 770438cee0 ci: fix the Linux build and the macOS SRT walker test crashes (#1922)
* graphics: give InlinePageOwnerList's const_iterator iterator traits

The Linux build failed in textureCache.cpp: std::any_of over the image
owners of a page needs std::iterator_traits, which libstdc++ only fills
from the iterator's own member types. MSVC's STL and libc++ don't
consult them there, so Windows and macOS built.

Declare the five member types. The category is input, not forward,
because operator* returns a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: assert InlinePageOwnerList's const_iterator has an input category

Only libstdc++ needs the iterator traits for std::any_of, so removing
them would compile on Windows and macOS and break only the Linux build.
The assert fails the build on every platform instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: serve SRT walker test loads on macOS too

FakeGuestMemory only installed its fault handler on Linux and Windows,
so on macOS the walker's deliberately faulting loads crashed
scalar_provenance, resource_materialization and resource_tracking.

Share the Linux sigaction path with macOS, read the registers from the
macOS thread state, and catch SIGBUS as well as SIGSEGV there, like the
emulator's own handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:26:08 +01:00
nmzik db12f0c5fb shader: select consecutive GPU-selected sampler table entries without a switch 2026-10-27 04:23:01 +01:00
nmzik 3f9666acbb shader: seal SSA blocks once their predecessors are filled 2026-10-27 04:23:01 +01:00
nmzik 7eb8abb279 shader: drop the duplicate-use scan from Inst::AddUse 2026-10-27 04:23:01 +01:00
Claxten 91a5ff6cb7 shader: decode s_dcache_inv as a no-op 2026-10-27 04:23:01 +01:00
Claxten 95a507a8ff shader: support 64-bit buffer atomic minimum 2026-10-27 04:23:01 +01:00
Claxten b2053c2861 shader: bound per-lane descriptor table loops by the program's uniform loop count 2026-10-27 04:23:01 +01:00
Claxten 9801fd862b shader: zero image descriptors that only unreached blocks use 2026-10-27 04:23:01 +01:00
nmzikandshadowbeat070 78adb9293d shader: give A16 1D and late 3D gradients their own address VGPRs
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-27 04:23:01 +01:00
Claxten 0a72641678 shader: evaluate saturating float-to-signed conversions in runtime descriptor values 2026-10-27 04:23:01 +01:00
Claxten 99f4eba223 shader: read bit-scan records only at ranks their mask buffer words set 2026-10-27 04:23:01 +01:00
nmzikandshadowbeat070 04bfd9070c shader: give an A16 bias its own address VGPR
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-27 04:23:01 +01:00
nmzikandshadowbeat070 ffa3992c1a shader: decode every PS5 image_gather4 encoding
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-27 04:23:00 +01:00
Claxten 1b70938052 agc: emit async memory semaphores and execute them in the CP 2026-10-27 04:23:00 +01:00
Claxten 914a3bb4b8 shader: enumerate set ranks of eight-word light bitmaps 2026-10-27 04:23:00 +01:00
Claxten ffb6ad6cab shader: bound selected-lane record indices by guarded shared append lists 2026-10-27 04:23:00 +01:00
Claxten ff5d16eefc tests: expect an inclusive child limit to bound one more record 2026-10-27 04:23:00 +01:00
Claxten d118afbeae tests: expect scalar selector records past the V# to read zero 2026-10-27 04:23:00 +01:00
Claxten a3ac6d0d32 shader: bound table keys by an inclusive guard with a constant limit 2026-10-27 04:23:00 +01:00
Claxten f297c0bfe1 shader: bound bit-scan record keys and drop the key a guard rules out 2026-10-27 04:23:00 +01:00
Claxten 20bb268a48 shader: bound raw image table keys by a signed clamp on the key or a sibling read 2026-10-27 04:23:00 +01:00
Claxten d3768c00e9 shader: accept indirect image rows whose type reads the leading sample coordinates 2026-10-27 04:23:00 +01:00
Claxten dfcf1c0b03 shader: bound waterfall image table keys converted from clamped float arithmetic 2026-10-27 04:23:00 +01:00
Claxten de3ed905c8 shader: track image table keys divided from a runtime base plus the workgroup ID 2026-10-27 04:23:00 +01:00
Claxten 0a140911ad kernel: default flexible memory to 448 MiB and charge loaded modules' segments to it 2026-10-27 04:23:00 +01:00
nmzik 2483573651 shader: mask V_MAD_U64_U32 carry-out bits of lanes outside EXEC 2026-10-27 04:23:00 +01:00
MrFr3di 986721f5bc shader: decode FLAT, GLOBAL and SCRATCH D16 high-half loads (#1924)
FLAT-family opcode 0x25 (FLAT/GLOBAL/SCRATCH_LOAD_SHORT_D16_HI) loads an
unsigned halfword into bits [31:16] of the destination and keeps bits [15:0].
Decode it beside the low-half variant and let it select the high half of the
existing partial destination write, so FLAT_LOAD needs no new path.

Ghost of Yotei CS 0x79b0066f0cd1f3cd stopped at
global_load_short_d16_hi v16, v18, s[0:1] offset:196 (0xdc9480c4 0x10000012).
2026-10-26 22:34:54 +01:00
f90a8b1589 graphics: decode register clears of RGBA16 UNORM, UINT and SINT and RG32 UINT and SINT targets
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Jetsku <59046009+Jetsku@users.noreply.github.com>
2026-10-26 07:43:11 +01:00
nmzikandusa-ratt 17a0f53d39 graphics: resolve MSAA color into the level and layer the texture cache placed each target at
Co-authored-by: usa-ratt <188116257+usa-ratt@users.noreply.github.com>
2026-10-26 07:43:11 +01:00
nmzikandSenaxx 943bc19f46 shader: leave the start instance out of mesh and NGG instance ID VGPRs
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 07:43:11 +01:00
nmzik 5429888d4c shader: keep pixel helper lanes out of the EXEC and VCC words 2026-10-26 07:43:11 +01:00
nmzikandSenaxx 025d1f7edf shader: decode the SHARED_LIMIT and PRIVATE_LIMIT inline constants
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 06:35:17 +01:00
nmzik 3c62269751 ci: build Windows with Visual Studio 2026 on windows-2025 2026-10-26 05:40:00 +01:00
shadowbeat070 ceb154d9e4 shader: acquire stored buffers and images at S_BARRIER 2026-10-26 05:32:02 +01:00
26ca609236 shader: type pixel outputs of UINT and SINT render targets as integer vectors
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
nmzikandSenaxx 4ebae87a1b shader: apply the LOD clamp of image_sample _cl variants
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
nmzikandAli Almohaya b4209dde1b graphics: write the subdraw index of multi-draw indirect packets
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-26 05:32:02 +01:00
nmzik 72d7da7266 texture_cache: view BC images for raw reads of their layers instead of evicting them 2026-10-26 05:32:02 +01:00
ce5e57aa36 shader: fold complementary masks so wave reductions see full EXEC
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
2026-10-26 05:32:02 +01:00
6357740fa1 shader: lower wave32 whole-wave UMin and UMax reductions to subgroup reductions
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
2026-10-26 05:32:02 +01:00
nmzik be1c84f4d7 graphics: shorten the mesh draw skip warning 2026-10-26 05:32:02 +01:00
nmzik 560756f34c graphics: skip draws of mesh shaders that exceed host mesh limits 2026-10-26 05:32:02 +01:00
52c80be266 savedata: stop logging empty sceSaveDataGetEventResult polls
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
22a9392d23 savedata: implement sceSaveDataConvert's synchronous checks
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
nmzik 95bbc9c65d graphics: hold image heap descriptors whose address another extent pins 2026-10-26 05:32:02 +01:00
Senaxx c5f7644751 audio: stub sceAcmBatchJobPriority 2026-10-26 05:32:02 +01:00
nmzik 4db225008b graphics: resize image heap tables in place when their descriptor size changes 2026-10-26 05:32:02 +01:00
nmzik dc3bd68dee gpu: bound null storage bindings to zero bytes 2026-10-26 05:32:02 +01:00
nmzik c1dfb309a1 graphics: drop image heap table snapshots superseded at the same base 2026-10-26 05:32:02 +01:00
nmzik ba7f9969ef shader: bound storage buffers by pushed binding sizes instead of OpArrayLength 2026-10-26 05:32:02 +01:00
7524e8f79e shader: count one SGPR for lane reads and wave32 lane masks in call targets
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
2026-10-26 05:32:02 +01:00
nmzik 153dadd0b3 shader: emit 16-bit packs with a zero high lane as a mask 2026-10-26 05:32:02 +01:00
nmzik 463a096249 graphics: unwatch image heap pages on writes next to a table 2026-10-26 05:32:02 +01:00
LloydNicholson 597137e8b1 vulkan: enable VK_EXT_extended_dynamic_state when available (#1906) 2026-10-26 02:08:43 +01:00
b2c9ec7609 renderer: decode R8, R8G8 and R16_UINT color register clears (#1893)
DecodePackedColorClear had no case for R8_UNORM/UINT, R8G8_UNORM/UINT and
R16_UINT, so a CMASK fast clear of such a render target was dropped and the
target kept its old contents. UFC 6 (PPSA23566) fast-clears an R8_UINT
region map this way every frame and then writes only part of it; a capture
showed the map never cleared. DWORD fills of these formats are decoded only
when every pixel in the word holds the same value.

Co-authored-by: 4wgycvn84p-afk <346802885+4wgycvn84p-afk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:47:45 +01:00
herrMirtoandAndre Michi 99044c7c49 libs: provide NpEntitlementAccessPft state query used by FC27 (#1897)
Co-authored-by: Andre Michi <herrmirto@proton.me>
2026-10-25 18:38:26 +01:00
nmzik 83f8bfd3c9 pipeline: print a console message before the shader cache warm-up 2026-10-25 02:20:44 +02:00
nmzik dd5bc26c64 pipeline: write the shader store on a background thread 2026-10-25 02:20:44 +02:00
nmzik 72903bfce6 shader: use the V_PERMLANE16 row directly instead of copying it 2026-10-25 02:20:44 +02:00
nmzik 59d8256b36 shader: encode 11/10-bit float formatted buffer stores as truncated unsigned floats 2026-10-25 02:20:44 +02:00
nmzik 9a20c6f738 shader: disable DPP16 writes from EXEC-disabled sources without BOUND_CTRL 2026-10-25 02:20:44 +02:00
nmzik 67c89e0915 shader: keep the old V_PERMLANE16 destination for inactive sources without BOUND_CTRL 2026-10-25 02:20:44 +02:00
nmzik 5d8381e0f3 launcher: hide the unused Shader optimization option 2026-10-25 02:20:44 +02:00
TheCruZ e864cb20bd shader: clamp V_FRACT_F16/F32/F64 results below one 2026-10-25 02:20:44 +02:00
nmzik 40aaab482e launcher: add a Cache shaders option, on by default 2026-10-25 02:20:44 +02:00
nmzik ec8b75b244 pipeline: persist shaders and pipelines per game and prewarm them at boot 2026-10-25 02:20:44 +02:00
TheCruZ 9210264d47 shader: saturate V_CVT_PK_U16_U32 and V_CVT_PK_I16_I32 2026-10-25 02:20:44 +02:00
nmzik 66e21a6245 addcont: add user add-ons with --addcont and a launcher list 2026-10-25 02:20:44 +02:00
nmzik 87e49282b8 pipeline: log real shader compiles to the console 2026-10-25 02:20:44 +02:00
nmzik 1816004357 shader: serialize resource plans for the shader cache 2026-10-25 02:20:44 +02:00
nmzik bfc377627a pipeline: create pipelines from GraphicsPipelineKey and shader modules 2026-10-25 02:20:44 +02:00
nmzik 4b651f2ba7 pipeline: fold the program cache into PipelineCache with stable module ids 2026-10-25 02:20:44 +02:00
nmzik 8c01d8e849 libc: fix swapped strcmp/strncmp NIDs and bind strcpy to its own NID instead of strcpy_s's 2026-10-25 02:20:44 +02:00
nmzik e0c5ae090c kernel: send _write to descriptors 0-2 through KernelWrite instead of dropping it 2026-10-25 02:20:44 +02:00
Mr. Big Britchesandnmzik 906d25568f launcher: option to close the terminal after a clean exit (#1869)
launcher: close the terminal after a clean exit and run several games at once

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-24 20:06:45 +02:00
nmzik 8291cfeb04 shader: parse vertex metadata once when a shader is mapped 2026-10-24 05:44:46 +02:00
nmzik dbc7b8b2ff pipeline: look up draw programs and pipelines from the command buffer 2026-10-24 05:18:46 +02:00
nmzik 269e411ef2 graphics: index live cache slots directly 2026-10-24 04:26:16 +02:00
nmzik 170c107779 texture_cache: dedupe region queries with a picked flag 2026-10-24 04:03:01 +02:00
nmzik 8dc0d56b7e common: SlotVector without generations 2026-10-24 03:48:46 +02:00
nmzik d7856753e0 shader: walk every resource plan with the x86 SRT walker 2026-10-24 02:57:24 +02:00
nmzik bdf48dcf3b shader: complete faulting SRT walker loads as zero reads 2026-10-24 01:39:42 +02:00
nmzik f356fa3730 gpu: move the BDA sync queue from MemoryTracker into BufferCache 2026-10-24 01:05:19 +02:00
nmzik 07f6420273 memory: take the address-space lock shared for tracker protections 2026-10-23 23:53:51 +02:00
nmzik 6aaa533296 gpu: queue GDS release interrupts behind earlier interrupts 2026-10-23 22:22:39 +02:00
nmzik dc40478e7b gpu: deliver RELEASE_MEM interrupts with the slice flush instead of their own 2026-10-23 22:14:55 +02:00
nmzik 56a82bcd41 launcher: draw the game grid as cards over the selected game's background 2026-10-23 21:24:32 +02:00
nmzik bf5dd62eea gpu: decode RELEASE_MEM directly from the driver's native packet 2026-10-23 21:11:07 +02:00
nmzik 227f12ee4b gpu: remove the PS4-era constant engine, marker NOPs and EVENT_WRITE_EOP/EOS packets 2026-10-23 20:42:41 +02:00
nmzik 246d511568 graphics: count occlusion queries on the host GPU behind --occlusion-queries 2026-10-23 19:48:53 +02:00
nmzik ba0e93daac libs: implement JpegDec and JpegEnc 2026-10-23 19:48:53 +02:00
Senaxx 77a8f30758 shader: read S_MEMREALTIME from the host device clock shifted to about 100 MHz 2026-10-23 19:48:53 +02:00
nmzik 1ffaaa96b1 kernel: share the direct-memory span walk; unchecked release frees the parts around gaps 2026-10-23 19:48:53 +02:00
Senaxx 706aa030ad shader: extract 9-bit NGG primitive vertex indices, leaving the edge flags out 2026-10-23 19:48:53 +02:00
nmzik ecc30a1f2d shader: decode S_FLBIT_I32 and S_FLBIT_I32_I64 with a 4-op signed leading-bit search 2026-10-23 19:48:53 +02:00
nmzik 3688412dca revert stub 2026-10-23 19:48:53 +02:00
nmzik 45f53236e8 hle: implement sceDeviceServiceGetEventState (My First Gran Turismo) 2026-10-23 19:48:53 +02:00
Felipe 9dcff95e80 shader: fix crash from packed ancillary used as DPP old operand (#1769)
shader: emit plain EXEC selects for DPP writes that reach every active lane (Street Fighter 6)
2026-10-23 09:51:32 +02:00
nmzik 719c44310a graphics: mark image heap resources as sampled reads 2026-10-23 08:08:56 +02:00
nmzik 7bb618d41a docs: show Cyberpunk 2077 and MLB The Show 23 in README screenshots 2026-10-23 07:57:57 +02:00
nmzik 87e815b250 net: accept a NULL user agent in sceHttpCreateTemplate (Cyberpunk 2077) 2026-10-23 07:42:28 +02:00
nmzik f9e14084d6 graphics: set the heap flag in ImageHeapResource 2026-10-23 07:22:33 +02:00
nmzik 38c884e8c1 graphics: keep image heap descriptors of unmapped textures null 2026-10-23 07:17:57 +02:00
nmzik 27dc7c281f graphics: keep bounded image heaps resident in a bindless descriptor set 2026-10-23 07:17:57 +02:00
nmzik 79837b423d graphics: support compute stores to 2D MSAA images 2026-10-22 22:11:06 +02:00
nmzikandArtyom Zabroda aafe014d0c shader: read raw D16 depth bits through kDepth k16UInt image loads
Co-authored-by: Artyom Zabroda <artyomzabroda@proton.me>
2026-10-22 22:11:06 +02:00
nmzik a358b847c1 graphics: stall waitUntilSafeForRendering in its command processor instead of the GPU thread 2026-10-22 22:11:06 +02:00
nmzik a951a91703 spirv: emit OpName only with debug info 2026-10-22 22:11:06 +02:00
nmzikandArtyom Zabroda a875d1b205 net: report open file and device descriptors ready in select (Cyberpunk 2077 Galaxy RNG seeding)
Co-authored-by: Artyom Zabroda <artyomzabroda@proton.me>
2026-10-22 22:11:06 +02:00
Henrique Reis Neves f5f4e7d48a usbd: return an empty device list from sceUsbdGetDeviceList (Gran Turismo 7) (#1831) 2026-10-22 16:18:28 +02:00
nmzik 86fed10b81 graphics: draw non-indexed and native-fetch indirect draws from GPU-read records 2026-10-22 16:12:20 +02:00
nmzik 949de19bb0 graphics: draw indexed indirect multi-draws from GPU-read arguments 2026-10-22 16:12:20 +02:00
nmzik c513a4a13a graphics: draw single indexed indirect draws from GPU-read arguments 2026-10-22 16:12:20 +02:00
Mr. Big Britches b54057942d texture cache: select color-metadata clears on the GPU with conditional rendering 2026-10-22 16:12:20 +02:00
nmzik b6495a5b67 texture cache: share color attachment rendering setup 2026-10-22 16:12:20 +02:00
nmzik 5b315a9ea7 gpu tiler: share compute pipeline creation 2026-10-22 16:12:20 +02:00
nmzik 5f5d6effa4 graphics: count DCC/CMASK metadata readbacks in the performance overlay 2026-10-22 16:12:20 +02:00
nmzik c5016d9455 shader: support D16 XY and XYZ formatted buffer loads and stores 2026-10-22 05:26:54 +02:00
nmzik 753f285198 shader: generalize D16 buffer format component handling 2026-10-22 05:26:54 +02:00
Nawid3333andnmzik d7c1896647 shader: interpolate f16 parameter halves (V_INTERP_P1LL_F16, V_INTERP_P2_F16) (#1792)
shader: interpolate packed f16 attributes for v_interp_p1ll_f16 and v_interp_p2_f16

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-22 04:54:10 +02:00
Arda Sahinandnmzik 8502d8a77d image: drop optional create flags the driver rejects instead of aborting (#1760)
* image: request extended usage only for storage images

* tests: restore block-texel-view support through the flag check loop order

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-22 03:56:13 +02:00
nmzik cb8042c5aa launcher: compress game folders to ZAR 2026-10-22 03:35:38 +02:00
Mr. Big Britchesandnmzik b46eeb7866 audio: stop AudioOut2 contexts from stalling each other (PPSA09809) (#1745)
audio: submit AudioOut2 pushes outside the state mutex so contexts stop stalling each other

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-22 02:52:11 +02:00
Phialityandnmzik 05694abad5 audio: play dataless and streamed NGS2 PCM waveform blocks for Oricru (#1787)
ngs2: stream PCM waveform blocks by byte like ATRAC9 so blocks may hold fewer frames than their duration

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-22 02:09:04 +02:00
Nawid3333 919fac13d0 shader: read lane 0 in V_READFIRSTLANE when EXEC is zero instead of an undefined lane 2026-10-22 01:20:54 +02:00
nmzik a52d1ed4e2 shader: drop the 64-bit min/max NaN-lhs select the ordered compare already covers 2026-10-22 01:09:09 +02:00
nmzik 2b37dee2b7 shader: track possible bits through bit-field inserts in resource tracking 2026-10-22 01:08:07 +02:00
nmzik a3aece2762 shader: check the DPP source lane bound only for row shifts 2026-10-22 01:02:38 +02:00
nmzik 64186404a3 shader: load the DPP lane id once for both partial write masks 2026-10-22 01:01:38 +02:00
nmzik 11465a5e3c shader: drop the large-input select before sin and cos since fract already returns zero 2026-10-22 00:44:43 +02:00
nmzik 09a9654458 shader: compute DPP permute, rotate, mirror and share lanes with bit-field and XOR ops 2026-10-22 00:40:07 +02:00
nmzik 6123419437 shader: skip full DPP row and bank masks and test partial ones with one bit extract 2026-10-22 00:35:09 +02:00
nmzik 8177969f33 shader: branch to the pixel kill on the inverted targets instead of negating the condition 2026-10-22 00:33:07 +02:00
nmzik 6ee2ef8715 shader: insert the V_CVT_PK_U8_F32 byte with one bit-field insert 2026-10-22 00:31:52 +02:00
nmzik 9e4234cdbe shader: extract the V_CVT_F32_UBYTE source byte with one bit-field extract 2026-10-22 00:30:39 +02:00
nmzik ce54aa8d14 shader: return the first source when both 64-bit min/max inputs are NaN 2026-10-22 00:28:43 +02:00
nmzik 2d00e13bf3 shader: rely on the saturating float-to-int conversion instead of re-saturating in the translator 2026-10-22 00:26:40 +02:00
nmzik 3bb09ed2a4 shader: clamp before float-to-int conversion and drop the redundant truncate 2026-10-22 00:26:40 +02:00
nmzik 1e4ac1371d shader: return the first source when both float min/max inputs are NaN 2026-10-22 00:00:04 +02:00
nmzik 9c39062d3e shader: double the cube major axis once after selecting the face 2026-10-21 23:56:13 +02:00
nmzik 93071ebdf8 shader: drop the denormal flush before exp2 2026-10-21 23:33:42 +02:00
nmzik 3a842e1a6b shader: flush FP32 denormals with a float compare and multiply instead of bit masks 2026-10-21 23:33:42 +02:00
nmzik da19501889 shader: translate zero-kind branches as the negated nonzero vote 2026-10-21 23:33:30 +02:00
nmzik 0f7d112545 shader: emit constant-true guarded blocks inline and fold immediate constant-buffer indices 2026-10-21 23:27:04 +02:00
nmzik ae47ac26e7 shader: compute scalar signed-overflow SCC with compares instead of XOR masks 2026-10-21 23:27:04 +02:00
nmzik 330d373dc6 launcher: add icon and docs logo 2026-10-21 23:27:04 +02:00
nmzik 7c40dbc36c shader: build the S_BFE_U64 mask from clamped halves and remove RightMask64 2026-10-21 23:16:17 +02:00
nmzik 0cdc5c1224 shader: build the V_MBCNT lanes-below mask with the shared right-mask helper 2026-10-21 23:11:26 +02:00
nmzik fa32940d14 shader: drop BFM mask bits shifted past bit 31 instead of an out-of-range bit-field insert 2026-10-21 22:59:56 +02:00
nmzik 2862ad5b25 shader: compute scalar signed-overflow and shift-add carry SCC with fewer ops 2026-10-21 22:56:21 +02:00
nmzik aacf15fd0b shader: write preserved SDWA fields with one bit-field insert and drop duplicate 16-bit masks 2026-10-21 22:52:14 +02:00
nmzik 10a852c2ac shader: pack 16-bit lane pairs with one bit-field insert 2026-10-21 22:46:04 +02:00
nmzik 4a1440197f shader: share one find-first-bit lowering for S_FF0/FF1/FLBIT_I32_B64 and remove FindUMsb64 2026-10-21 22:39:34 +02:00
nmzik 6e5167598e shader: compute add-with-carry with compares and remove IAddCarry32 2026-10-21 22:28:11 +02:00
nmzik c571d8ed5a shader: remove IR opcodes and IREmitter helpers that nothing produces 2026-10-21 22:19:26 +02:00
286481889f shader: emit float min/max as host instructions on AMD GPUs (#1761)
* shader: emit float min/max as host instructions on AMD GPUs

V_MIN_F32, V_MAX_F32, V_MIN3_F32 and V_MAX3_F32 were emulated with a
classify-and-select sequence of about a dozen instructions each, to give
the guest's NaN and signed-zero results on any host. AMD GPUs execute
GLSL.std.450 NMin/NMax with those same rules, so emit them directly when
the device is AMD and supports VK_KHR_shader_float_controls2. Each
instruction is decorated FPFastMathMode None, since
SignedZeroInfNanPreserve does not cover extended instructions. MED3 keeps
its exact emulation. KYTY_EMULATED_FLOAT_MIN_MAX=1 keeps the emulation.

In SAROS the two heaviest compute shaders get 37% and 59% faster on an
RX 9070 XT (RADV), about 4 ms of GPU time per frame.

Adds VectorMinMaxF32RuntimeEdges (runtime operands, so nothing is folded)
and a --float-min-max-only mode that runs it and
VectorMinMaxF32NanAndSignedZeroEdges with the emulation and, where
supported, with host instructions.

Developed with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: gate host float min/max on RADV and check the emitted instructions

* shader: drop float controls 2 from host float min/max

* shader: drop the float min/max env override and atomic flag

* shader: pass host capabilities to the SPIR-V emitter as a profile built once per device

* shader: enable host float min/max on AMD GPUs

---------

Co-authored-by: arda <ardaeska@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-21 21:36:08 +02:00
c07ad621f2 graphics: write small GPU-written tiled images back to guest memory behind --readback-tiled-images
Co-authored-by: Dimitar Vagalinski <275970482+overlordxrz-source@users.noreply.github.com>
Co-authored-by: Mr. Big Britches <111622360+MrBigBritches@users.noreply.github.com>
2026-10-21 20:55:02 +02:00
80932f8da1 Remove leftover update workspace folders on launcher startup (#1782)
* Remove leftover update workspace folders on launcher startup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Skip recently active update workspaces during cleanup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep update workspaces for an hour before cleanup so Windows downloads are not removed

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-21 20:43:30 +02:00
Artyom Zabroda f0e55803ba Add CMakeUserPresets.json to .gitignore (#1766)
Add CMakeUserPresets.json to the .gitignore
2026-10-21 20:41:03 +02:00
FelipeandCopilot App ca6a7b88ce launcher: add Enable all / Disable all to the cheats dialog (#1773)
launcher: add Enable all / Disable all buttons to the cheats dialog

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-21 20:34:59 +02:00
nmzik 13b291f998 gpu: honour DB_ALPHA_TO_MASK as alpha-to-coverage and keep its depth writes behind the shader 2026-10-21 19:17:50 +02:00
nmzik 2944d126b3 audio: implement sceAudioOutGetSystemState 2026-10-21 19:17:50 +02:00
nmzik 7b7294aa03 gpu: drop attachments whose sample count differs from the raster when a draw mixes MSAA and single-sample targets 2026-10-21 19:17:50 +02:00
nmzikandLeandro Bras 4644c52927 graphics: initialize BDA tables before first buffer registration
Co-authored-by: Leandro Bras <shadowgaming.7@gmail.com>
2026-10-21 05:48:13 +02:00
nmzik 4bb4be50b9 agc: remove count-limited logging and register trace 2026-10-21 05:48:13 +02:00
nmzik be9e13915d log: format LOGF output out of line so small guest fiber stacks do not reserve it 2026-10-21 05:48:13 +02:00
nmzik 68d0ba5a81 shader: bound scalar-read overlap checks by the bytes fixed-offset buffer writes reach 2026-10-21 05:48:13 +02:00
nmzik 70668fbd98 gpu: drop global barriers for PM4 flush events and releases, ordering buffer-writing draws like dispatches 2026-10-21 02:02:25 +02:00
nmzik ea1b032c59 shader: drop the unread F32Class bits and the per-pass HS reflection counter reset 2026-10-20 23:54:32 +02:00
nmzik 3c4124dafa shader: emit F32 min/max as exact compare-selects, MED3 with one max and saturate as NClamp 2026-10-20 23:36:26 +02:00
nmzik 8cc5f5364a shader: share SMEM load results across emulated wave64 halves 2026-10-20 22:32:48 +02:00
nmzikandNeySlim 1ce663b5a9 shader: lower HS neighbour LDS reads, control-point output read-back, looped HS reflection and integer partitioning
Co-authored-by: NeySlim <4509984+NeySlim@users.noreply.github.com>
2026-10-20 22:18:00 +02:00
nmzik 66f488a17f main: take boolean emulator options as value-less flags 2026-10-20 21:34:45 +02:00
shadowbeat070 7b7f9a70c6 kernel: answer ESRCH for a thread the detached-thread reaper has joined 2026-10-20 15:54:14 +02:00
nmzik 6ad393775f memory: write GPU publishes to backed pages and skip reserved holes like sparse reads 2026-10-20 15:53:23 +02:00
nmzik f492027be2 shader: select pointer-table images through indexed x3 material keys in readlane waterfalls 2026-10-20 15:21:19 +02:00
nmzik 8843291506 gpu: accept any T# perf_mod on storage textures through a shared canonical encoding check 2026-10-20 15:21:19 +02:00
nmzik 63ca9c0b80 shader: decode PS5 image_gather4_c_a and image_gather4_c_o_a 2026-10-20 15:21:19 +02:00
Mr. Big Britches 0caf4a6aa0 audio: route NGS2 custom mastering voices to the render output 2026-10-20 15:21:19 +02:00
Mr. Big Britches c715891242 shader: memoize the tessellation layout per LS/HS pair and VGT configuration 2026-10-20 15:21:19 +02:00
nmzik f3e01aaf1e tests: cover tessellation patch constants, predicated mul24 addresses, and forward-branch joins 2026-10-20 15:21:19 +02:00
Mr. Big Britches 0957f088ad shader: reflect HS patch constants and lower TES patch inputs through predicated addresses 2026-10-20 15:21:19 +02:00
Mr. Big Britches 4a2961cdf9 shader: reflect tessellation strides across forward branches, DS write widths, and packed patch masks 2026-10-20 15:21:19 +02:00
Mr. Big Britches f0e462a8a2 shader: allocate split-wave dispatcher spill IDs in program order 2026-10-20 15:21:19 +02:00
Mr. Big Britches feed075a9f gpu: implement PA_CL_UCP user clip planes through draw push constants 2026-10-20 15:21:19 +02:00
Mr. Big Britches 18e6747dbf audio: allow looping waveform blocks and exit-loop on NGS2 custom samplers 2026-10-20 15:21:19 +02:00
Mr. Big Britches 6b6274fa5a audio: route NGS2 custom rack buffers and run USER_FX side-chain modules 2026-10-20 15:21:19 +02:00
Andre Michi 1a053f9e13 shader: accept dword SETPC tables whose load reuses the selector 2026-10-20 05:11:48 +02:00
nmzik 12ca1599cf net: implement the libSceHttp callback, gzip, chunked, cert and error ABIs 2026-10-20 04:38:12 +02:00
nmzik f186b35706 shader: drop null checks on the stage input info pointers 2026-10-20 03:22:40 +02:00
992a83003d tests: stub VR pad ports in controller settings tests (#1741)
* tests: stub VR pad ports in controller settings tests

#1726 added PadVrPortHand/PadVrPort/PadVrClose calls to controller.cpp,
which controller_settings_tests #includes directly, breaking its link
(the full-emulator tests pick up libs/padVr.cpp via glob and are fine).
Stub the three VR entry points as unavailable, matching the file's
existing DualSenseHaptics fakes.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* tests: stub PadVrCheckHandle for the Windows controller settings link

---------

Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-20 03:22:27 +02:00
0xcrstn fa8216c4d9 shader: size per-thread LDS emulation by the guest LDS allocation 2026-10-20 02:51:27 +02:00
Leandro Bras 742fe17173 vr: add OpenXR headset output, VR tracker and VR controller input behind --vr (#1726) 2026-10-20 02:18:51 +02:00
Pipe530 0cad4672e9 launcher: add System/Dark/Light theme option 2026-10-19 16:06:00 +02:00
Almo7aya cd4eab9d65 gpu: clamp resolved buffer ranges to storage buffer limits 2026-10-19 13:53:18 +02:00
nmzik fde0b98e69 gpu: preserve zero-stride buffer fetch payloads 2026-10-19 12:23:43 +02:00
shadowbeat070 d4465faa79 gpu: preserve ordered-append command order 2026-10-19 11:46:39 +02:00
nmzik e5f0ee7fc2 gpu: preserve finite buffer descriptors across sparse PRT holes 2026-10-19 05:26:41 +02:00
nmzik 5f29374d5b gpu: approximate PPSA24156 EQAA with 2x MSAA and warn once 2026-10-19 05:17:32 +02:00
nmzik 2c730e58ea revert HTile lifetime changes and opt-in ZMask support 2026-10-19 05:04:56 +02:00
Pixeluted e87fb8c859 kernel: hand guest mutex ownership directly to FIFO waiters 2026-10-19 05:04:56 +02:00
Pixeluted 440245f616 gpu: bound overlapping image copies by both native backings 2026-10-19 05:04:56 +02:00
nmzik 05d25ef28c revert 01ce97388: restore compute metadata clear consumption 2026-10-19 05:04:56 +02:00
Pixeluted cad6aafdc5 shader: detect and loudly skip unsupported GPU-buffer callables 2026-10-19 05:04:56 +02:00
nmzik defb2f11d3 Share median classifications and preserve RDNA2 signed-zero selection 2026-10-19 02:34:25 +02:00
nmzik 75d7a4311d Share repeated SSA bitcasts and guard emitted shader size 2026-10-19 02:34:25 +02:00
nmzik bb505a3ae8 Recognize packed descriptor keys in GPU sampler waterfalls 2026-10-19 02:34:25 +02:00
nmzik ab8566591f shader: link multiple compute helpers without synthetic return loops 2026-10-19 02:34:25 +02:00
nmzik b2d3ce6dfc shader: size expanded sampler bindings independently of native roots 2026-10-19 02:34:25 +02:00
nmzik 09c993dfe0 shader: query dimensions through shared indirect image selection 2026-10-19 02:34:25 +02:00
nmzik de81300b28 shader: bound indexed material image selectors before heap expansion 2026-10-19 02:34:25 +02:00
nmzik ac3711ce7c shader: select the primary mip for linear explicit-LOD gathers 2026-10-19 02:34:24 +02:00
nmzik 8b51370ca1 shader: eliminate predicates for empty branch detours 2026-10-19 02:34:24 +02:00
nmzik e9bcc77e0b shader: retain resource guards for 64-bit scalar comparisons 2026-10-18 20:22:54 +02:00
nmzik 8639e0de81 shader: share indirect image accesses by type and guard emitted size 2026-10-18 20:16:49 +02:00
nmzik 7675579e79 kernel: implement POSIX fsync through the shared file API 2026-10-18 20:15:53 +02:00
nmzik 831862a50b razor: stub 2026-10-18 19:29:54 +02:00
nmzik 099de8dea6 graphics: publish each PM4 WRITE_DATA word exactly once 2026-10-18 16:17:51 +02:00
opcod3 f4bdb52922 agc: restore verified fused shader size name 2026-10-18 16:12:28 +02:00
nmzik 22ef16a044 shader: implement PS5 vector data cache invalidation 2026-10-18 15:57:14 +02:00
nmzik 37df7f3278 Show pipeline cache sizes in MB and remove obsolete texture folder cleanup 2026-10-18 03:57:45 +02:00
nmzik 33208a545a shader: remove artificial sampled image and sampler pair limit 2026-10-18 03:20:38 +02:00
Santiago Paqueteandnmzik 4059179e9d graphics: write proven compute buffer fills to guest memory (#1628)
Avoid readbacks for compute metadata fills

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-18 05:42:14 +02:00
nmzik fe3857a171 graphics: materialize CMASK clears for multisampled targets (PPSA13794) 2026-10-18 02:37:56 +02:00
nmzik c3e7ec8285 graphics: fix Jusant regression from d8c4a9af3 2026-10-18 01:03:37 +02:00
239 changed files with 22203 additions and 9066 deletions

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
windows:
name: Build KytyPS5 (Windows)
needs: metadata
runs-on: windows-2022
runs-on: windows-2025
steps:
- name: Checkout repository
+1
View File
@@ -6,3 +6,4 @@ _Build/vscode-clang/
_Build/
cmake-build-debug/
_PipelineCache/
CMakeUserPresets.json
+1 -1
View File
@@ -27,7 +27,7 @@
"args": [
"--game", "D:/gamesps5/PPSA07429",
"--printf-direction", "File",
"--graphics-debug-dump", "True"
"--graphics-debug-dump"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\_Build\\vscode-clang",
+39
View File
@@ -25,6 +25,45 @@ FetchContent_MakeAvailable(opus)
set(BUILD_TESTING "${kyty_build_testing_saved}")
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
# Headsets are driven through OpenXR. Only the static loader is built; the runtime that serves it
# (for example SteamVR, Monado or Virtual Desktop) comes from the user's system.
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_SYSTEM_JSONCPP OFF CACHE BOOL "" FORCE)
FetchContent_Declare(openxr
GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git
GIT_TAG f2448a8797c85814aa892efc1ab8707900fbcc78 # release-1.1.63
GIT_SHALLOW FALSE)
FetchContent_MakeAvailable(openxr)
# Keep the loader's headers and library out of the installed package.
set_property(DIRECTORY "${openxr_SOURCE_DIR}" PROPERTY EXCLUDE_FROM_ALL TRUE)
# The loader uses exceptions internally.
if(NOT KYTY_CLANG_CL)
target_compile_options(openxr_loader PRIVATE -fexceptions)
endif()
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
# Noto Sans v2.015 (SIL Open Font License 1.1) is libFont's fallback font.
set(KYTY_NOTO_SANS_REPOSITORY "https://raw.githubusercontent.com/notofonts/notofonts.github.io")
set(KYTY_NOTO_SANS_COMMIT 42af67ccd12bc0e31ed85384bf73e39248265374)
set(KYTY_NOTO_SANS_BASE_URL "${KYTY_NOTO_SANS_REPOSITORY}/${KYTY_NOTO_SANS_COMMIT}/fonts")
set(KYTY_NOTO_SANS_DIR "${FETCHCONTENT_BASE_DIR}/noto-sans")
function(kyty_fetch_noto_sans path sha256)
get_filename_component(name "${path}" NAME)
file(DOWNLOAD "${KYTY_NOTO_SANS_BASE_URL}/${path}" "${KYTY_NOTO_SANS_DIR}/${name}"
EXPECTED_HASH SHA256=${sha256})
endfunction()
kyty_fetch_noto_sans(LICENSE f2095b08bed08b23a6fe26112fcd679a2bee3f002eef077eb05d215ed1051bd8)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Regular.ttf
478c558ea716033cd60c03438f628dfa75694dcf6b5f6d505a2f05fd2b4f3823)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Bold.ttf
1df075a380fc7cb898acf64c1f7b3b4dd780de3caa860178bf929de35817a913)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Italic.ttf
467e3f89eeca4108bb8710a2b9e0cf2281ac56d5b0609211a83776d0505eecb5)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-BoldItalic.ttf
1b602a9d6353be42c91df097a4857b69fa2696f26703d7a33b54a15d87c2622c)
install(FILES "${KYTY_NOTO_SANS_DIR}/LICENSE" DESTINATION "licenses/noto-sans")
set(KYTY_NOTO_SANS_DIR "${KYTY_NOTO_SANS_DIR}" PARENT_SCOPE)
# The guest instruction patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+55 -1
View File
@@ -265,6 +265,7 @@ set(gpu_tiler_shader_names
promote_d16
demote_d16
color_transform
color_clear
)
file(GLOB gpu_tiler_shader_includes CONFIGURE_DEPENDS "${gpu_tiler_shader_dir}/gpu_tiler_*.inc")
foreach(shader_name IN LISTS gpu_tiler_shader_names)
@@ -336,7 +337,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus OpenXR::openxr_loader libzstd_static)
# Linux system libraries.
if(LINUX)
@@ -406,6 +407,8 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(build_flags_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/BuildFlagsTests.cpp")
add_executable(page_manager_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
@@ -450,6 +453,11 @@ add_executable(lru_cache_tests EXCLUDE_FROM_ALL
)
target_include_directories(lru_cache_tests PRIVATE ${inc_headers})
add_executable(slot_vector_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SlotVectorTests.cpp"
)
target_include_directories(slot_vector_tests PRIVATE ${inc_headers})
add_executable(ime_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImeDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/imeCommon.cpp"
@@ -540,6 +548,19 @@ add_executable(png_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/PngEncTests.cpp
target_link_libraries(png_enc_tests common fmt::fmt)
target_include_directories(png_enc_tests PRIVATE ${inc_headers})
add_executable(jpeg_dec_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/JpegDecTests.cpp")
target_link_libraries(jpeg_dec_tests common fmt::fmt)
target_include_directories(jpeg_dec_tests PRIVATE ${inc_headers})
add_executable(jpeg_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/JpegEncTests.cpp")
target_link_libraries(jpeg_enc_tests common fmt::fmt)
target_include_directories(jpeg_enc_tests PRIVATE ${inc_headers})
add_executable(occlusion_queries_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/OcclusionQueriesTests.cpp")
target_link_libraries(occlusion_queries_tests common fmt::fmt Vulkan::Headers)
target_include_directories(occlusion_queries_tests PRIVATE ${inc_headers})
add_executable(ces_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/CesTests.cpp")
target_link_libraries(ces_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(ces_tests PRIVATE ${inc_headers})
@@ -631,6 +652,7 @@ if(BUILD_TESTING)
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
add_test(NAME build_flags COMMAND $<TARGET_FILE:build_flags_tests>)
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
@@ -651,10 +673,17 @@ if(BUILD_TESTING)
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
add_test(NAME jpeg_dec COMMAND $<TARGET_FILE:jpeg_dec_tests>)
add_test(NAME jpeg_enc COMMAND $<TARGET_FILE:jpeg_enc_tests>)
add_test(NAME occlusion_queries COMMAND $<TARGET_FILE:occlusion_queries_tests>)
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_tessellation
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --tessellation-only)
add_test(NAME shader_recompiler_float_min_max
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --float-min-max-only)
add_test(NAME shader_recompiler_alignbyte
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
add_test(NAME graphics_pipeline_rasterization
@@ -671,6 +700,14 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --stream-buffer-only)
add_test(NAME gpu_command_lane
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
add_test(NAME wait_until_safe_for_rendering
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --wait-until-safe-for-rendering-only)
add_test(NAME pm4_flip
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --flip-only)
add_test(NAME depth_bits
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --depth-bits-only)
add_test(NAME msaa_storage
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --msaa-storage-only)
add_test(NAME pm4_context_state
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
@@ -709,6 +746,7 @@ if(BUILD_TESTING)
image_page_table_tests
memory_tracker_tests
page_manager_tests
build_flags_tests
archive_file_tests
trophy_system_tests
avplayer_file_tests
@@ -731,6 +769,9 @@ if(BUILD_TESTING)
host_input_tests
save_data_memory_tests
png_enc_tests
jpeg_dec_tests
jpeg_enc_tests
occlusion_queries_tests
ces_tests
save_data_dialog_tests
http_uri_parse_tests
@@ -774,6 +815,19 @@ add_custom_target(kyty_touch_cursors
add_dependencies(kyty_emulator kyty_touch_cursors)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
set(KYTY_NOTO_SANS_FONTS
"${KYTY_NOTO_SANS_DIR}/NotoSans-Regular.ttf"
"${KYTY_NOTO_SANS_DIR}/NotoSans-Bold.ttf"
"${KYTY_NOTO_SANS_DIR}/NotoSans-Italic.ttf"
"${KYTY_NOTO_SANS_DIR}/NotoSans-BoldItalic.ttf")
add_custom_target(kyty_noto_sans_fonts
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:kyty_emulator>/assets/fonts"
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KYTY_NOTO_SANS_FONTS}
"$<TARGET_FILE_DIR:kyty_emulator>/assets/fonts"
VERBATIM)
add_dependencies(kyty_emulator kyty_noto_sans_fonts)
install(FILES ${KYTY_NOTO_SANS_FONTS} DESTINATION assets/fonts)
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
if (WIN32)
target_link_libraries(kyty_emulator opengl32)
+10
View File
@@ -46,6 +46,16 @@ or graphical glitches, so please include the version you tested when reporting a
## Screenshots
<table align="center">
<tr>
<td align="center">
<strong>Cyberpunk 2077</strong><br>
<img src="docs/screenshots/ps5-07.png" width="300" alt="Cyberpunk 2077 running in KytyPS5">
</td>
<td align="center">
<strong>MLB The Show 23</strong><br>
<img src="docs/screenshots/ps5-08.png" width="300" alt="MLB The Show 23 running in KytyPS5">
</td>
</tr>
<tr>
<td align="center">
<strong>Astro Bot</strong><br>
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e3e9f2"/></linearGradient>
</defs>
<g transform="translate(256 268) scale(1.16) translate(-256 -268)" stroke-linejoin="round" stroke-linecap="round">
<path d="M100 196 L120 104 Q124 92 134 99 L214 160 Z" fill="url(#body)"/><path d="M412 196 L392 104 Q388 92 378 99 L298 160 Z" fill="url(#body)"/>
<path d="M128 164 L137 128 L174 156 Z" fill="#0b0d12"/><path d="M384 164 L375 128 L338 156 Z" fill="#0b0d12"/>
<g fill="none" stroke="#0b0d12" stroke-width="16"><path d="M100 196 L120 104 Q124 92 134 99 L214 160 Z"/><path d="M412 196 L392 104 Q388 92 378 99 L298 160 Z"/></g>
<path d="M256 154 C330 154 380 160 406 178 C432 196 447 244 459 302 C471 358 466 398 430 408 C400 416 380 398 360 368 L326 318 C316 304 306 300 290 300 L222 300 C206 300 196 304 186 318 L152 368 C132 398 112 416 82 408 C46 398 41 358 53 302 C65 244 80 196 106 178 C132 160 182 154 256 154 Z" fill="url(#body)"/>
<path d="M256 154 C330 154 380 160 406 178 C432 196 447 244 459 302 C471 358 466 398 430 408 C400 416 380 398 360 368 L326 318 C316 304 306 300 290 300 L222 300 C206 300 196 304 186 318 L152 368 C132 398 112 416 82 408 C46 398 41 358 53 302 C65 244 80 196 106 178 C132 160 182 154 256 154 Z" fill="none" stroke="#0b0d12" stroke-width="16"/>
<g fill="none" stroke="#0b0d12" stroke-width="9"><path d="M82 260 C100 274 118 288 128 304 C134 316 121 350 108 388"/><path d="M430 260 C412 274 394 288 384 304 C378 316 391 350 404 388"/></g>
<path d="M64 322 L82 340 M62 340 L76 354 M448 322 L430 340 M450 340 L436 354" fill="none" stroke="#0b0d12" stroke-width="7"/>
<path d="M168 230 Q194 202 220 230" fill="none" stroke="#0b0d12" stroke-width="15"/>
<circle cx="322" cy="220" r="22" fill="#0b0d12"/>
<path d="M238 252 L274 252 L256 276 Z" fill="#0b0d12" stroke="#0b0d12" stroke-width="8"/>
</g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+19
View File
@@ -109,4 +109,23 @@ std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
return entries;
}
std::optional<AddContentEntry> ParseAddContent(std::string_view value) {
static const std::regex label_pattern("^[A-Z0-9]{16}$");
const auto eq = value.find('=');
const auto label = value.substr(0, eq);
if (!std::regex_match(label.begin(), label.end(), label_pattern)) {
return std::nullopt;
}
AddContentEntry entry {{}, std::string(label), {}, {}};
if (eq != std::string_view::npos) {
entry.mount_dir = PathFromUtf8(FixFilenameSlash(value.substr(eq + 1)));
std::error_code ec;
if (!std::filesystem::is_directory(entry.mount_dir, ec)) {
return std::nullopt;
}
}
return entry;
}
} // namespace Common
+6 -1
View File
@@ -2,7 +2,9 @@
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace Common {
@@ -10,7 +12,7 @@ namespace Common {
struct AddContentEntry {
std::string content_id;
std::string entitlement_label;
std::filesystem::path mount_dir;
std::filesystem::path mount_dir; // Empty for an add-on without extra data.
std::string title;
};
@@ -18,6 +20,9 @@ struct AddContentEntry {
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
const std::string& title_id);
// Parses a user add-on: "LABEL" without extra data, or "LABEL=DIR" with its data in DIR.
std::optional<AddContentEntry> ParseAddContent(std::string_view value);
} // namespace Common
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
+7
View File
@@ -5,6 +5,7 @@
#include <cstdint>
#include <filesystem>
#include <functional>
#include <memory>
#include <vector>
@@ -45,6 +46,12 @@ private:
// Accepts either a host archive filename or a virtual path inside the archive.
[[nodiscard]] std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path);
// Packs a host directory into a new ZArchive outside it. progress gets 0 < done <= total and
// returns false to cancel. A failed or cancelled pack removes the partial archive.
[[nodiscard]] bool PackArchive(const std::filesystem::path& directory,
const std::filesystem::path& archive,
const std::function<bool(uint64_t done, uint64_t total)>& progress);
} // namespace Common
#endif /* KYTY_COMMON_ARCHIVE_H_ */
+84
View File
@@ -0,0 +1,84 @@
// Kept out of archive.cpp so the statically linked emulator does not pull in the writer and the
// zstd compressor.
#include "common/archive.h"
#include "common/stringUtils.h"
#include <algorithm>
#include <zarchive/zarchivewriter.h>
namespace Common {
bool PackArchive(const std::filesystem::path& directory, const std::filesystem::path& archive,
const std::function<bool(uint64_t done, uint64_t total)>& progress) {
namespace fs = std::filesystem;
struct Output {
File file;
bool ok = true;
} output;
// Create the archive before walking, so a re-run started while a cancelled pack is still
// walking sees it exists instead of losing its archive to that pack's cleanup.
if (!output.file.Create(archive)) {
return false;
}
struct Entry {
fs::path path;
uint64_t size;
bool is_file;
};
std::vector<Entry> entries;
uint64_t total = 0;
std::error_code error;
// Follow directory links, as the emulator does when it runs the folder.
fs::recursive_directory_iterator it(directory, fs::directory_options::follow_directory_symlink,
error);
for (; !error && it != fs::end(it); it.increment(error)) {
const bool is_file = !it->is_directory(error);
const auto size = is_file && !error ? it->file_size(error) : 0;
if (error) {
break;
}
entries.push_back({it->path(), size, is_file});
total += size;
}
output.ok = !error;
ZArchiveWriter writer(
[](int32_t, void*) {},
[](const void* data, size_t size, void* opaque) {
auto& out = *static_cast<Output*>(opaque);
uint32_t written = 0;
out.file.Write(data, static_cast<uint32_t>(size), &written);
out.ok = out.ok && written == size;
},
&output);
std::vector<uint8_t> buffer(1u << 20u);
uint64_t done = 0;
for (size_t index = 0; output.ok && index < entries.size(); index++) {
const auto& entry = entries[index];
const auto name = PathToGenericString(entry.path.lexically_relative(directory));
File input;
output.ok = entry.is_file ? writer.StartNewFile(name.c_str()) &&
input.Open(entry.path, File::Mode::Read)
: writer.MakeDir(name.c_str());
for (uint64_t left = entry.size; output.ok && left != 0;) {
uint32_t read = 0;
input.Read(buffer.data(), static_cast<uint32_t>(std::min<uint64_t>(left, buffer.size())),
&read);
writer.AppendData(buffer.data(), read);
done += read;
left -= read;
output.ok = output.ok && read != 0 && progress(done, total);
}
}
if (output.ok) {
writer.Finalize();
}
output.ok = output.ok && output.file.Flush();
output.file.Close();
if (!output.ok) {
File::DeleteFile(archive);
}
return output.ok;
}
} // namespace Common
+3
View File
@@ -29,6 +29,9 @@ enum class Counter {
TextureUploadBytes,
TextureDownloadBytes,
TextureEvictions,
ColorMetadataReadbacks, // CPU blocked reading back GPU-written DCC/CMASK metadata.
ColorMetadataReadbackNs,
ColorMetadataChecks, // GPU clear-key checks of DCC/CMASK metadata.
BufferUploadBytes,
BufferDownloadBytes,
BufferEvictions,
+12 -4
View File
@@ -133,6 +133,10 @@ bool ShaderValidationEnabled() {
return g_config->shader_validation_enabled;
}
bool ShaderCacheEnabled() {
return g_config->shader_cache_enabled;
}
ShaderOptimizationType GetShaderOptimizationType() {
return g_config->shader_optimization_type;
}
@@ -185,18 +189,22 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
bool ReadbackTiledImagesEnabled() {
return g_config->readback_tiled_images;
}
bool SyncRawImageBuffersEnabled() {
return g_config->sync_raw_image_buffers;
}
bool SupportZmaskEnabled() {
return g_config->support_zmask;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
bool OcclusionQueriesEnabled() {
return g_config->occlusion_queries_enabled;
}
bool TrophyEnabled() {
return g_config->trophy_enabled;
}
+6 -2
View File
@@ -72,6 +72,7 @@ struct ConfigOptions {
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
bool shader_validation_enabled = false;
bool shader_cache_enabled = false;
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::None;
LogDirection shader_log_direction = LogDirection::Silent;
std::filesystem::path shader_log_folder = "_Shaders";
@@ -85,9 +86,10 @@ struct ConfigOptions {
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool readback_tiled_images = false;
bool sync_raw_image_buffers = false;
bool support_zmask = false;
bool tessellation_enabled = false;
bool occlusion_queries_enabled = false;
bool trophy_enabled = true;
uint32_t trophy_duration_seconds = 6;
bool perf_overlay_enabled = false;
@@ -131,6 +133,7 @@ uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
bool ShaderValidationEnabled();
bool ShaderCacheEnabled();
ShaderOptimizationType GetShaderOptimizationType();
LogDirection GetShaderLogDirection();
std::filesystem::path GetShaderLogFolder();
@@ -151,9 +154,10 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool ReadbackTiledImagesEnabled();
bool SyncRawImageBuffersEnabled();
bool SupportZmaskEnabled();
bool TessellationEnabled();
bool OcclusionQueriesEnabled();
bool TrophyEnabled();
uint32_t GetTrophyDurationSeconds();
bool PlayGoHackEnabled();
+8 -2
View File
@@ -51,20 +51,26 @@ inline constexpr auto BrightWhite = fmt::fg(fmt::terminal_color::bright_white)
} // namespace Color
// Out of line, so callers on small guest stacks (fibers) do not reserve the formatting frame.
template <typename... Args>
[[gnu::noinline]] void Printf(fmt::text_style style, const char* format, const Args&... args) {
Write(style, fmt::sprintf(format, args...));
}
} // namespace Log
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define LOGF(...) \
do { \
if (!::Log::IsSilent()) { \
::Log::Write(::fmt::sprintf(__VA_ARGS__)); \
::Log::Printf({}, __VA_ARGS__); \
} \
} while (false)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define LOGF_COLOR(style, ...) \
do { \
if (!::Log::IsSilent()) { \
::Log::Write((style), ::fmt::sprintf(__VA_ARGS__)); \
::Log::Printf((style), __VA_ARGS__); \
} \
} while (false)
+1 -1
View File
@@ -367,7 +367,7 @@ bool SysFileDeleteFile(const std::filesystem::path& name) {
bool SysFileFlush(sys_file_t& f) {
if (f.type == SYS_FILE_FILE && f.f != nullptr) {
return (fflush(f.f) == 0);
return fflush(f.f) == 0 && fsync(fileno(f.f)) == 0;
}
return false;
+52 -52
View File
@@ -1,15 +1,15 @@
#ifndef EMULATOR_SRC_COMMON_SLOTVECTOR_H_
#define EMULATOR_SRC_COMMON_SLOTVECTOR_H_
#include "common/assert.h"
#include "common/abi.h"
#include "common/common.h"
#include <compare>
#include <cstdint>
#include <deque>
#include <functional>
#include <limits>
#include <optional>
#include <memory>
#include <new>
#include <numeric>
#include <utility>
#include <vector>
@@ -19,105 +19,105 @@ struct SlotId {
static constexpr uint32_t INVALID_INDEX = std::numeric_limits<uint32_t>::max();
constexpr SlotId() noexcept = default;
constexpr SlotId(uint32_t value) noexcept: index(value), generation(1) {}
constexpr SlotId(uint32_t value, uint32_t slot_generation) noexcept
: index(value), generation(slot_generation) {}
constexpr SlotId(uint32_t index) noexcept: index(index) {}
[[nodiscard]] constexpr explicit operator bool() const noexcept { return index != INVALID_INDEX; }
[[nodiscard]] constexpr explicit operator bool() const noexcept {
return index != INVALID_INDEX;
}
constexpr auto operator<=>(const SlotId&) const noexcept = default;
uint32_t index = INVALID_INDEX;
uint32_t generation = 0;
};
// Stable-address slot storage for cache resources that are intentionally non-movable.
// Fixed-size chunks keep the stored, non-movable resources in place. Occupancy is a byte per
// slot, not a bitset: a deferred erase runs while fault threads read other slots.
template <typename T>
class SlotVector {
static constexpr uint32_t ChunkSize = 256;
public:
SlotVector() = default;
KYTY_CLASS_NO_COPY(SlotVector);
~SlotVector() noexcept {
ForEach([](SlotId, T& value) { value.~T(); });
}
[[nodiscard]] T& operator[](SlotId id) noexcept {
EXIT_IF(!is_allocated(id));
return *m_values[id.index].value;
return m_chunks[id.index / ChunkSize][id.index % ChunkSize].object;
}
[[nodiscard]] const T& operator[](SlotId id) const noexcept {
EXIT_IF(!is_allocated(id));
return *m_values[id.index].value;
return m_chunks[id.index / ChunkSize][id.index % ChunkSize].object;
}
[[nodiscard]] T* try_get(SlotId id) noexcept {
return is_allocated(id) ? &*m_values[id.index].value : nullptr;
return is_allocated(id) ? &(*this)[id] : nullptr;
}
[[nodiscard]] const T* try_get(SlotId id) const noexcept {
return is_allocated(id) ? &*m_values[id.index].value : nullptr;
return is_allocated(id) ? &(*this)[id] : nullptr;
}
[[nodiscard]] bool is_allocated(SlotId id) const noexcept {
return id && id.index < m_values.size() &&
m_values[id.index].generation == id.generation && m_values[id.index].value.has_value();
return id.index < m_stored.size() && m_stored[id.index] != 0;
}
template <typename... Args>
[[nodiscard]] SlotId insert(Args&&... args) {
uint32_t index = 0;
if (m_free_list.empty()) {
index = static_cast<uint32_t>(m_values.size());
m_values.emplace_back();
m_values.back().value.emplace(std::forward<Args>(args)...);
} else {
index = m_free_list.back();
m_free_list.pop_back();
EXIT_IF(m_values[index].value.has_value());
m_values[index].value.emplace(std::forward<Args>(args)...);
Reserve();
}
++m_size;
return SlotId {index, m_values[index].generation};
const uint32_t index = m_free_list.back();
m_free_list.pop_back();
new (&(*this)[index]) T(std::forward<Args>(args)...);
m_stored[index] = 1;
return SlotId {index};
}
void erase(SlotId id) noexcept {
EXIT_IF(!is_allocated(id));
auto& slot = m_values[id.index];
slot.value.reset();
if (++slot.generation == 0) {
slot.generation = 1;
}
(*this)[id].~T();
m_stored[id.index] = 0;
m_free_list.push_back(id.index);
--m_size;
}
[[nodiscard]] size_t size() const noexcept { return m_size; }
[[nodiscard]] size_t capacity() const noexcept { return m_values.size(); }
template <typename F>
void ForEach(F&& fn) {
for (uint32_t index = 0; index < m_values.size(); ++index) {
if (m_values[index].value) {
fn(SlotId {index, m_values[index].generation}, *m_values[index].value);
for (uint32_t index = 0; index < m_stored.size(); ++index) {
if (m_stored[index] != 0) {
fn(SlotId {index}, (*this)[index]);
}
}
}
template <typename F>
void ForEach(F&& fn) const {
for (uint32_t index = 0; index < m_values.size(); ++index) {
if (m_values[index].value) {
fn(SlotId {index, m_values[index].generation}, *m_values[index].value);
for (uint32_t index = 0; index < m_stored.size(); ++index) {
if (m_stored[index] != 0) {
fn(SlotId {index}, (*this)[index]);
}
}
}
private:
struct Slot {
std::optional<T> value;
uint32_t generation = 1;
union Slot {
Slot() noexcept {}
~Slot() noexcept {}
T object;
};
std::deque<Slot> m_values;
std::vector<uint32_t> m_free_list;
size_t m_size = 0;
void Reserve() {
const auto first = static_cast<uint32_t>(m_stored.size());
m_chunks.push_back(std::make_unique<Slot[]>(ChunkSize));
m_stored.resize(first + ChunkSize);
m_free_list.resize(ChunkSize);
std::iota(m_free_list.rbegin(), m_free_list.rend(), first);
}
std::vector<std::unique_ptr<Slot[]>> m_chunks;
std::vector<uint8_t> m_stored;
std::vector<uint32_t> m_free_list;
};
} // namespace Common
@@ -125,7 +125,7 @@ private:
template <>
struct std::hash<Common::SlotId> {
[[nodiscard]] size_t operator()(Common::SlotId id) const noexcept {
return std::hash<uint64_t> {}((static_cast<uint64_t>(id.generation) << 32u) | id.index);
return std::hash<uint32_t> {}(id.index);
}
};
+1 -43
View File
@@ -89,42 +89,6 @@ static void MountSandboxDirs() {
MountOrCreateDir("_TempData/" + title_id, "/temp");
}
static bool ClearDirectoryContents(const std::filesystem::path& dir) {
bool ok = true;
for (const auto& entry: Common::File::GetDirEntries(dir)) {
if (entry.name == "." || entry.name == "..") {
continue;
}
auto path = dir / entry.name;
if (entry.is_file) {
Common::File::RemoveReadonly(path);
ok = Common::File::DeleteFile(path) && ok;
} else {
ok = ClearDirectoryContents(path) && ok;
ok = Common::File::DeleteDirectory(path) && ok;
}
}
return ok;
}
static void ClearDebugTextureFolder() {
const std::string debug_texture_folder = "_Textures";
if (!Common::File::IsDirectoryExisting(debug_texture_folder)) {
Common::File::CreateDirectories(debug_texture_folder);
return;
}
if (!ClearDirectoryContents(debug_texture_folder)) {
LOGF_COLOR(Log::Color::BrightYellow, "TextureDump: failed to completely clear %s\n",
debug_texture_folder.c_str());
}
}
static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path& param_json,
Common::Subsystems& subsystems) {
EXIT_IF(!Common::Thread::IsMainThread());
@@ -135,10 +99,6 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
if (Common::File::IsFileExisting(param_json)) {
Loader::SystemContentLoadParamSfo(param_json);
if (const auto flexible_memory_size = Loader::SystemContentGetFlexibleMemorySize();
flexible_memory_size != 0) {
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
}
}
if (cfg.splash_enabled) {
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
@@ -198,8 +158,6 @@ void Run(const RunOptions& options) {
Common::Subsystems subsystems(true);
Init(options.config, param_json, subsystems);
ClearDebugTextureFolder();
PrintSystemInfo();
std::string title_id;
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
@@ -226,7 +184,7 @@ void Run(const RunOptions& options) {
MountSandboxDirs();
Loader::AddContentLoad(options.addcont_dir, title_id);
Loader::AddContentLoad(options.addcont_dir, options.addcont, title_id);
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
Libs::InitAll(rt->Symbols());
+7 -5
View File
@@ -1,6 +1,7 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
#define EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
#include "common/additionalContent.h"
#include "common/emulatorConfig.h"
#include "common/stringUtils.h"
@@ -9,11 +10,12 @@
namespace Emulator {
struct RunOptions {
Config::ConfigOptions config;
std::filesystem::path app0_dir;
std::filesystem::path elf;
std::filesystem::path game_patch;
std::filesystem::path addcont_dir;
Config::ConfigOptions config;
std::filesystem::path app0_dir;
std::filesystem::path elf;
std::filesystem::path game_patch;
std::filesystem::path addcont_dir;
std::vector<Common::AddContentEntry> addcont;
};
void Run(const RunOptions& options);
@@ -27,6 +27,7 @@ struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
uint32_t draw_index;
};
class Pm4Execution {
@@ -50,13 +51,6 @@ private:
class CommandProcessor {
public:
struct FlipInfo {
int handle = 0;
int index = 0;
int flip_mode = 0;
int64_t flip_arg = 0;
};
CommandProcessor(RenderContext& renderer, int interrupt_event_id)
: m_renderer(renderer), m_interrupt_event_id(interrupt_event_id) {}
~CommandProcessor() = default;
@@ -90,44 +84,21 @@ public:
void DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr, uint32_t stride_in_bytes,
IndirectDrawRegisters registers, uint32_t draw_initiator, bool indexed);
void WriteAtEndOfPipe32(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action, uint32_t event_index,
uint32_t event_write_source, void* dst_gpu_addr, uint32_t value,
uint32_t interrupt_selector, uint32_t interrupt_context_id = 0);
void WriteAtEndOfPipe64(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action, uint32_t event_index,
uint32_t event_write_source, void* dst_gpu_addr, uint64_t value,
uint32_t interrupt_selector, uint32_t interrupt_context_id = 0);
void Flip();
void Flip(void* dst_gpu_addr, uint32_t value);
void FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache_action, void* dst_gpu_addr,
uint32_t value);
void ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector, void* address, uint64_t data,
uint32_t interrupt_context_id);
void Flip(int handle, int index, int flip_mode, int64_t flip_arg);
void PrepareCpuFlip(uint64_t request_id);
void SynchronizeGpu();
void EmitGlobalBarrier();
void TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id);
void DispatchDirect(uint32_t thread_group_x, uint32_t thread_group_y, uint32_t thread_group_z,
uint32_t mode);
void DispatchIndirect(uint64_t args_addr, uint32_t mode);
void WaitFlipDone(uint32_t video_out_handle, uint32_t display_buffer_index);
void TriggerEvent(uint32_t event_type, uint32_t event_index, uint64_t event_address = 0);
void SetUserDataMarker(HW::UserSgprType type) { m_user_data_marker = type; }
[[nodiscard]] HW::UserSgprType GetUserDataMarker() const { return m_user_data_marker; }
void ResetDeCe();
void SetCeComplete(bool complete) { m_ce_complete = complete; }
void WaitCe();
void WaitDeDiff(uint32_t diff);
void WaitForRewind(bool valid);
void IncrementDe();
void IncrementCe();
void WriteConstRam(uint32_t offset, const uint32_t* src, uint32_t dw_num);
void DumpConstRam(uint32_t* dst, uint32_t offset, uint32_t dw_num);
template <typename T>
void WaitRegMem(uint32_t func, const T* addr, T ref, T mask, uint32_t poll, uint32_t wait_op);
void MemSemaphore(uint64_t* counter, bool signal, bool write);
void WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num, uint32_t write_control);
void WriteReferenceClock(uint64_t dst_address, uint32_t num_bytes);
void DmaData(uint8_t engine, uint8_t dst_sel, uint8_t dst_cache_policy,
@@ -141,45 +112,34 @@ public:
Pm4ProcessResult Process(Pm4Execution& execution, std::span<const uint32_t> commands);
void ProcessIndirectBuffer(std::span<const uint32_t> commands, bool chain);
void SetFlip(const FlipInfo& flip) { m_flip = flip; }
[[nodiscard]] uint64_t GetSubmitId() const { return m_submit_id; }
void SetSubmitId(uint64_t submit_id) { m_submit_id = submit_id; }
[[nodiscard]] bool IsAsyncComputeQueue() const { return m_interrupt_event_id >= 0x20; }
private:
template <typename T>
void WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_write_dest, uint32_t eop_event_type,
uint32_t cache_action, uint32_t event_index, uint32_t event_write_source,
void* dst_gpu_addr, T value, uint32_t interrupt_selector,
uint32_t interrupt_context_id);
void ProcessPm4(Pm4Execution& execution);
void SuspendPm4();
uint32_t NumInstances();
const void* IndexAddress(uint32_t first_index) const;
CommandScheduler& GetScheduler() const { return m_renderer.GetCommandScheduler(); }
CommandBuffer& CurrentBuffer() { return GetScheduler().Current(); }
RenderContext& m_renderer;
HW::Context m_ctx;
HW::Context m_saved_ctx;
bool m_context_state_pushed = false;
HW::UserConfig m_ucfg;
HW::Shader m_sh_ctx;
HW::UserSgprType m_user_data_marker = HW::UserSgprType::Unknown;
uint32_t m_index_type_and_size = 0;
uint32_t m_index_buffer_size = 0;
uint64_t m_index_base_addr = 0;
uint64_t m_draw_indirect_args_base_addr = 0;
uint64_t m_dispatch_indirect_args_base_addr = 0;
// Persistent draw state: indirect draws update it for subsequent draws.
uint32_t m_num_instances = 1;
RenderContext& m_renderer;
HW::Context m_ctx;
HW::Context m_saved_ctx;
bool m_context_state_pushed = false;
HW::UserConfig m_ucfg;
HW::Shader m_sh_ctx;
uint32_t m_index_type_and_size = 0;
uint32_t m_index_buffer_size = 0;
uint64_t m_index_base_addr = 0;
uint64_t m_draw_indirect_args_base_addr = 0;
uint64_t m_dispatch_indirect_args_base_addr = 0;
// Persistent draw state. Indirect draws leave the address of their last instance count,
// read only when a later draw inherits it.
uint32_t m_num_instances = 1;
uint64_t m_num_instances_addr = 0;
uint32_t m_de_count = 0;
uint32_t m_ce_count = 0;
bool m_ce_complete = false;
uint32_t m_const_ram[0x3000] = {0};
FlipInfo m_flip;
const int m_interrupt_event_id;
uint64_t m_submit_id = 0;
uint64_t m_synthetic_occlusion_counter = 0;
@@ -73,7 +73,6 @@ constexpr auto MakeContextDispatchTable() {
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_0] = HwCtxSetPaScExtendedControl;
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_1] = HwCtxSetPaScExtendedControl;
g_hw_ctx_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] = HwCtxSetPaScExtendedControl;
g_hw_ctx_func[Pm4::DB_ALPHA_TO_MASK] = HwCtxSetAlphaToMask;
g_hw_ctx_func[Pm4::VGT_DRAW_PAYLOAD_CNTL] = HwCtxSetDrawPayloadControl;
g_hw_ctx_func[Pm4::VGT_PRIMITIVEID_RESET] = HwCtxSetPrimitiveIdReset;
g_hw_ctx_func[Pm4::PA_CL_OBJPRIM_ID_CNTL] = HwCtxSetObjprimIdControl;
@@ -200,10 +199,10 @@ constexpr auto MakeOpcodeDispatchTable() {
g_cp_op_func[Pm4::IT_INDIRECT_BUFFER] = CpOpIndirectBuffer;
g_cp_op_func[Pm4::IT_COPY_DATA] = CpOpCopyData;
g_cp_op_func[Pm4::IT_EVENT_WRITE] = CpOpEventWrite;
g_cp_op_func[Pm4::IT_EVENT_WRITE_EOP] = CpOpEventWriteEop;
g_cp_op_func[Pm4::IT_EVENT_WRITE_EOS] = CpOpEventWriteEos;
g_cp_op_func[Pm4::IT_DMA_DATA] = CpOpDmaData;
g_cp_op_func[Pm4::IT_ACQUIRE_MEM] = CpOpAcquireMem;
g_cp_op_func[Pm4::IT_RELEASE_MEM] = CpOpReleaseMem;
g_cp_op_func[Pm4::IT_MEM_SEMAPHORE] = CpOpMemSemaphore;
g_cp_op_func[Pm4::IT_REWIND] = CpOpRewind;
g_cp_op_func[Pm4::IT_SET_CONTEXT_REG] = CpOpSetContextReg;
g_cp_op_func[Pm4::IT_SET_SH_REG] = CpOpSetShaderReg;
@@ -212,12 +211,6 @@ constexpr auto MakeOpcodeDispatchTable() {
g_cp_op_func[Pm4::IT_SET_CONTEXT_REG_INDIRECT] = CpOpIndirectCxRegs;
g_cp_op_func[Pm4::IT_SET_SH_REG_INDIRECT] = CpOpIndirectShRegs;
g_cp_op_func[Pm4::IT_SET_UCONFIG_REG_INDIRECT] = CpOpIndirectUcRegs;
g_cp_op_func[Pm4::IT_WRITE_CONST_RAM] = CpOpWriteConstRam;
g_cp_op_func[Pm4::IT_DUMP_CONST_RAM] = CpOpDumpConstRam;
g_cp_op_func[Pm4::IT_INCREMENT_CE_COUNTER] = CpOpIncrementCeCounter;
g_cp_op_func[Pm4::IT_INCREMENT_DE_COUNTER] = CpOpIncrementDeCounter;
g_cp_op_func[Pm4::IT_WAIT_ON_CE_COUNTER] = CpOpWaitOnCeCounter;
g_cp_op_func[Pm4::IT_WAIT_ON_DE_COUNTER_DIFF] = CpOpWaitOnDeCounterDiff;
g_cp_op_func[Pm4::IT_GET_LOD_STATS] = CpOpGetLodStats;
g_cp_op_func[Pm4::IT_WAIT_REG_MEM_64] = CpOpWaitRegMem64;
@@ -232,7 +225,6 @@ constexpr auto MakeCustomOpcodeDispatchTable() {
g_cp_op_custom_func[Pm4::R_PUSH_MARKER] = CpOpPushMarker;
g_cp_op_custom_func[Pm4::R_POP_MARKER] = CpOpPopMarker;
g_cp_op_custom_func[Pm4::R_FLIP] = CpOpFlip;
g_cp_op_custom_func[Pm4::R_RELEASE_MEM] = CpOpReleaseMem;
g_cp_op_custom_func[Pm4::R_CONTEXT_STATE] = CpOpContextState;
return g_cp_op_custom_func;
@@ -79,7 +79,6 @@ uint32_t HwCtxSetAaMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
uint32_t HwCtxSetVtxControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
uint32_t HwCtxSetPaScExtendedControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
uint32_t);
uint32_t HwCtxSetAlphaToMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
uint32_t HwCtxSetDrawPayloadControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
uint32_t);
uint32_t HwCtxSetPrimitiveIdReset(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
@@ -134,22 +133,16 @@ uint32_t CpOpWriteData(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, u
uint32_t CpOpIndirectBuffer(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpCopyData(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpEventWrite(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpEventWriteEop(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpEventWriteEos(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpDmaData(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpAcquireMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpReleaseMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpMemSemaphore(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetContextReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetShaderReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetUconfigReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIndirectCxRegs(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIndirectShRegs(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIndirectUcRegs(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpWriteConstRam(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpDumpConstRam(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIncrementCeCounter(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIncrementDeCounter(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpWaitOnCeCounter(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpWaitOnDeCounterDiff(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpGetLodStats(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpDispatchReset(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpWaitRegMem32(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
@@ -158,7 +151,6 @@ uint32_t CpOpPushMarker(CommandProcessor&, uint32_t, const uint32_t*, uint32_t,
uint32_t CpOpPopMarker(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpWaitRegMem64(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpFlip(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpReleaseMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
} // namespace Libs::Graphics
@@ -54,40 +54,12 @@ namespace Libs::Graphics {
namespace {
constexpr uint32_t GcrGl2MetadataInvalidate = 1u << 1u;
constexpr uint32_t GcrGl0VectorInvalidate = 1u << 2u;
constexpr uint32_t GcrGl1Invalidate = 1u << 3u;
constexpr uint32_t GcrGl2Unshared = 1u << 4u;
constexpr uint32_t GcrGl2Invalidate = 1u << 8u;
constexpr uint32_t GcrGl2Writeback = 1u << 9u;
constexpr uint32_t GcrOrder012 = 1u << 10u;
constexpr uint32_t GcrOrder210 = 2u << 10u;
constexpr uint32_t GcrKnownMask = GcrGl2MetadataInvalidate | GcrGl0VectorInvalidate |
GcrGl1Invalidate | GcrGl2Unshared | GcrGl2Invalidate |
GcrGl2Writeback | GcrOrder012 | GcrOrder210;
constexpr uint32_t RegisterSelectorMask = 0x70000000u;
constexpr uint32_t RegisterSelectorMask = 0x70000000u;
constexpr uint32_t NormalizeRegisterOffset(uint32_t raw_offset) {
return raw_offset & ~RegisterSelectorMask;
}
bool ReleaseMemGcrNeedsBarrier(uint32_t eop_event_type, uint32_t gcr_cntl) {
return eop_event_type != 0x28u ||
(gcr_cntl & (GcrGl2MetadataInvalidate | GcrGl0VectorInvalidate | GcrGl1Invalidate |
GcrGl2Invalidate | GcrGl2Writeback)) != 0;
}
uint32_t ReleaseMemCacheActionFromGcr(uint32_t gcr_cntl) {
return ((gcr_cntl & GcrGl2Writeback) != 0 ? 0x38u : 0x00u);
}
void LogUnknownReleaseMemGcr(uint32_t gcr_cntl) {
const uint32_t unknown = (gcr_cntl & ~GcrKnownMask);
if (unknown != 0) {
LOGF("\t warning: release_mem uses unknown GCR bits: 0x%04" PRIx32 "\n", unknown);
}
}
} // namespace
static HW::BlendControl DecodeBlendControl(uint32_t value) {
@@ -289,12 +261,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetCentroidPriority) {
static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
[[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreAlphaToMaskRegister([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t value) {
EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0);
}
static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {}
@@ -439,16 +405,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetPointState) {
return num_values;
}
KYTY_HW_CTX_PARSER(HwCtxSetAlphaToMask) {
auto num_values = KYTY_PM4_LEN(cmd_id) - 2u;
for (uint32_t i = 0; i < num_values; i++) {
HwCtxIgnoreAlphaToMaskRegister(buffer[i]);
}
return num_values;
}
KYTY_HW_CTX_PARSER(HwCtxSetDrawPayloadControl) {
cp.GetCtx().SetDrawPayloadControl(buffer[0]);
return KYTY_PM4_LEN(cmd_id) - 2u;
@@ -1090,9 +1046,8 @@ KYTY_HW_SH_PARSER(HwShSetCsUserSgpr) {
auto write_num = UserSgprWriteNum(slot, reg_num, 16u, "cs");
for (uint32_t i = 0; i < write_num; i++) {
cp.GetShCtx().SetCsUserSgpr(slot + i, buffer[i], cp.GetUserDataMarker());
cp.GetShCtx().SetCsUserSgpr(slot + i, buffer[i]);
}
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
return reg_num;
}
@@ -1107,9 +1062,8 @@ KYTY_HW_SH_PARSER(HwShSetPsUserSgpr) {
auto write_num = UserSgprWriteNum(slot, reg_num, 32u, "ps");
for (uint32_t i = 0; i < write_num; i++) {
cp.GetShCtx().SetPsUserSgpr(slot + i, buffer[i], cp.GetUserDataMarker());
cp.GetShCtx().SetPsUserSgpr(slot + i, buffer[i]);
}
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
return reg_num;
}
@@ -1124,9 +1078,8 @@ KYTY_HW_SH_PARSER(HwShSetGsUserSgpr) {
auto write_num = UserSgprWriteNum(slot, reg_num, 32u, "gs");
for (uint32_t i = 0; i < write_num; i++) {
cp.GetShCtx().SetGsUserSgpr(slot + i, buffer[i], cp.GetUserDataMarker());
cp.GetShCtx().SetGsUserSgpr(slot + i, buffer[i]);
}
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
return reg_num;
}
@@ -1141,9 +1094,8 @@ KYTY_HW_SH_PARSER(HwShSetHsUserSgpr) {
auto write_num = UserSgprWriteNum(slot, reg_num, 32u, "hs");
for (uint32_t i = 0; i < write_num; i++) {
cp.GetShCtx().SetHsUserSgpr(slot + i, buffer[i], cp.GetUserDataMarker());
cp.GetShCtx().SetHsUserSgpr(slot + i, buffer[i]);
}
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
return reg_num;
}
@@ -1630,7 +1582,7 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
// The native indexed packet enables the high-half first-index destination in word 3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP, Pm4::SH_NOP};
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
@@ -1650,10 +1602,12 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
const auto stride_in_bytes = buffer[7];
const auto draw_initiator = buffer[8];
const bool indexed = (cmd_id == 0xc0083800);
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3. Word 4
// bit 31 enables the subdraw index, written to the register in its low half.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP,
(buffer[3] >> 31u) != 0 ? buffer[3] & 0xffffu : Pm4::SH_NOP};
if (count_indirect == 0) {
count_addr = nullptr;
@@ -1722,24 +1676,6 @@ KYTY_CP_OP_PARSER(CpOpContextState) {
return packet_size_dw - 1u;
}
KYTY_CP_OP_PARSER(CpOpDumpConstRam) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0038300);
auto offset = buffer[0];
auto dw_num = buffer[1];
auto* dst = reinterpret_cast<uint32_t*>(buffer[2] | (static_cast<uint64_t>(buffer[3]) << 32u));
EXIT_NOT_IMPLEMENTED(dw_num >= 0x3000);
EXIT_NOT_IMPLEMENTED(offset > 0xbffc);
EXIT_NOT_IMPLEMENTED((offset & 0x3u) != 0);
cp.DumpConstRam(dst, offset, dw_num);
return 4;
}
KYTY_CP_OP_PARSER(CpOpEventWrite) {
KYTY_PROFILER_FUNCTION();
@@ -1760,90 +1696,16 @@ KYTY_CP_OP_PARSER(CpOpEventWrite) {
return packet_size_dw - 1u;
}
KYTY_CP_OP_PARSER(CpOpEventWriteEop) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0044700);
uint32_t cache_policy = (buffer[0] >> 25u) & 0x3u;
uint32_t event_write_dest = ((buffer[0] >> 23u) & 0x10u) | ((buffer[2] >> 16u) & 0x01u);
uint32_t eop_event_type = (buffer[0]) & 0x3fu;
uint32_t cache_action = (buffer[0] >> 12u) & 0x3fu;
uint32_t event_index = (buffer[0] >> 8u) & 0x7u;
uint32_t event_write_source = ((buffer[2] >> 29u) & 0x7u);
uint32_t interrupt_selector = (buffer[2] >> 24u) & 0x7u;
auto* dst_gpu_addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2] & 0xffffu) << 32u));
uint64_t value = (buffer[3] | (static_cast<uint64_t>(buffer[4]) << 32u));
cp.WriteAtEndOfPipe64(cache_policy, event_write_dest, eop_event_type, cache_action, event_index,
event_write_source, dst_gpu_addr, value, interrupt_selector);
return 5;
}
KYTY_CP_OP_PARSER(CpOpEventWriteEos) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0034802);
uint32_t cache_policy = (buffer[0] >> 25u) & 0x3u;
uint32_t event_write_dest = 0;
uint32_t eop_event_type = (buffer[0]) & 0x3fu;
uint32_t cache_action = (buffer[0] >> 12u) & 0x3fu;
uint32_t event_index = (buffer[0] >> 8u) & 0x7u;
uint32_t event_write_source = ((buffer[2] >> 29u) & 0x7u);
uint32_t interrupt_selector = (buffer[2] >> 24u) & 0x7u;
auto* dst_gpu_addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2] & 0xffffu) << 32u));
uint32_t value = buffer[3];
cp.WriteAtEndOfPipe32(cache_policy, event_write_dest, eop_event_type, cache_action, event_index,
event_write_source, dst_gpu_addr, value, interrupt_selector);
return 4;
}
KYTY_CP_OP_PARSER(CpOpFlip) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc004105c);
CommandProcessor::FlipInfo f;
f.handle = static_cast<int>(buffer[0]);
f.index = static_cast<int>(buffer[1]);
f.flip_mode = static_cast<int>(buffer[2]);
f.flip_arg = static_cast<int64_t>(buffer[3] | (static_cast<uint64_t>(buffer[4]) << 32u));
cp.SetFlip(f);
cp.Flip();
cp.Flip(static_cast<int>(buffer[0]), static_cast<int>(buffer[1]), static_cast<int>(buffer[2]),
static_cast<int64_t>(buffer[3] | (static_cast<uint64_t>(buffer[4]) << 32u)));
return 5;
}
KYTY_CP_OP_PARSER(CpOpIncrementCeCounter) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0008400);
EXIT_NOT_IMPLEMENTED(buffer[0] != 1);
cp.IncrementCe();
return 1;
}
KYTY_CP_OP_PARSER(CpOpIncrementDeCounter) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0008500);
EXIT_NOT_IMPLEMENTED(buffer[0] != 0);
cp.IncrementDe();
return 1;
}
KYTY_CP_OP_PARSER(CpOpIndexType) {
KYTY_PROFILER_FUNCTION();
@@ -2125,50 +1987,12 @@ KYTY_CP_OP_PARSER(CpOpIndirectUcRegs) {
return KYTY_PM4_LEN(cmd_id) - 1u;
}
KYTY_CP_OP_PARSER(CpOpMarker) {
KYTY_PROFILER_FUNCTION();
uint32_t len_dw = ((cmd_id >> 16u) & 0x3fffu);
switch (buffer[0]) {
case 0x68750004: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
case 0x6875000d: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
case 0x68750777: {
cp.Flip();
break;
}
case 0x68750778: {
auto* addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
uint32_t value = buffer[3];
cp.Flip(addr, value);
break;
}
case 0x68750781: {
auto* addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
uint32_t value = buffer[3];
uint32_t eop_event_type = buffer[4];
uint32_t cache_action = buffer[5];
cp.FlipWithInterrupt(eop_event_type, cache_action, addr, value);
break;
}
// NOP payloads are caller-owned data; only complete known marker tags execute.
default: break;
}
return len_dw + 1;
}
KYTY_CP_OP_PARSER(CpOpNop) {
KYTY_PROFILER_FUNCTION();
auto r = KYTY_PM4_R(cmd_id);
if (r == Pm4::R_ZERO) {
if ((buffer[0] & 0xffff0000u) == 0x68750000u) {
return CpOpMarker(cp, cmd_id, buffer, dw, num_dw);
}
return KYTY_PM4_LEN(cmd_id) - 1;
}
@@ -2224,128 +2048,33 @@ KYTY_CP_OP_PARSER(CpOpPushMarker) {
KYTY_CP_OP_PARSER(CpOpReleaseMem) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0061060);
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0064900);
uint32_t cache_policy = (buffer[0] >> 25u) & 0x3u;
uint32_t eop_event_type = buffer[0] & 0x3fu;
uint32_t event_index = (buffer[0] >> 8u) & 0x7u;
uint32_t gcr_cntl = (buffer[0] >> 12u) & 0xfffu;
uint32_t release_dst = (buffer[1] >> 16u) & 0x3u;
uint32_t event_write_dest = 0;
uint32_t data_sel = (buffer[1] >> 29u) & 0x7u;
uint32_t interrupt_selector = (buffer[1] >> 24u) & 0x7u;
auto* dst_gpu_addr =
reinterpret_cast<void*>(buffer[2] | (static_cast<uint64_t>(buffer[3]) << 32u));
uint64_t value = buffer[4] | (static_cast<uint64_t>(buffer[5]) << 32u);
uint32_t interrupt_context_id = buffer[6] & 0x07ffffffu;
constexpr uint32_t ReleaseMemDstMemory = 0;
constexpr uint32_t ReleaseMemDstTcL2 = 1;
EXIT_NOT_IMPLEMENTED(release_dst != ReleaseMemDstMemory && release_dst != ReleaseMemDstTcL2);
EXIT_NOT_IMPLEMENTED(data_sel != 0 && data_sel != 1 && data_sel != 2 && data_sel != 3 &&
data_sel != 5);
LogUnknownReleaseMemGcr(gcr_cntl);
const bool gl2_writeback = ((gcr_cntl & GcrGl2Writeback) != 0);
auto trigger_interrupt = [&]() {
switch (interrupt_selector) {
case 0x00:
case 0x03: break;
case 0x01:
case 0x02:
case 0x04:
cp.TriggerEopEventAtEndOfPipe(interrupt_context_id);
cp.BufferFlush();
break;
default: EXIT("unknown release_mem interrupt selector\n");
}
};
if (data_sel == 0 || interrupt_selector == 4) {
if (eop_event_type != 0x28 || gcr_cntl != 0) {
cp.EmitGlobalBarrier();
}
trigger_interrupt();
return 7;
}
if (release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr) {
if (eop_event_type != 0x28 || gcr_cntl != 0) {
cp.EmitGlobalBarrier();
}
trigger_interrupt();
return 7;
}
if (ReleaseMemGcrNeedsBarrier(eop_event_type, gcr_cntl)) {
cp.EmitGlobalBarrier();
}
auto cache_action = ReleaseMemCacheActionFromGcr(gcr_cntl);
cache_policy = 0;
if (data_sel == 1) {
eop_event_type = 0x2f;
event_index = 6;
auto event_source = 2u;
cp.WriteAtEndOfPipe32(cache_policy, event_write_dest, eop_event_type, cache_action,
event_index, event_source, dst_gpu_addr, static_cast<uint32_t>(value),
interrupt_selector, interrupt_context_id);
if (interrupt_selector == 0x01 || interrupt_selector == 0x02) {
cp.BufferFlush();
}
return 7;
}
if (data_sel == 5) {
if (gl2_writeback) {
cache_action = 0;
}
eop_event_type = 0x2f;
event_index = 6;
auto event_source = 1u;
cp.WriteAtEndOfPipe32(cache_policy, event_write_dest, eop_event_type, cache_action,
event_index, event_source, dst_gpu_addr, static_cast<uint32_t>(value),
interrupt_selector, interrupt_context_id);
if (interrupt_selector == 0x01) {
cp.BufferFlush();
}
return 7;
}
const bool keep_native_event_index =
(eop_event_type == 0x04 && cache_action == 0x00) ||
(eop_event_type == 0x2f && event_index == 0x06 && cache_action == 0x38);
if (data_sel == 2) {
if (!keep_native_event_index) {
event_index = 0;
}
} else {
EXIT_IF(data_sel != 3);
if (!keep_native_event_index) {
event_index = 0;
}
data_sel = 4;
}
cp.WriteAtEndOfPipe64(cache_policy, event_write_dest, eop_event_type, cache_action, event_index,
data_sel, dst_gpu_addr, value, interrupt_selector, interrupt_context_id);
// The event type, cache actions and destination cache only order GPU work, which the renderer's
// resource barriers already order.
cp.ReleaseMem(
(buffer[1] >> 29u) & 0x7u, (buffer[1] >> 24u) & 0x7u,
reinterpret_cast<void*>((buffer[2] & ~0x3u) | (static_cast<uint64_t>(buffer[3]) << 32u)),
buffer[4] | (static_cast<uint64_t>(buffer[5]) << 32u), buffer[6] & 0x07ffffffu);
return 7;
}
KYTY_CP_OP_PARSER(CpOpMemSemaphore) {
KYTY_PROFILER_FUNCTION();
const auto control = buffer[2];
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0023900 || (control & ~0xe0110000u) != 0 ||
(control >> 29u) < 6u);
auto* counter = reinterpret_cast<uint64_t*>(
(buffer[0] & ~0x7u) | (static_cast<uint64_t>(buffer[1] & 0x3ffffu) << 32u));
// A signal updates the counter before the next packet, so its mailbox stall never waits.
cp.MemSemaphore(counter, (control >> 29u) == 6u, ((control >> 20u) & 0x1u) != 0);
return 3;
}
KYTY_CP_OP_PARSER(CpOpSetContextReg) {
KYTY_PROFILER_FUNCTION();
@@ -2562,42 +2291,6 @@ KYTY_CP_OP_PARSER(CpOpWaitRegMem64) {
return CpOpWaitRegMemSized<uint64_t>(cp, cmd_id, buffer);
}
KYTY_CP_OP_PARSER(CpOpWaitOnCeCounter) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0008600);
EXIT_NOT_IMPLEMENTED(buffer[0] != 1);
cp.WaitCe();
return 1;
}
KYTY_CP_OP_PARSER(CpOpWaitOnDeCounterDiff) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0008800);
cp.WaitDeDiff(buffer[0]);
return 1;
}
KYTY_CP_OP_PARSER(CpOpWriteConstRam) {
KYTY_PROFILER_FUNCTION();
auto dw_num = (cmd_id >> 16u) & 0x3fffu;
auto offset = buffer[0];
EXIT_NOT_IMPLEMENTED(dw_num >= 0x3000);
EXIT_NOT_IMPLEMENTED(offset > 0xbffc);
EXIT_NOT_IMPLEMENTED((offset & 0x3u) != 0);
cp.WriteConstRam(offset, buffer + 1, dw_num);
return 1 + dw_num;
}
KYTY_CP_OP_PARSER(CpOpWriteData) {
KYTY_PROFILER_FUNCTION();
@@ -2899,7 +2592,7 @@ void GraphicsInitJmpTablesCxIndirect() {
for (auto cmd_offset = Pm4::PA_CL_UCP_0_X; cmd_offset <= Pm4::PA_CL_UCP_5_W; cmd_offset++) {
g_hw_ctx_indirect_func[cmd_offset] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDisabledUserClipPlane(cp, value);
cp.GetCtx().SetUserClipPlane(cmd_offset - Pm4::PA_CL_UCP_0_X, value);
};
}
@@ -3491,7 +3184,8 @@ void GraphicsInitJmpTablesCxIndirect() {
g_hw_ctx_indirect_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] =
[](KYTY_HW_CTX_INDIRECT_ARGS) { HwCtxIgnorePaScExtendedControl(cmd_offset, value); };
g_hw_ctx_indirect_func[Pm4::DB_ALPHA_TO_MASK] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreAlphaToMaskRegister(value);
cp.GetCtx().SetAlphaToMaskEnable(
KYTY_PM4_GET(value, DB_ALPHA_TO_MASK, ALPHA_TO_MASK_ENABLE) != 0);
};
g_hw_ctx_indirect_func[Pm4::CB_COLOR_CONTROL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
cp.GetCtx().SetColorControl(DecodeColorControl(value));
@@ -3556,9 +3250,7 @@ void GraphicsInitJmpTablesShIndirect() {
for (uint32_t slot = 0; slot < 16; slot++) {
g_hw_sh_indirect_func[Pm4::COMPUTE_USER_DATA_0 + slot] = [](KYTY_HW_SH_INDIRECT_ARGS) {
auto sgpr = cmd_offset - Pm4::COMPUTE_USER_DATA_0;
cp.GetShCtx().SetCsUserSgpr(sgpr, value, cp.GetUserDataMarker());
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
cp.GetShCtx().SetCsUserSgpr(cmd_offset - Pm4::COMPUTE_USER_DATA_0, value);
};
}
for (uint32_t slot = 0; slot < 4; slot++) {
@@ -3569,21 +3261,15 @@ void GraphicsInitJmpTablesShIndirect() {
for (uint32_t slot = 0; slot < 32; slot++) {
g_hw_sh_indirect_func[Pm4::SPI_SHADER_USER_DATA_PS_0 + slot] =
[](KYTY_HW_SH_INDIRECT_ARGS) {
auto sgpr = cmd_offset - Pm4::SPI_SHADER_USER_DATA_PS_0;
cp.GetShCtx().SetPsUserSgpr(sgpr, value, cp.GetUserDataMarker());
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
cp.GetShCtx().SetPsUserSgpr(cmd_offset - Pm4::SPI_SHADER_USER_DATA_PS_0, value);
};
g_hw_sh_indirect_func[Pm4::SPI_SHADER_USER_DATA_GS_0 + slot] =
[](KYTY_HW_SH_INDIRECT_ARGS) {
auto sgpr = cmd_offset - Pm4::SPI_SHADER_USER_DATA_GS_0;
cp.GetShCtx().SetGsUserSgpr(sgpr, value, cp.GetUserDataMarker());
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
cp.GetShCtx().SetGsUserSgpr(cmd_offset - Pm4::SPI_SHADER_USER_DATA_GS_0, value);
};
g_hw_sh_indirect_func[Pm4::SPI_SHADER_USER_DATA_HS_0 + slot] =
[](KYTY_HW_SH_INDIRECT_ARGS) {
auto sgpr = cmd_offset - Pm4::SPI_SHADER_USER_DATA_HS_0;
cp.GetShCtx().SetHsUserSgpr(sgpr, value, cp.GetUserDataMarker());
cp.SetUserDataMarker(HW::UserSgprType::Unknown);
cp.GetShCtx().SetHsUserSgpr(cmd_offset - Pm4::SPI_SHADER_USER_DATA_HS_0, value);
};
}
for (uint32_t slot = 0; slot < 4; slot++) {
+215 -480
View File
@@ -30,6 +30,7 @@
#include <mutex>
#include <semaphore>
#include <thread>
#include <utility>
#include <vector>
namespace Libs::Graphics {
@@ -39,21 +40,6 @@ static thread_local Pm4Execution* g_current_execution = nullptr;
static thread_local bool g_gpu_thread = false;
static thread_local GuestGpu* g_gpu_state = nullptr;
struct DrawIndirectArgs {
uint32_t vertex_count_per_instance;
uint32_t instance_count;
uint32_t start_vertex_location;
uint32_t start_instance_location;
};
struct DrawIndexedIndirectArgs {
uint32_t index_count_per_instance;
uint32_t instance_count;
uint32_t start_index_location;
uint32_t base_vertex_location;
uint32_t start_instance_location;
};
static bool GraphicsRunDebugDumpEnabled() {
return Config::GraphicsDebugDumpEnabled() &&
Config::GetPrintfDirection() != Config::LogDirection::Silent;
@@ -106,6 +92,12 @@ void GuestGpu::SendCommand(Common::UniqueFunction<void>&& command) {
m_work_available.Signal();
}
void GuestGpu::Wake() {
Common::LockGuard lock(m_queue_mutex);
m_wake_pending = true;
m_work_available.Signal();
}
void GuestGpu::ProcessCommands() {
EXIT_IF(!IsGpuThread());
while (m_pending_commands.load(std::memory_order_acquire) != 0) {
@@ -135,16 +127,14 @@ void GuestGpu::SendCommandSync(Common::UniqueFunction<void>&& command) {
done.acquire();
}
void GuestGpu::Submit(std::span<const uint32_t> draw_commands,
std::span<const uint32_t> constant_commands) {
if (draw_commands.empty()) {
void GuestGpu::Submit(std::span<const uint32_t> commands) {
if (commands.empty()) {
return;
}
Submission submission;
submission.type = SubmissionType::Graphics;
submission.queue_id = 0;
submission.commands = draw_commands;
submission.constant_commands = constant_commands;
submission.type = SubmissionType::Graphics;
submission.queue_id = 0;
submission.commands = commands;
Enqueue(std::move(submission));
}
@@ -206,12 +196,10 @@ void CommandProcessor::Reset() {
m_index_buffer_size = 0;
m_index_base_addr = 0;
m_num_instances = 1;
m_num_instances_addr = 0;
m_predicate_skip = false;
m_user_data_marker = HW::UserSgprType::Unknown;
m_draw_indirect_args_base_addr = 0;
m_dispatch_indirect_args_base_addr = 0;
std::memset(m_const_ram, 0, sizeof(m_const_ram));
}
void CommandProcessor::ApplyContextStateOperation(ContextStateOperation operation) {
@@ -244,6 +232,8 @@ void CommandProcessor::BufferInit() {
void CommandProcessor::BufferFlush() {
GetScheduler().Flush();
// The occlusion counters reach a guest that polls them without waiting for a later fence.
m_renderer.GetOcclusionQueries().Publish();
}
void CommandProcessor::BufferWait() {
@@ -251,47 +241,12 @@ void CommandProcessor::BufferWait() {
GetScheduler().Finish();
}
void CommandProcessor::ResetDeCe() {
m_de_count = 0;
m_ce_count = 0;
m_ce_complete = false;
}
void CommandProcessor::WaitCe() {
if (m_ce_count <= m_de_count && !m_ce_complete) {
SuspendPm4();
}
}
void CommandProcessor::WaitDeDiff(uint32_t diff) {
EXIT_IF(m_de_count > m_ce_count);
if (m_ce_count - m_de_count >= diff) {
SuspendPm4();
}
}
void CommandProcessor::WaitForRewind(bool valid) {
if (!valid) {
SuspendPm4();
}
}
void CommandProcessor::IncrementDe() {
m_de_count++;
}
void CommandProcessor::IncrementCe() {
m_ce_count++;
}
void CommandProcessor::WriteConstRam(uint32_t offset, const uint32_t* src, uint32_t dw_num) {
memcpy(m_const_ram + offset / 4, src, static_cast<size_t>(dw_num) * 4);
}
void CommandProcessor::DumpConstRam(uint32_t* dst, uint32_t offset, uint32_t dw_num) {
memcpy(dst, m_const_ram + offset / 4, static_cast<size_t>(dw_num) * 4);
}
bool TestWaitRegMemValue(uint64_t value, uint64_t ref, uint64_t mask, uint32_t func) {
switch (func) {
case 0: return true;
@@ -326,6 +281,16 @@ template void CommandProcessor::WaitRegMem<uint32_t>(uint32_t, const uint32_t*,
template void CommandProcessor::WaitRegMem<uint64_t>(uint32_t, const uint64_t*, uint64_t, uint64_t,
uint32_t, uint32_t);
void CommandProcessor::MemSemaphore(uint64_t* counter, bool signal, bool write) {
if (signal) {
*counter = write ? 1u : *counter + 1u;
} else if (*counter == 0) {
SuspendPm4();
} else {
(*counter)--;
}
}
void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num,
uint32_t write_control) {
const uint32_t dst_sel = ((write_control >> 30u) & 0x1u) | ((write_control >> 7u) & 0x1eu);
@@ -343,12 +308,11 @@ void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw
return;
}
if (write_one_address) {
for (uint32_t i = 0; i < dw_num; i++) {
dst[0] = src[i];
}
} else {
memcpy(dst, src, static_cast<size_t>(dw_num) * sizeof(uint32_t));
// Completion labels can be reused immediately; memcpy may write a destination twice.
auto* output = static_cast<volatile uint32_t*>(dst);
for (uint32_t i = 0; i < dw_num; i++) {
*output = src[i];
output += !write_one_address;
}
}
@@ -479,7 +443,10 @@ void GuestGpu::ThreadRun(void* data) {
}
if (selected_queue < 0) {
gpu->m_processing = false;
gpu->m_work_available.WaitFor(&gpu->m_queue_mutex, 100);
// A wake that came before this wait must not be lost.
if (!std::exchange(gpu->m_wake_pending, false)) {
gpu->m_work_available.WaitFor(&gpu->m_queue_mutex, 100);
}
for (auto& queue: gpu->m_queues) {
if (!queue.empty()) {
queue.front().blocked = false;
@@ -544,71 +511,40 @@ bool GuestGpu::Process(Submission& submission) {
if (first_slice) {
submission.started = true;
cp.SetSubmitId(++m_submit_id);
cp.ResetDeCe();
cp.SetFlip({});
}
cp.BufferInit();
bool complete = true;
switch (submission.type) {
case SubmissionType::Graphics: {
bool progressed = false;
submission.constant_complete |= submission.constant_commands.empty();
for (;;) {
bool round_progress = false;
if (!submission.constant_complete) {
submission.constant_complete =
cp.Process(submission.constant_execution, submission.constant_commands) ==
Pm4ProcessResult::Complete;
round_progress |= submission.constant_execution.MadeProgress();
}
cp.SetCeComplete(submission.constant_complete);
if (!submission.command_complete) {
submission.command_complete =
cp.Process(submission.command_execution, submission.commands) ==
Pm4ProcessResult::Complete;
round_progress |= submission.command_execution.MadeProgress();
}
progressed |= round_progress;
complete = submission.command_complete && submission.constant_complete;
if (complete || !round_progress) {
break;
}
}
if (progressed) {
cp.BufferFlush();
}
break;
}
case SubmissionType::Compute: {
const auto num_dw = static_cast<uint32_t>(submission.commands.size());
const auto* buffer = submission.commands.data();
static uint32_t compute_batch_log_count = 0;
if (first_slice && num_dw <= 128 && compute_batch_log_count++ < 32) {
LOGF("compute direct batch: data=0x%016" PRIx64 ", num_dw=%" PRIu32 "\n",
reinterpret_cast<uint64_t>(buffer), num_dw);
for (uint32_t i = 0; i < std::min<uint32_t>(num_dw, 16); i++) {
LOGF("\t compute[%02" PRIu32 "] = 0x%08" PRIx32 "\n", i, buffer[i]);
}
}
case SubmissionType::Compute:
if (first_slice) {
const auto num_dw = static_cast<uint32_t>(submission.commands.size());
const auto* buffer = submission.commands.data();
static uint32_t compute_batch_log_count = 0;
if (num_dw <= 128 && compute_batch_log_count++ < 32) {
LOGF("compute direct batch: data=0x%016" PRIx64 ", num_dw=%" PRIu32 "\n",
reinterpret_cast<uint64_t>(buffer), num_dw);
for (uint32_t i = 0; i < std::min<uint32_t>(num_dw, 16); i++) {
LOGF("\t compute[%02" PRIu32 "] = 0x%08" PRIx32 "\n", i, buffer[i]);
}
}
GraphicsDbgDumpDcb("cc", num_dw, buffer);
}
complete = cp.Process(submission.command_execution, submission.commands) ==
Pm4ProcessResult::Complete;
if (submission.command_execution.MadeProgress()) {
[[fallthrough]];
case SubmissionType::Graphics:
complete =
cp.Process(submission.execution, submission.commands) == Pm4ProcessResult::Complete;
if (submission.execution.MadeProgress()) {
cp.BufferFlush();
}
break;
}
case SubmissionType::FlipPreparation:
cp.PrepareCpuFlip(submission.flip_request_id);
break;
case SubmissionType::SuspendPoint:
// Age caches at the frame boundary, not once per command list.
m_renderer.RunGarbageCollector();
cp.EmitGlobalBarrier();
m_renderer.GetCommandScheduler().DeferPriorityOperation(
[ready = m_suspend_point_ready] { ready->release(); });
cp.BufferFlush();
@@ -705,18 +641,6 @@ void CommandProcessor::ProcessPm4(Pm4Execution& execution) {
opcode, KYTY_PM4_R(packet_header), packet_dw,
reinterpret_cast<uint64_t>(packet), packet_header);
}
if (opcode == Pm4::IT_NOP && KYTY_PM4_R(packet_header) == Pm4::R_RELEASE_MEM &&
packet_dw >= 7) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 128) {
const auto dst = packet[3] | (static_cast<uint64_t>(packet[4]) << 32u);
const auto val = packet[5] | (static_cast<uint64_t>(packet[6]) << 32u);
LOGF("\t predicated skip: R_RELEASE_MEM dst=0x%016" PRIx64
", value=0x%016" PRIx64 ", action=0x%08" PRIx32
", gcr/data/int=0x%08" PRIx32 "\n",
dst, val, packet[1], packet[2]);
}
}
cursor.offset_dw += packet_dw;
execution.m_made_progress = true;
continue;
@@ -786,7 +710,16 @@ void CommandProcessor::SetNumInstances(uint32_t num_instances) {
num_instances = 1;
}
m_num_instances = num_instances;
m_num_instances = num_instances;
m_num_instances_addr = 0;
}
uint32_t CommandProcessor::NumInstances() {
if (m_num_instances_addr != 0) {
m_num_instances = *reinterpret_cast<const uint32_t*>(m_num_instances_addr);
m_num_instances_addr = 0;
}
return m_num_instances;
}
void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t wait_op,
@@ -800,6 +733,7 @@ void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t
return;
case 0x01: {
EXIT_NOT_IMPLEMENTED(address == nullptr);
m_renderer.GetOcclusionQueries().Publish();
// One begin/end pair per DB; bit 63 marks each counter ready.
constexpr uint64_t ready_bit = 1ull << 63u;
const auto* results = reinterpret_cast<const volatile uint64_t*>(address);
@@ -843,16 +777,12 @@ void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t
void CommandProcessor::DrawIndex(DrawIndexArgs args) {
args.index_type_and_size = m_index_type_and_size;
if (args.instance_count == 0) {
args.instance_count = m_num_instances;
}
if (GraphicsRunDebugDumpEnabled() && (args.base_vertex != 0 || args.first_instance != 0)) {
LOGF("\t draw indexed offsets: base_vertex = %" PRId32 ", first_instance = %" PRIu32 "\n",
args.base_vertex, args.first_instance);
args.instance_count = NumInstances();
}
m_renderer.GetRenderExecutor().DrawIndex(m_submit_id, CurrentBuffer(), args);
}
void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_count) {
const void* CommandProcessor::IndexAddress(uint32_t first_index) const {
uint64_t index_size = 0;
switch (m_index_type_and_size) {
case 0: index_size = 2; break;
@@ -860,16 +790,16 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
case 2: index_size = 1; break;
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
}
return reinterpret_cast<const void*>(m_index_base_addr + first_index * index_size);
}
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(index_offset) * index_size);
DrawIndex({.index_count = index_count, .index_addr = index_addr});
void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_count) {
DrawIndex({.index_count = index_count, .index_addr = IndexAddress(index_offset)});
}
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
uint32_t vertex_offset, uint32_t instance_offset,
uint32_t first_index) {
uint32_t first_index, uint32_t draw_index) {
const auto write = [&](uint32_t location, uint32_t value) {
if (location == Pm4::SH_NOP) {
return;
@@ -881,11 +811,34 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
write(registers.vertex_offset, vertex_offset);
write(registers.instance_offset, instance_offset);
write(registers.index_offset, first_index);
write(registers.draw_index, draw_index);
}
// Writes the GS user-data slots the packet patches; false when the shaders cannot read the
// offsets from GPU records: a first-index or subdraw-index destination, or another stage's
// user data.
static bool IndirectDrawOffsets(IndirectDrawRegisters registers, ShaderIndirectOffsets& offsets) {
if (registers.index_offset != Pm4::SH_NOP || registers.draw_index != Pm4::SH_NOP) {
return false;
}
for (const auto [location, slot]: {std::pair {registers.vertex_offset, &offsets.vertex},
std::pair {registers.instance_offset, &offsets.instance}}) {
if (location == Pm4::SH_NOP) {
continue;
}
const uint32_t index = location - Pm4::SPI_SHADER_USER_DATA_GS_0;
if (index >= HW::UserSgprInfo::SGPRS_MAX) {
return false;
}
*slot = static_cast<uint8_t>(index);
}
return true;
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
const auto args_size =
indexed ? sizeof(vk::DrawIndexedIndirectCommand) : sizeof(vk::DrawIndirectCommand);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, registers, draw_initiator, indexed);
}
@@ -895,77 +848,64 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
// The guest records match Vulkan's indirect commands byte for byte.
EXIT_NOT_IMPLEMENTED((stride_in_bytes & 3u) != 0 ||
stride_in_bytes < (indexed ? sizeof(vk::DrawIndexedIndirectCommand)
: sizeof(vk::DrawIndirectCommand)));
uint32_t draw_count = max_count_or_count;
if (count_addr != nullptr) {
draw_count = *count_addr;
if (draw_count > max_count_or_count) {
draw_count = max_count_or_count;
}
}
const auto draw_count = count_addr == nullptr
? max_count_or_count
: std::min(uint32_t {*count_addr}, max_count_or_count);
if (draw_count == 0) {
return;
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::IndirectDraws, draw_count);
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
EXIT_NOT_IMPLEMENTED(stride_in_bytes < args_size);
uint64_t index_size = 0;
if (indexed) {
switch (m_index_type_and_size) {
case 0: index_size = 2; break;
case 1: index_size = 4; break;
case 2: index_size = 1; break;
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
}
auto& executor = m_renderer.GetRenderExecutor();
const auto args_addr = m_draw_indirect_args_base_addr + data_offset;
// NUM_INSTANCES keeps the last draw's instance count.
m_num_instances_addr = args_addr + static_cast<uint64_t>(draw_count - 1) * stride_in_bytes +
offsetof(vk::DrawIndirectCommand, instanceCount);
// The GPU reads the records if the shaders allow; the VS prolog loads the patched SGPRs from
// Vulkan's bases. With no SGPR patched, Vulkan applies the offsets, which matches while they
// are zero.
DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) &&
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = m_index_buffer_size,
.index_addr = IndexAddress(0),
.index_type_and_size = m_index_type_and_size,
.indirect = records})
: executor.DrawAuto(m_submit_id, CurrentBuffer(), {.indirect = records}))) {
return;
}
const uint32_t max_indices = m_index_buffer_size != 0 ? m_index_buffer_size : UINT32_MAX;
for (uint32_t i = 0; i < draw_count; i++) {
const auto args_addr = m_draw_indirect_args_base_addr + data_offset +
static_cast<uint64_t>(i) * stride_in_bytes;
const auto addr = args_addr + static_cast<uint64_t>(i) * stride_in_bytes;
if (!indexed) {
auto* args = reinterpret_cast<const DrawIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->start_vertex_location,
args->start_instance_location, 0);
m_num_instances = args->instance_count;
DrawIndexAuto({.vertex_count = args->vertex_count_per_instance,
.instance_count = args->instance_count,
.first_vertex = args->start_vertex_location,
.first_instance = args->start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
const auto& args = *reinterpret_cast<const vk::DrawIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0, i);
executor.DrawAuto(m_submit_id, CurrentBuffer(),
{.vertex_count = args.vertexCount,
.instance_count = args.instanceCount,
.first_vertex = args.firstVertex,
.first_instance = args.firstInstance,
.offset_source = DrawOffsetSource::IndirectArgs});
continue;
}
auto* args = reinterpret_cast<const DrawIndexedIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->base_vertex_location,
args->start_instance_location, args->start_index_location);
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args->start_index_location) * index_size);
const uint32_t index_count =
(m_index_buffer_size != 0
? std::min(args->index_count_per_instance, m_index_buffer_size)
: args->index_count_per_instance);
if (GraphicsRunDebugDumpEnabled() && index_count != args->index_count_per_instance) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1, std::memory_order_relaxed) < 64) {
LOGF("\t DrawIndexIndirectMulti: clamped index_count from %" PRIu32 " to %" PRIu32
" using INDEX_BUFFER_SIZE\n",
args->index_count_per_instance, index_count);
}
}
m_num_instances = args->instance_count;
DrawIndex({.index_count = index_count,
.index_addr = index_addr,
.instance_count = args->instance_count,
.base_vertex = static_cast<int32_t>(args->base_vertex_location),
.first_instance = args->start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
const auto& args = *reinterpret_cast<const vk::DrawIndexedIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, static_cast<uint32_t>(args.vertexOffset),
args.firstInstance, args.firstIndex, i);
executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = std::min(args.indexCount, max_indices),
.index_addr = IndexAddress(args.firstIndex),
.instance_count = args.instanceCount,
.index_type_and_size = m_index_type_and_size,
.base_vertex = args.vertexOffset,
.first_instance = args.firstInstance,
.offset_source = DrawOffsetSource::IndirectArgs});
}
}
@@ -1014,237 +954,86 @@ void CommandProcessor::DispatchIndirect(uint64_t args_addr, uint32_t mode) {
void CommandProcessor::DrawIndexAuto(DrawAutoArgs args) {
if (args.instance_count == 0) {
args.instance_count = m_num_instances;
args.instance_count = NumInstances();
}
m_renderer.GetRenderExecutor().DrawAuto(m_submit_id, CurrentBuffer(), args);
}
void CommandProcessor::WaitFlipDone(uint32_t video_out_handle, uint32_t display_buffer_index) {
BufferFlush();
m_renderer.GetVideoOut().WaitFlipDone(static_cast<int>(video_out_handle),
static_cast<int>(display_buffer_index));
// Stall only this command processor, not the host command lane or other queues.
if (!m_renderer.GetVideoOut().IsSafeForRendering(static_cast<int>(video_out_handle),
static_cast<int>(display_buffer_index))) {
SuspendPm4();
}
}
template <typename T>
void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action,
uint32_t event_index, uint32_t event_write_source,
void* dst_gpu_addr, T value, uint32_t interrupt_selector,
uint32_t interrupt_context_id) {
static_assert(sizeof(T) == sizeof(uint32_t) || sizeof(T) == sizeof(uint64_t));
auto& command = CurrentBuffer();
void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector, void* address,
uint64_t data, uint32_t interrupt_context_id) {
if (GraphicsRunDebugDumpEnabled()) {
const auto bits = static_cast<unsigned>(sizeof(T) * 8u);
const auto log_width = static_cast<int>(sizeof(T) * 2u);
LOGF("CommandProcessor::WriteAtEndOfPipe%u()\n"
"\t cache_policy = 0x%08" PRIx32 "\n"
"\t event_write_dest = 0x%08" PRIx32 "\n"
"\t eop_event_type = 0x%08" PRIx32 "\n"
"\t cache_action = 0x%08" PRIx32 "\n"
"\t event_index = 0x%08" PRIx32 "\n"
"\t event_write_source = 0x%08" PRIx32 "\n"
"\t interrupt_selector = 0x%08" PRIx32 "\n"
"\t interrupt_context = 0x%08" PRIx32 "\n"
"\t dst_gpu_addr = 0x%016" PRIx64 "\n"
"\t value = 0x%0*" PRIx64 "\n",
bits, cache_policy, event_write_dest, eop_event_type, cache_action, event_index,
event_write_source, interrupt_selector, interrupt_context_id,
reinterpret_cast<uint64_t>(dst_gpu_addr), log_width, static_cast<uint64_t>(value));
LOGF("CommandProcessor::ReleaseMem()\n"
"\t data_sel = %" PRIu32 "\n"
"\t interrupt_selector = %" PRIu32 "\n"
"\t address = 0x%016" PRIx64 "\n"
"\t data = 0x%016" PRIx64 "\n"
"\t interrupt_context = 0x%08" PRIx32 "\n",
data_sel, interrupt_selector, reinterpret_cast<uint64_t>(address), data,
interrupt_context_id);
}
EXIT_NOT_IMPLEMENTED(cache_policy != 0x00000000);
EXIT_NOT_IMPLEMENTED(event_write_dest != 0x00000000);
bool with_interrupt = false;
switch (interrupt_selector) {
case 0x00:
case 0x03: with_interrupt = false; break;
case 0x01:
if (!IsAsyncComputeQueue()) {
TriggerEopEventAtEndOfPipe(interrupt_context_id);
return;
}
with_interrupt = true;
break;
case 0x02: with_interrupt = true; break;
default: EXIT("unknown interrupt_selector\n");
// QueuedInterrupt: kNone, kOnly, kAfterWriteConfirm, kDataAfterWriteConfirm, kSendIntCtx.
EXIT_NOT_IMPLEMENTED(interrupt_selector > 4);
const bool interrupt = (interrupt_selector != 0 && interrupt_selector != 3);
// kSendIntCtx has no label, and kOnly writes none on graphics queues.
if (address == nullptr || interrupt_selector == 4 ||
(interrupt_selector == 1 && !IsAsyncComputeQueue())) {
data_sel = 0;
}
if (data_sel == 0 && !interrupt) {
return;
}
auto write32 = [&] {
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
auto data = static_cast<uint32_t>(value);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &data, sizeof(data));
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
m_renderer.GetOcclusionQueries().Publish();
if (with_interrupt) {
TriggerEopEventAtEndOfPipe(interrupt_context_id);
// A label beside GPU-written bytes goes to the backing store, so they are not downloaded.
// Other labels go through the guest mapping: its fault keeps a clean cached copy current and
// downloads GPU-written bytes under the label before the store.
const auto write_label = [this, address](const void* value, uint64_t size) {
const auto vaddr = reinterpret_cast<uint64_t>(address);
auto& buffers = m_renderer.GetBufferCache();
if (!GuestRange {vaddr, size}.Valid() || !buffers.IsRegionGpuModified(vaddr, size) ||
buffers.HasGpuDirtyBytes(vaddr, size) ||
!LibKernel::Memory::TryWriteBacking(vaddr, value, size)) {
std::memcpy(address, value, size);
}
};
switch (event_write_source) {
case 0x01:
if constexpr (sizeof(T) == sizeof(uint32_t)) {
if (eop_event_type == 0x2f && cache_action == 0x00 && event_index == 0x06) {
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
SynchronizeGpu();
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(), dst, value & 0xffffu,
value >> 16u);
if (with_interrupt) {
m_renderer.TriggerInterrupt(m_interrupt_event_id, interrupt_context_id);
}
return;
}
} else if (eop_event_type == 0x04 && cache_action == 0x00 && event_index == 0x05) {
write32();
return;
}
// EndOfPipeData: kNone, kImmediate32b, kImmediate64b, kRefClk; 5 reads GDS.
switch (data_sel) {
case 0: break;
case 1: {
const auto value = static_cast<uint32_t>(data);
write_label(&value, sizeof(value));
break;
case 0x02:
case 0x04:
if constexpr (sizeof(T) == sizeof(uint32_t)) {
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
switch (cache_action) {
case 0x00:
case 0x38: write32(); return;
default: break;
}
}
} else {
if (event_write_source == 0x04) {
value = Sync::ReadReferenceClock();
}
auto write64 = [&] {
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &value, sizeof(value));
if (with_interrupt) {
TriggerEopEventAtEndOfPipe(interrupt_context_id);
}
};
switch (cache_action) {
case 0x00:
switch (eop_event_type) {
case 0x04:
if (event_index == 0x05) {
write64();
return;
}
break;
case 0x14:
case 0x28:
case 0x2f:
if (event_index == 0x00) {
write64();
return;
}
break;
case 0x2b:
case 0x2d:
case 0x30:
if (event_index == 0x00 && !with_interrupt) {
write64();
return;
}
break;
default: break;
}
break;
case 0x38:
switch (eop_event_type) {
case 0x04:
case 0x14:
case 0x28:
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
event_index == 0x05) ||
(event_index == 0x00)) {
write64();
return;
}
break;
case 0x2b:
case 0x2d:
if (event_index == 0x00 && !with_interrupt) {
write64();
return;
}
break;
case 0x2f:
if (event_index == 0x06 && !with_interrupt) {
write64();
return;
}
break;
default: break;
}
break;
case 0x3b:
if (eop_event_type == 0x04 && event_index == 0x05 && with_interrupt) {
write64();
return;
}
break;
default: break;
}
}
}
case 3: data = Sync::ReadReferenceClock(); [[fallthrough]];
case 2: write_label(&data, sizeof(data)); break;
case 5: {
GetScheduler().Finish();
const auto gds = static_cast<uint32_t>(data);
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(),
static_cast<uint32_t*>(address), gds & 0xffffu, gds >> 16u);
break;
default: break;
}
default: EXIT("unknown release_mem data selector: %" PRIu32 "\n", data_sel);
}
EXIT("unknown event type\n");
}
void CommandProcessor::WriteAtEndOfPipe32(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action,
uint32_t event_index, uint32_t event_write_source,
void* dst_gpu_addr, uint32_t value,
uint32_t interrupt_selector,
uint32_t interrupt_context_id) {
WriteAtEndOfPipe(cache_policy, event_write_dest, eop_event_type, cache_action, event_index,
event_write_source, dst_gpu_addr, value, interrupt_selector,
interrupt_context_id);
}
void CommandProcessor::WriteAtEndOfPipe64(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action,
uint32_t event_index, uint32_t event_write_source,
void* dst_gpu_addr, uint64_t value,
uint32_t interrupt_selector,
uint32_t interrupt_context_id) {
WriteAtEndOfPipe(cache_policy, event_write_dest, eop_event_type, cache_action, event_index,
event_write_source, dst_gpu_addr, value, interrupt_selector,
interrupt_context_id);
}
void CommandProcessor::EmitGlobalBarrier() {
Common::LockGuard lock(m_renderer.GetMutex());
vk::MemoryBarrier2 barrier {};
barrier.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
barrier.srcAccessMask = vk::AccessFlagBits2::eMemoryWrite;
barrier.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
barrier.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
vk::DependencyInfo dependency {};
dependency.memoryBarrierCount = 1;
dependency.pMemoryBarriers = &barrier;
GetScheduler().EndRendering();
CurrentBuffer().Handle().pipelineBarrier2(dependency);
}
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
(void)CurrentBuffer().Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
renderer.TriggerInterrupt(event_id, interrupt_context_id);
});
// GuestGpu::Process's end-of-slice flush submits the work this interrupt waits on.
if (interrupt) {
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
renderer.TriggerInterrupt(event_id, interrupt_context_id);
});
}
}
void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
@@ -1258,21 +1047,17 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
}
const auto valid_cache_event_index = event_index == 0x00000000 || event_index == 0x00000007;
// Partial flushes and cache writebacks only order GPU work, which the renderer's resource
// barriers already order.
switch (event_type) {
// CsPartialFlush, GsPartialFlush, PsPartialFlush.
case 0x00000007:
case 0x0000000f:
case 0x00000010: EmitGlobalBarrier(); break;
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate.
case 0x00000010: break;
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate, DbDataWritebackInvalidate,
// DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
case 0x00000016:
case 0x00000031:
if (!valid_cache_event_index) {
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
event_index);
}
EmitGlobalBarrier();
break;
// DbDataWritebackInvalidate, DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
case 0x0000002a:
case 0x0000002c:
case 0x0000002e:
@@ -1280,7 +1065,6 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
event_index);
}
EmitGlobalBarrier();
break;
case 0x0000000d:
case 0x0000000e:
@@ -1302,15 +1086,19 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
"\n",
event_index, event_address);
}
if (auto& queries = m_renderer.GetOcclusionQueries(); queries.Enabled()) {
queries.Dump(event_address);
break;
}
static std::once_flag warning_once;
std::call_once(warning_once, [] {
std::printf("Warning: game uses occlusion queries, which are currently treated as "
"always visible; GPU usage may be higher and FPS may be lower.\n");
std::printf("Warning: game uses occlusion queries, which are treated as always "
"visible without --occlusion-queries; GPU usage may be higher and FPS "
"may be lower.\n");
});
// Until host occlusion queries are implemented, publish an always-visible result. The
// PS5 layout contains one interleaved begin/end pair per DB, and bit 63 marks a result
// ready.
// Without host occlusion queries, publish an always-visible result. The PS5 layout
// contains one interleaved begin/end pair per DB, and bit 63 marks a result ready.
constexpr uint64_t ready_bit = 1ull << 63u;
constexpr uint64_t counter_mask = ready_bit - 1u;
auto* results = reinterpret_cast<volatile uint64_t*>(event_address);
@@ -1326,69 +1114,20 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
}
}
void CommandProcessor::Flip() {
void CommandProcessor::Flip(int handle, int index, int flip_mode, int64_t flip_arg) {
if (GraphicsRunDebugDumpEnabled()) {
LOGF("CommandProcessor::Flip()\n");
}
m_renderer.GetOcclusionQueries().Publish();
auto& command = CurrentBuffer();
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
auto request = Sync::PrepareVideoOutFlip(command, handle, index, flip_mode, flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
}
void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
auto& command = CurrentBuffer();
if (GraphicsRunDebugDumpEnabled()) {
LOGF("CommandProcessor::Flip()\n"
"\t dst_gpu_addr = 0x%016" PRIx64 "\n"
"\t value = 0x%08" PRIx32 "\n",
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
}
void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache_action,
void* dst_gpu_addr, uint32_t value) {
auto& command = CurrentBuffer();
if (GraphicsRunDebugDumpEnabled()) {
LOGF("CommandProcessor::FlipWithInterrupt()\n"
"\t eop_event_type = 0x%08" PRIx32 "\n"
"\t cache_action = 0x%08" PRIx32 "\n"
"\t dst_gpu_addr = 0x%016" PRIx64 "\n"
"\t value = 0x%08" PRIx32 "\n",
eop_event_type, cache_action, reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
if (eop_event_type != 0x00000004 || cache_action != 0x00000038) {
EXIT("unknown event type\n");
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, request] {
renderer.GetVideoOut().CompleteFlip(request);
renderer.TriggerInterrupt(event_id, 0);
});
GetScheduler().Flush();
}
void CommandProcessor::PrepareCpuFlip(uint64_t request_id) {
auto& command = CurrentBuffer();
if (g_current_processor != nullptr) {
@@ -1406,10 +1145,6 @@ void CommandProcessor::PrepareCpuFlip(uint64_t request_id) {
GetScheduler().Flush();
}
void CommandProcessor::SynchronizeGpu() {
GetScheduler().Finish();
}
bool GuestGpu::IsGpuThread() noexcept {
return g_gpu_thread;
}
+8 -10
View File
@@ -31,10 +31,11 @@ public:
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
// Retries blocked submissions now instead of after the 100 ms stall timeout.
void Wake();
// Submitted command memory is borrowed and must remain valid until GPU execution completes.
void Submit(std::span<const uint32_t> draw_commands,
std::span<const uint32_t> constant_commands);
void Submit(std::span<const uint32_t> commands);
void SubmitCompute(uint32_t queue, std::span<const uint32_t> commands);
void SubmitFlipPreparation(uint64_t request_id);
// Insert an ordered graphics drain; only a previous suspend point can block the caller.
@@ -58,14 +59,10 @@ private:
SubmissionType type = SubmissionType::Graphics;
uint32_t queue_id = 0;
std::span<const uint32_t> commands;
std::span<const uint32_t> constant_commands;
Pm4Execution command_execution;
Pm4Execution constant_execution;
bool started = false;
bool command_complete = false;
bool constant_complete = false;
bool blocked = false;
uint64_t flip_request_id = 0;
Pm4Execution execution;
bool started = false;
bool blocked = false;
uint64_t flip_request_id = 0;
};
void Enqueue(Submission submission);
@@ -87,6 +84,7 @@ private:
bool m_processing = false;
bool m_accepting = true;
bool m_stopping = false;
bool m_wake_pending = false;
bool m_shutdown_complete = false;
// Completion callbacks can outlive GuestGpu during renderer shutdown.
std::shared_ptr<std::binary_semaphore> m_suspend_point_ready =
+21 -29
View File
@@ -5,6 +5,8 @@
#include "common/common.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include <array>
namespace Libs::Graphics::HW {
struct ColorBase {
@@ -574,6 +576,12 @@ struct ShaderRegisters {
uint32_t m_paScShaderControl = 0;
DepthShaderControl db_shader_control;
bool alpha_to_mask_enable = false;
// DB_SHADER_CONTROL.ALPHA_TO_MASK_DISABLE overrides DB_ALPHA_TO_MASK per shader.
[[nodiscard]] bool AlphaToMask() const {
return alpha_to_mask_enable && !db_shader_control.alpha_to_mask_disable;
}
[[nodiscard]] uint32_t GetExportCount() const {
return 1u + ((m_spiVsOutConfig >> 1u) & 0x1Fu);
@@ -589,14 +597,10 @@ struct ShaderRegisters {
}
};
enum class UserSgprType { Unknown, Region, Vsharp };
struct UserSgprInfo {
static constexpr int SGPRS_MAX = 32;
uint32_t value[SGPRS_MAX] = {0};
UserSgprType type[SGPRS_MAX] = {};
uint32_t count = 0;
uint32_t value[SGPRS_MAX] = {0};
};
struct VertexShaderInfo {
@@ -824,6 +828,11 @@ public:
void SetBlendColor(const BlendColor& color) { m_blend_color = color; }
[[nodiscard]] const ClipControl& GetClipControl() const { return m_clip_control; }
void SetClipControl(const ClipControl& control) { m_clip_control = control; }
// PA_CL_UCP_0_X..PA_CL_UCP_5_W as raw float bits, four per plane.
[[nodiscard]] const std::array<uint32_t, 24>& GetUserClipPlanes() const {
return m_user_clip_planes;
}
void SetUserClipPlane(uint32_t index, uint32_t value) { m_user_clip_planes[index] = value; }
[[nodiscard]] const RenderControl& GetRenderControl() const { return m_render_control; }
void SetRenderControl(const RenderControl& control) { m_render_control = control; }
[[nodiscard]] const DepthRenderOverride& GetDepthRenderOverride() const {
@@ -908,6 +917,7 @@ public:
void SetDepthShaderControl(const DepthShaderControl& value) {
m_sh_regs.db_shader_control = value;
}
void SetAlphaToMaskEnable(bool value) { m_sh_regs.alpha_to_mask_enable = value; }
void SetScShaderControl(uint32_t value) { m_sh_regs.m_paScShaderControl = value; }
@@ -924,6 +934,8 @@ private:
ColorControl m_color_control;
ScanModeControl m_scan_mode_control;
std::array<uint32_t, 24> m_user_clip_planes {};
AaSampleControl m_aa_sample_control;
AaConfig m_aa_config;
@@ -1040,30 +1052,10 @@ public:
void SetCsShader(const CsStageRegisters& cs_regs) { m_cs.cs_regs = cs_regs; }
void SetCsWaveSize(uint8_t wave_size) { m_cs.cs_regs.wave_size = wave_size; }
void SetPsUserSgpr(uint32_t id, uint32_t value, UserSgprType type) {
m_ps.ps_user_sgpr.value[id] = value;
m_ps.ps_user_sgpr.type[id] = type;
m_ps.ps_user_sgpr.count =
((id + 1) > m_ps.ps_user_sgpr.count ? (id + 1) : m_ps.ps_user_sgpr.count);
}
void SetCsUserSgpr(uint32_t id, uint32_t value, UserSgprType type) {
m_cs.cs_user_sgpr.value[id] = value;
m_cs.cs_user_sgpr.type[id] = type;
m_cs.cs_user_sgpr.count =
((id + 1) > m_cs.cs_user_sgpr.count ? (id + 1) : m_cs.cs_user_sgpr.count);
}
void SetHsUserSgpr(uint32_t id, uint32_t value, UserSgprType type) {
m_vs.hs_user_sgpr.value[id] = value;
m_vs.hs_user_sgpr.type[id] = type;
m_vs.hs_user_sgpr.count =
((id + 1) > m_vs.hs_user_sgpr.count ? (id + 1) : m_vs.hs_user_sgpr.count);
}
void SetGsUserSgpr(uint32_t id, uint32_t value, UserSgprType type) {
m_vs.gs_user_sgpr.value[id] = value;
m_vs.gs_user_sgpr.type[id] = type;
m_vs.gs_user_sgpr.count =
((id + 1) > m_vs.gs_user_sgpr.count ? (id + 1) : m_vs.gs_user_sgpr.count);
}
void SetPsUserSgpr(uint32_t id, uint32_t value) { m_ps.ps_user_sgpr.value[id] = value; }
void SetCsUserSgpr(uint32_t id, uint32_t value) { m_cs.cs_user_sgpr.value[id] = value; }
void SetHsUserSgpr(uint32_t id, uint32_t value) { m_vs.hs_user_sgpr.value[id] = value; }
void SetGsUserSgpr(uint32_t id, uint32_t value) { m_vs.gs_user_sgpr.value[id] = value; }
[[nodiscard]] const PixelShaderInfo& GetPs() const { return m_ps; }
[[nodiscard]] const VertexShaderInfo& GetVs() const { return m_vs; }
-10
View File
@@ -25,7 +25,6 @@ constexpr auto MakeRegisterNames() {
names[R_POP_MARKER] = "R_POP_MARKER";
names[R_WRITE_DATA] = "R_WRITE_DATA";
names[R_FLIP] = "R_FLIP";
names[R_RELEASE_MEM] = "R_RELEASE_MEM";
names[R_DMA_DATA] = "R_DMA_DATA";
names[R_CONTEXT_STATE] = "R_CONTEXT_STATE";
return names;
@@ -54,7 +53,6 @@ constexpr auto MakeOpcodeNames() {
names[IT_DRAW_INDIRECT_MULTI] = "IT_DRAW_INDIRECT_MULTI";
names[IT_DRAW_INDEX_AUTO] = "IT_DRAW_INDEX_AUTO";
names[IT_NUM_INSTANCES] = "IT_NUM_INSTANCES";
names[IT_INDIRECT_BUFFER_CNST] = "IT_INDIRECT_BUFFER_CNST";
names[IT_DRAW_INDEX_OFFSET_2] = "IT_DRAW_INDEX_OFFSET_2";
names[IT_WRITE_DATA] = "IT_WRITE_DATA";
names[IT_MEM_SEMAPHORE] = "IT_MEM_SEMAPHORE";
@@ -66,8 +64,6 @@ constexpr auto MakeOpcodeNames() {
names[IT_PFP_SYNC_ME] = "IT_PFP_SYNC_ME";
names[IT_SURFACE_SYNC] = "IT_SURFACE_SYNC";
names[IT_EVENT_WRITE] = "IT_EVENT_WRITE";
names[IT_EVENT_WRITE_EOP] = "IT_EVENT_WRITE_EOP";
names[IT_EVENT_WRITE_EOS] = "IT_EVENT_WRITE_EOS";
names[IT_RELEASE_MEM] = "IT_RELEASE_MEM";
names[IT_DMA_DATA] = "IT_DMA_DATA";
names[IT_ACQUIRE_MEM] = "IT_ACQUIRE_MEM";
@@ -80,12 +76,6 @@ constexpr auto MakeOpcodeNames() {
names[IT_SET_QUEUE_REG] = "IT_SET_QUEUE_REG";
names[IT_SET_UCONFIG_REG] = "IT_SET_UCONFIG_REG";
names[IT_SET_UCONFIG_REG_INDEX] = "IT_SET_UCONFIG_REG_INDEX";
names[IT_WRITE_CONST_RAM] = "IT_WRITE_CONST_RAM";
names[IT_DUMP_CONST_RAM] = "IT_DUMP_CONST_RAM";
names[IT_INCREMENT_CE_COUNTER] = "IT_INCREMENT_CE_COUNTER";
names[IT_INCREMENT_DE_COUNTER] = "IT_INCREMENT_DE_COUNTER";
names[IT_WAIT_ON_CE_COUNTER] = "IT_WAIT_ON_CE_COUNTER";
names[IT_WAIT_ON_DE_COUNTER_DIFF] = "IT_WAIT_ON_DE_COUNTER_DIFF";
names[IT_DISPATCH_DRAW_PREAMBLE] = "IT_DISPATCH_DRAW_PREAMBLE";
names[IT_DISPATCH_DRAW] = "IT_DISPATCH_DRAW";
names[IT_GET_LOD_STATS] = "IT_GET_LOD_STATS";
+2 -10
View File
@@ -40,7 +40,6 @@ constexpr uint32_t IT_INDEX_TYPE = 0x2A;
constexpr uint32_t IT_DRAW_INDIRECT_MULTI = 0x2C;
constexpr uint32_t IT_DRAW_INDEX_AUTO = 0x2D;
constexpr uint32_t IT_NUM_INSTANCES = 0x2F;
constexpr uint32_t IT_INDIRECT_BUFFER_CNST = 0x33;
constexpr uint32_t IT_DRAW_INDEX_OFFSET_2 = 0x35;
constexpr uint32_t IT_WRITE_DATA = 0x37;
constexpr uint32_t IT_MEM_SEMAPHORE = 0x39;
@@ -53,8 +52,6 @@ constexpr uint32_t IT_CP_DMA = 0x41;
constexpr uint32_t IT_PFP_SYNC_ME = 0x42;
constexpr uint32_t IT_SURFACE_SYNC = 0x43;
constexpr uint32_t IT_EVENT_WRITE = 0x46;
constexpr uint32_t IT_EVENT_WRITE_EOP = 0x47;
constexpr uint32_t IT_EVENT_WRITE_EOS = 0x48;
constexpr uint32_t IT_RELEASE_MEM = 0x49;
constexpr uint32_t IT_DMA_DATA = 0x50;
constexpr uint32_t IT_ACQUIRE_MEM = 0x58;
@@ -67,12 +64,6 @@ constexpr uint32_t IT_SET_SH_REG = 0x76;
constexpr uint32_t IT_SET_QUEUE_REG = 0x78;
constexpr uint32_t IT_SET_UCONFIG_REG = 0x79;
constexpr uint32_t IT_SET_UCONFIG_REG_INDEX = 0x7A;
constexpr uint32_t IT_WRITE_CONST_RAM = 0x81;
constexpr uint32_t IT_DUMP_CONST_RAM = 0x83;
constexpr uint32_t IT_INCREMENT_CE_COUNTER = 0x84;
constexpr uint32_t IT_INCREMENT_DE_COUNTER = 0x85;
constexpr uint32_t IT_WAIT_ON_CE_COUNTER = 0x86;
constexpr uint32_t IT_WAIT_ON_DE_COUNTER_DIFF = 0x88;
constexpr uint32_t IT_DISPATCH_DRAW = 0x8D;
constexpr uint32_t IT_GET_LOD_STATS = 0x8E;
constexpr uint32_t IT_WAIT_REG_MEM_64 = 0x93;
@@ -88,7 +79,6 @@ constexpr uint32_t R_PUSH_MARKER = 0x0B;
constexpr uint32_t R_POP_MARKER = 0x0C;
constexpr uint32_t R_WRITE_DATA = 0x15;
constexpr uint32_t R_FLIP = 0x17;
constexpr uint32_t R_RELEASE_MEM = 0x18;
constexpr uint32_t R_DMA_DATA = 0x19;
constexpr uint32_t R_CONTEXT_STATE = 0x1A;
@@ -549,6 +539,8 @@ constexpr uint32_t VGT_SHADER_STAGES_EN = 0x2D5;
constexpr uint32_t VGT_LS_HS_CONFIG = 0x2D6;
constexpr uint32_t VGT_TF_PARAM = 0x2DB;
constexpr uint32_t DB_ALPHA_TO_MASK = 0x2DC;
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_SHIFT = 0;
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_MASK = 0x1;
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL = 0x2DE;
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_SHIFT = 0;
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_MASK = 0xFF;
+5
View File
@@ -35,6 +35,9 @@ struct GraphicContext {
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool maintenance8_enabled = false;
bool conditional_rendering_enabled = false;
bool occlusion_queries_enabled = false;
bool occlusion_query_precise_enabled = false;
bool supports_block_texel_view = false;
bool mesh_shader_enabled = false;
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
@@ -42,6 +45,8 @@ struct GraphicContext {
uint32_t min_subgroup_size = 0;
uint32_t max_subgroup_size = 0;
uint32_t max_push_descriptors = 0;
uint32_t device_clock_period_ps = 0;
vk::DescriptorSetLayout image_heap_layout = nullptr;
vk::ShaderStageFlags required_subgroup_size_stages = {};
Common::Mutex queue_mutex;
uint32_t queue_family = static_cast<uint32_t>(-1);
-15
View File
@@ -66,18 +66,6 @@ RegionManager* MemoryTracker::GetOrCreateRegion(uint64_t index) {
return ptr;
}
void MemoryTracker::QueueBdaSync(uint64_t vaddr, uint64_t size) {
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
std::lock_guard lock(m_bda_sync_mutex);
m_bda_sync.Add(begin, end - begin);
}
RangeSet MemoryTracker::TakeBdaSync() {
std::lock_guard lock(m_bda_sync_mutex);
return std::exchange(m_bda_sync, RangeSet {});
}
bool MemoryTracker::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
CheckNotInUploadCallback();
return Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
@@ -100,7 +88,6 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
std::scoped_lock lock(manager->lock);
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
QueueBdaSync(vaddr, size);
}
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
@@ -141,8 +128,6 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
locks.clear();
QueueBdaSync(vaddr, size);
}
} // namespace Libs::Graphics
-7
View File
@@ -29,10 +29,6 @@ public:
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UntrackMemory(uint64_t vaddr, uint64_t size);
// Queue after changing ownership; a concurrent drain must not consume an event
// before the dirty pages it describes become visible.
void QueueBdaSync(uint64_t vaddr, uint64_t size);
[[nodiscard]] RangeSet TakeBdaSync();
// Removes protection from a range and flushes GPU-owned data when required.
template <typename Flush>
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
@@ -55,7 +51,6 @@ public:
on_flush();
}
});
QueueBdaSync(vaddr, size);
}
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
@@ -154,8 +149,6 @@ private:
std::unique_ptr<std::atomic<RegionManager*>[]> m_regions;
std::vector<std::unique_ptr<RegionManager>> m_region_storage;
std::mutex m_region_mutex;
std::mutex m_bda_sync_mutex;
RangeSet m_bda_sync;
PageManager& m_page_manager;
};
+42 -9
View File
@@ -127,7 +127,7 @@ void BufferCache::ChangeRegister(BufferId id) {
}
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
m_memory_tracker.QueueBdaSync(buffer.CpuAddress(), buffer.Size());
QueueBdaSync(buffer.CpuAddress(), buffer.Size());
} else {
const auto found = m_buffers.find(buffer.CpuAddress());
EXIT_IF(found == m_buffers.end() || found->second != id);
@@ -174,6 +174,7 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t
total_size += Common::AlignUp(end - start, 64);
});
m_gpu_modified_ranges.Subtract(address, bytes);
ForgetColorMetadataChecks(address, bytes);
});
if (copies.empty()) {
return false;
@@ -256,6 +257,10 @@ BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
m_gds_buffer.Flush(0, m_gds_buffer.Size());
SetVulkanObjectNameF(m_graphics.device, m_bda_pagetable_buffer.Handle(),
"BDA Page Table Buffer");
m_scheduler.BeginCommand();
for (auto* buffer: {&m_bda_pagetable_buffer, m_fault_manager.GetFaultBuffer()}) {
buffer->Fill(0, buffer->Size(), 0);
}
const auto null_id =
m_slot_buffers.insert(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16);
EXIT_IF(null_id != NULL_BUFFER_ID);
@@ -294,6 +299,7 @@ void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) {
}
m_memory_tracker.InvalidateRegion(vaddr, size,
[this, vaddr, size] { ReadMemory(vaddr, size, true); });
QueueBdaSync(vaddr, size);
}
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
@@ -313,6 +319,7 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
}
if (is_write) {
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
QueueBdaSync(vaddr, size);
}
});
}
@@ -515,6 +522,7 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t
(void)SynchronizeBuffer(buffer, vaddr, size, is_written, is_texel_buffer);
if (is_written) {
m_gpu_modified_ranges.Add(vaddr, size);
ForgetColorMetadataChecks(vaddr, size);
}
return {&buffer, buffer.Offset(vaddr)};
}
@@ -619,6 +627,29 @@ bool BufferCache::IsRegionRegistered(uint64_t vaddr, uint64_t size) {
return address + m_slot_buffers[id].Size() > vaddr;
}
void BufferCache::MarkColorMetadataChecked(uint64_t vaddr, uint64_t size, uint32_t keys) {
for (uint32_t slot = 0; slot < m_checked_color_metadata.size(); slot++) {
if ((keys & (1u << slot)) != 0) {
m_checked_color_metadata[slot].Add(vaddr, size);
}
}
}
bool BufferCache::IsColorMetadataChecked(uint64_t vaddr, uint64_t size, uint32_t keys) const {
for (uint32_t slot = 0; slot < m_checked_color_metadata.size(); slot++) {
if ((keys & (1u << slot)) != 0 && !m_checked_color_metadata[slot].Contains(vaddr, size)) {
return false;
}
}
return true;
}
void BufferCache::ForgetColorMetadataChecks(uint64_t vaddr, uint64_t size) {
for (auto& checked: m_checked_color_metadata) {
checked.Subtract(vaddr, size);
}
}
bool BufferCache::IsRegionGpuModified(uint64_t vaddr, uint64_t size) {
return m_memory_tracker.IsRegionGpuModified(vaddr, size);
}
@@ -690,14 +721,16 @@ void BufferCache::ProcessFaultBuffer() {
m_fault_manager.ProcessFaultBuffer();
}
void BufferCache::SynchronizeBda(const RangeSet& mapped) {
// Take before walking: writes queued during synchronization remain pending.
const auto pending = m_memory_tracker.TakeBdaSync();
pending.ForEach([&](uint64_t begin, uint64_t end) {
mapped.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
SynchronizeBuffersInRange(start, finish - start);
});
});
void BufferCache::QueueBdaSync(uint64_t vaddr, uint64_t size) {
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
std::lock_guard lock(m_bda_sync_mutex);
m_bda_sync.Add(begin, end - begin);
}
RangeSet BufferCache::TakeBdaSync() {
std::lock_guard lock(m_bda_sync_mutex);
return std::exchange(m_bda_sync, {});
}
void BufferCache::SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size) {
+16 -9
View File
@@ -11,7 +11,9 @@
#include "graphics/host_gpu/renderer/cache/multiLevelPageTable.h"
#include "graphics/host_gpu/renderer/cache/streamBuffer.h"
#include <array>
#include <map>
#include <mutex>
#include <span>
#include <utility>
#include <vector>
@@ -79,20 +81,21 @@ public:
[[nodiscard]] bool HasGpuDirtyBytes(uint64_t vaddr, uint64_t size);
[[nodiscard]] bool IsRegionCpuModified(uint64_t vaddr, uint64_t size);
[[nodiscard]] bool IsRegionGpuModified(uint64_t vaddr, uint64_t size);
// Color metadata the GPU found free of the clear keys in `keys` (bits of code >> 5) stays so
// until a GPU write or download.
void MarkColorMetadataChecked(uint64_t vaddr, uint64_t size, uint32_t keys);
[[nodiscard]] bool IsColorMetadataChecked(uint64_t vaddr, uint64_t size, uint32_t keys) const;
void ProcessFaultBuffer();
void SynchronizeBda(const RangeSet& mapped);
void QueueBdaSync(uint64_t vaddr, uint64_t size) {
m_memory_tracker.QueueBdaSync(vaddr, size);
}
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
// Queue after the range's state change becomes visible, or a drain could consume it first.
void QueueBdaSync(uint64_t vaddr, uint64_t size);
[[nodiscard]] RangeSet TakeBdaSync();
void RunGarbageCollector();
private:
friend struct BufferCacheTestAccess;
bool IsBufferInvalid(BufferId id) const {
const auto* buffer = m_slot_buffers.try_get(id);
return buffer == nullptr || buffer->is_deleted;
}
bool IsBufferInvalid(BufferId id) const { return !id || m_slot_buffers[id].is_deleted; }
using BufferMap = std::map<uint64_t, BufferId>;
struct OverlapResult {
@@ -117,13 +120,13 @@ private:
void DeleteBuffer(BufferId id);
[[nodiscard]] bool SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size,
bool is_written, bool is_texel_buffer);
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
uint64_t total_size);
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
// Synchronous downloads publish before returning; asynchronous callers wait before reuse.
template <bool async>
[[nodiscard]] bool DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t size);
void ForgetColorMetadataChecks(uint64_t vaddr, uint64_t size);
GraphicContext& m_graphics;
CommandScheduler& m_scheduler;
@@ -135,7 +138,11 @@ private:
BufferMap m_buffers;
PageTable m_page_table;
RangeSet m_gpu_modified_ranges;
// One per TileManager::ColorClearSlots key slot.
std::array<RangeSet, 8> m_checked_color_metadata;
MemoryTracker m_memory_tracker;
std::mutex m_bda_sync_mutex;
RangeSet m_bda_sync;
StreamBuffer m_staging_buffer;
StreamBuffer m_stream_buffer;
StreamBuffer m_download_buffer;
@@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <memory>
#include <type_traits>
#include <utility>
@@ -118,6 +119,13 @@ public:
class const_iterator final {
public:
// libstdc++'s algorithms need these. Input, not forward, because operator* returns a copy.
using iterator_category = std::input_iterator_tag;
using value_type = OwnerT;
using difference_type = std::ptrdiff_t;
using pointer = void;
using reference = OwnerT;
[[nodiscard]] OwnerT operator*() const noexcept { return m_owners->At(m_index); }
const_iterator& operator++() noexcept {
++m_index;
+238 -142
View File
@@ -34,6 +34,8 @@ namespace Libs::Graphics {
namespace {
constexpr uint64_t NumFramesBeforeRemoval = 32;
// Largest GPU-written tiled image (in texels) that is written back to guest memory on submit.
constexpr uint64_t SmallTiledReadbackTexels = 64 * 64;
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
vk::ClearColorValue& clear) {
@@ -122,7 +124,8 @@ TextureCache::TextureCache(GraphicContext& graphics, CommandScheduler& scheduler
m_blit_helper(graphics, scheduler),
m_tiler(graphics, scheduler, buffer_cache.GetUtilityBuffer(MemoryUsage::Stream)),
m_buffer_cache(buffer_cache),
m_readback_linear_images(Config::ReadbackLinearImagesEnabled()) {
m_readback_linear_images(Config::ReadbackLinearImagesEnabled()),
m_readback_tiled_images(Config::ReadbackTiledImagesEnabled()) {
if (m_graphics.CanReportMemoryUsage()) {
constexpr int64_t GiB = 1024ll * 1024 * 1024;
const auto budget =
@@ -226,6 +229,7 @@ void TextureCache::UnregisterImage(ImageId id) {
if (!image.registered) {
return;
}
image.HeapChanged();
UntrackImage(id);
ImagePageTable::PageRange pages {};
if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) {
@@ -247,22 +251,25 @@ void TextureCache::UnregisterImage(ImageId id) {
}
void TextureCache::DeleteImage(ImageId id) {
auto* image = m_slot_images.try_get(id);
if (image == nullptr || !image->registered) {
auto& image = m_slot_images[id];
if (!image.registered) {
return;
}
if (image->depth_id) {
std::erase(m_slot_images[image->depth_id].stencil_ids, id);
if (image.depth_id) {
std::erase(m_slot_images[image.depth_id].stencil_ids, id);
} else {
// Detach first: deleting each association also unlinks it from its depth owner.
for (const auto association: std::exchange(image->stencil_ids, {})) {
for (const auto association: std::exchange(image.stencil_ids, {})) {
FreeImage(association);
}
}
if (image->IsGpuModified()) {
if (image.IsGpuModified()) {
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
}
m_download_images.erase(id);
if (image.info.metadata.kind == ImageMetadataKind::Htile) {
m_surface_metas.erase(image.info.metadata.range.address);
}
UnregisterImage(id);
if (m_scheduler.Active()) {
m_scheduler.DeferOperation([this, id] { m_slot_images.erase(id); });
@@ -372,6 +379,7 @@ void TextureCache::UntrackImageHead(ImageId id) {
const auto address = Common::AlignDown(begin + TRACKER_PAGE_SIZE, TRACKER_PAGE_SIZE);
const auto size = address - begin;
image.track_addr = address;
image.HeapChanged();
if (image.track_addr == image.track_addr_end) {
MarkAsMaybeDirty(id, image);
}
@@ -389,6 +397,7 @@ void TextureCache::UntrackImageTail(ImageId id) {
const auto address = Common::AlignDown(end, TRACKER_PAGE_SIZE);
const auto size = end - address;
image.track_addr_end = address;
image.HeapChanged();
if (image.track_addr == image.track_addr_end) {
MarkAsMaybeDirty(id, image);
}
@@ -398,7 +407,14 @@ void TextureCache::UntrackImageTail(ImageId id) {
}
void TextureCache::TrackImageDownload(ImageId id, Image& image) {
if (m_readback_linear_images && !image.info.IsTiled()) {
// Small tiled targets are written back too: games read GPU results such as reductions and
// simulation state from them on the CPU (PPSA04263 keeps the origin of its ocean wave grid
// in a 1x1 R32G32 target, and its water physics read it to sample the wave heights).
const auto& info = image.info;
const auto texels = static_cast<uint64_t>(info.extent.width) * info.extent.height *
info.extent.depth * info.resources.layers;
if (info.IsTiled() ? m_readback_tiled_images && texels <= SmallTiledReadbackTexels
: m_readback_linear_images) {
if (!image.IsGpuModified()) {
EXIT("TextureCache: cannot enroll a non-GPU-owned image for download\n");
}
@@ -413,12 +429,6 @@ TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64
return {};
}
uint32_t query_epoch = ++m_image_query_epoch;
if (query_epoch == 0) {
m_slot_images.ForEach([](ImageId, const Image& image) { image.query_epoch = 0; });
query_epoch = ++m_image_query_epoch;
}
ImageIds result;
ForEachPage(address, size, [&](uint64_t page) {
const auto* owners = m_image_page_table.Find(page);
@@ -426,19 +436,17 @@ TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64
return;
}
owners->ForEach([&](ImageId id) {
auto* image = m_slot_images.try_get(id);
if (image == nullptr) {
const auto& image = m_slot_images[id];
if (image.picked || !image.Overlaps(address, size, page_overlap)) {
return;
}
if (image->query_epoch == query_epoch) {
return;
}
image->query_epoch = query_epoch;
if (image->Overlaps(address, size, page_overlap)) {
result.push_back(id);
}
image.picked = true;
result.push_back(id);
});
});
for (const auto id: result) {
m_slot_images[id].picked = false;
}
return result;
}
@@ -480,16 +488,15 @@ void TextureCache::RefreshCopySource(ImageId id) {
}
bool TextureCache::CopyD16(Image& destination, Image& source) {
const bool source_depth = source.info.IsDepth();
const bool destination_depth = destination.info.IsDepth();
if (source_depth == destination_depth) {
// Converts guest D16 between a four-byte host depth aspect and two-byte color or D16 depth.
const bool source_wide = DepthAspectTransferBytes(source.backing.format) == sizeof(uint32_t);
if (source_wide == (DepthAspectTransferBytes(destination.backing.format) == sizeof(uint32_t))) {
return false;
}
auto& depth = source_depth ? source : destination;
auto& color = source_depth ? destination : source;
const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format);
auto& depth = source_wide ? source : destination;
auto& narrow = source_wide ? destination : source;
if (depth.info.bytes_per_block != sizeof(uint16_t) ||
color.info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)) {
narrow.info.bytes_per_block != sizeof(uint16_t)) {
return false;
}
EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 ||
@@ -499,42 +506,42 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
const auto layers = depth.info.resources.layers;
const uint64_t depth_slice =
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * transfer_bytes;
const uint64_t color_slice =
static_cast<uint64_t>(color.info.pitch) * color.info.extent.height * sizeof(uint16_t);
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
const auto depth_size = depth_slice * layers;
const auto color_size = color_slice * layers;
const std::array depth_copies {
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * sizeof(uint32_t);
const uint64_t narrow_slice =
static_cast<uint64_t>(narrow.info.pitch) * narrow.info.extent.height * sizeof(uint16_t);
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || narrow_slice > UINT64_MAX / layers);
const std::array depth_copies {
vk::BufferImageCopy {0,
depth.info.pitch,
depth.info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
depth.info.extent}};
const std::array color_copies {
const std::array narrow_copies {
vk::BufferImageCopy {0,
color.info.pitch,
color.info.extent.height,
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
narrow.info.pitch,
narrow.info.extent.height,
{narrow.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor,
0, 0, layers},
{},
color.info.extent}};
narrow.info.extent}};
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
auto depth_buffer = m_tiler.GetScratchBuffer(depth_slice * layers);
auto narrow_buffer = m_tiler.GetScratchBuffer(narrow_slice * layers, depth_buffer.buffer);
const TileManager::D16Layout promote_layout {
.width = depth.info.extent.width,
.height = depth.info.extent.height,
.layers = layers,
.source_row_stride = static_cast<uint64_t>(color.info.pitch) * sizeof(uint16_t),
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * transfer_bytes,
.source_slice_stride = color_slice,
.source_row_stride = static_cast<uint64_t>(narrow.info.pitch) * sizeof(uint16_t),
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * sizeof(uint32_t),
.source_slice_stride = narrow_slice,
.target_slice_stride = depth_slice,
};
const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat;
if (source_depth) {
if (source_wide) {
source.Download(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size);
m_tiler.ConvertD16(depth_buffer, color_buffer, TileManager::D16Direction::Demote, d32,
m_tiler.ConvertD16(depth_buffer, narrow_buffer, TileManager::D16Direction::Demote, d32,
{.width = promote_layout.width,
.height = promote_layout.height,
.layers = promote_layout.layers,
@@ -542,11 +549,12 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
.target_row_stride = promote_layout.source_row_stride,
.source_slice_stride = promote_layout.target_slice_stride,
.target_slice_stride = promote_layout.source_slice_stride});
destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset,
color_buffer.size);
destination.Upload(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
narrow_buffer.size);
} else {
source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size);
m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
source.Download(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
narrow_buffer.size);
m_tiler.ConvertD16(narrow_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
promote_layout);
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
depth_buffer.size);
@@ -771,18 +779,26 @@ void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) {
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
BindingType binding, ImageId cached_id,
ImageId merged_id) {
auto owner = m_slot_images.try_get(cached_id);
if (owner == nullptr || !owner->registered) {
auto& cached = m_slot_images[cached_id];
if (!cached.registered) {
return {merged_id};
}
if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) {
// PS5: image heaps keep BC images and raw block views of their layers resident together, so a
// raw read of a layer subset with the same levels views the BC image instead of evicting it.
const bool raw_view = binding == BindingType::Texture && !requested.IsBlock() &&
requested.type == cached.info.type &&
requested.data.address == cached.info.data.address &&
requested.BlockExtent() == cached.info.BlockExtent() &&
requested.resources.levels == cached.info.resources.levels &&
requested.resources.layers < cached.info.resources.layers;
if (!raw_view && BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
BcAlias::Resolve(*this, cached_id, merged_id);
return {merged_id};
}
auto& cached = *owner;
const auto current_tick = m_scheduler.CurrentTick();
const bool safe_to_delete =
current_tick - std::min(current_tick, cached.tick_accessed_last) > NumFramesBeforeRemoval;
current_tick - std::min(current_tick, cached.tick_accessed_last) > NumFramesBeforeRemoval &&
cached.heap_refs.load(std::memory_order_relaxed) == 0u;
const uint32_t requested_block = requested.bytes_per_block * requested.samples;
const uint32_t cached_block = cached.info.bytes_per_block * cached.info.samples;
@@ -852,9 +868,8 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
if (requested.pixel_format != cached.info.pixel_format ||
requested.data.size <= cached.info.data.size) {
const auto result_id = merged_id ? merged_id : cached_id;
const auto result = m_slot_images.try_get(result_id);
return {result != nullptr && ImageViewOps::FormatsCompatible(result->info.pixel_format,
requested.pixel_format)
return {ImageViewOps::FormatsCompatible(m_slot_images[result_id].info.pixel_format,
requested.pixel_format)
? result_id
: ImageId {}};
}
@@ -1033,7 +1048,7 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
std::scoped_lock lock {m_lock};
auto& image = m_slot_images[id];
image.info.metadata = desc.info.metadata;
// Native color metadata must not retain a reused HTile/CMask/FMask clear flag.
// Native color metadata must not retain a reused HTile clear flag.
m_surface_metas.erase(range.address);
if (range.size == 0 || desc.info.resources.levels != 1) {
return;
@@ -1054,12 +1069,78 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
if (first >= layers || count > layers - first) {
EXIT("TextureCache: color view exceeds its native metadata slices\n");
}
// Finish native metadata writes before reading backing bytes. This can submit the scheduler,
// so discovery runs before final draw uploads and never holds the texture lock across it.
if (m_buffer_cache.IsRegionGpuModified(range.address, range.size)) {
m_buffer_cache.ReadMemory(range.address, range.size, false);
const auto slice_size = range.size / layers;
const GuestRange viewed {range.address + slice_size * first, slice_size * count};
if (m_buffer_cache.IsRegionGpuModified(viewed.address, viewed.size)) {
std::array<vk::ClearAttachment, TileManager::ColorClearSlots> clears {};
uint32_t keys = 0;
for (uint32_t key = 0; key < clears.size(); key++) {
clears[key].aspectMask = vk::ImageAspectFlagBits::eColor;
if (DecodeColorClear(desc, static_cast<uint8_t>(key << 5u),
clears[key].clearValue.color)) {
keys |= 1u << key;
}
}
// No selected key to clear, or none left since the last check.
if (m_buffer_cache.IsColorMetadataChecked(viewed.address, viewed.size, keys)) {
return;
}
{
std::scoped_lock lock {m_lock};
auto& image = m_slot_images[id];
// Compressed images cannot upload, so they must already hold native contents.
const bool native_current = (image.usage.render_target || image.IsGpuModified()) &&
!image.IsBufferModified() && !image.IsCpuDirty();
// 2D-array attachment views select 2D layers or, of a volume, depth slices.
if (m_graphics.conditional_rendering_enabled &&
image.backing.image_type != vk::ImageType::e1D &&
count <= m_graphics.GetPhysicalDeviceProperties().limits.maxFramebufferLayers &&
(image.backing.usage & vk::ImageUsageFlagBits::eColorAttachment) &&
(m_graphics.GetFormatProperties(view.format).optimalTilingFeatures &
vk::FormatFeatureFlagBits::eColorAttachment) &&
(desc.info.metadata.compression == VideoOutCompression::Uncompressed ||
native_current)) {
// The GPU selects each slice's clear, so the CPU never waits for the metadata.
Common::DebugCounters::Add(Common::DebugCounters::Counter::ColorMetadataChecks);
InitializeImage(id);
const bool expand = desc.type != BindingType::VideoOut;
const auto [buffer, offset] =
m_buffer_cache.ObtainBuffer(viewed.address, viewed.size, expand);
const auto predicates = m_tiler.CheckColorClear(
{buffer->Handle(), offset, viewed.size}, count, keys, expand);
if (expand) {
m_buffer_cache.MarkColorMetadataChecked(viewed.address, viewed.size, keys);
}
const auto command = m_scheduler.Current().Handle();
const auto area = BeginColorRendering(image, view.format, view.base_level,
image_first, count, nullptr, command);
vk::ClearRect rect {.rect = area, .layerCount = 1};
for (uint32_t slice = 0; slice < count; slice++) {
rect.baseArrayLayer = slice;
for (uint32_t key = 0; key < clears.size(); key++) {
if ((keys & (1u << key)) != 0) {
const vk::ConditionalRenderingBeginInfoEXT predicate {
.buffer = predicates,
.offset = (slice * clears.size() + key) * sizeof(uint32_t)};
command.beginConditionalRenderingEXT(&predicate);
command.clearAttachments(1, &clears[key], 1, &rect);
command.endConditionalRenderingEXT();
}
}
}
command.endRendering();
CommitGpuWrite(image);
return;
}
}
// Finish native metadata writes before reading backing bytes. This can submit the
// scheduler, so discovery runs before final draw uploads and never holds the texture
// lock across it.
Common::DebugCounters::ScopedTimer timer(
Common::DebugCounters::Counter::ColorMetadataReadbacks,
Common::DebugCounters::Counter::ColorMetadataReadbackNs);
m_buffer_cache.ReadMemory(viewed.address, viewed.size, false);
}
const auto slice_size = range.size / layers;
for (uint32_t slice = 0; slice < count; slice++) {
const auto address = range.address + slice_size * (first + slice);
uint8_t code = 0;
@@ -1135,9 +1216,8 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
ImageId association {};
for (const auto id: FindImagesInRegion(stencil.address, stencil.size, false)) {
const auto owner = m_slot_images.try_get(id);
if (owner != nullptr && owner->info.data == stencil &&
owner->info.extent == depth.info.extent) {
const auto& owner = m_slot_images[id];
if (owner.info.data == stencil && owner.info.extent == depth.info.extent) {
association = id;
}
}
@@ -1230,12 +1310,24 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
view_layer = overlap.layer;
}
}
// PS5: image heaps hold a descriptor per dynamic-resolution extent of a render target,
// and only one extent at an address holds data, so heap lookups create no second one.
if (!result && desc.heap &&
std::any_of(candidates.begin(), candidates.end(), [&](ImageId id) {
const auto* image = m_slot_images.try_get(id);
return image != nullptr && image->registered &&
image->heap_refs.load(std::memory_order_relaxed) != 0u &&
image->info.data.address == desc.info.data.address &&
image->info.type == desc.info.type &&
image->info.extent != desc.info.extent;
})) {
return {};
}
} else if (block_alias) {
for (const auto candidate: candidates) {
const auto* owner = m_slot_images.try_get(candidate);
if (candidate != result && owner != nullptr && owner->registered &&
!owner->IsCpuDirty() &&
BcAlias::Matches(m_slot_images[result].info, owner->info)) {
const auto& owner = m_slot_images[candidate];
if (candidate != result && owner.registered && !owner.IsCpuDirty() &&
BcAlias::Matches(m_slot_images[result].info, owner.info)) {
BcAlias::Resolve(*this, candidate, result);
}
}
@@ -1298,14 +1390,14 @@ ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool e
std::scoped_lock lock {m_lock};
ImageIds matches;
for (const auto id: FindImagesInRegion(address, size, false)) {
auto owner = m_slot_images.try_get(id);
if (owner == nullptr || owner->info.data.address != address) {
const auto* owner = &m_slot_images[id];
if (owner->info.data.address != address) {
continue;
}
if (ensure_valid && owner->depth_id) {
owner = m_slot_images.try_get(owner->depth_id);
owner = &m_slot_images[owner->depth_id];
}
if (owner == nullptr || (ensure_valid && !owner->SafeToDownload())) {
if (ensure_valid && !owner->SafeToDownload()) {
continue;
}
matches.push_back(id);
@@ -1322,11 +1414,8 @@ ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool e
}
}
}
if (selected && ensure_valid) {
const auto owner = m_slot_images.try_get(selected);
if (owner != nullptr && owner->depth_id) {
selected = owner->depth_id;
}
if (selected && ensure_valid && m_slot_images[selected].depth_id) {
selected = m_slot_images[selected].depth_id;
}
return selected;
}
@@ -1347,9 +1436,8 @@ vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) {
desc.info.data.End() <= image.info.stencil.End()) {
for (const auto stencil_id:
FindImagesInRegion(image.info.stencil.address, image.info.stencil.size, false)) {
const auto* stencil = m_slot_images.try_get(stencil_id);
if (stencil != nullptr && stencil->depth_id == id &&
stencil->info.data == image.info.stencil) {
const auto& stencil = m_slot_images[stencil_id];
if (stencil.depth_id == id && stencil.info.data == image.info.stencil) {
RefreshImage(stencil_id);
break;
}
@@ -1411,11 +1499,8 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
image.info.metadata = desc.info.metadata;
if (desc.info.HasMetadata()) {
const auto [metadata, inserted] = m_surface_metas.try_emplace(desc.info.metadata.range.address);
if (inserted) {
metadata->second.type = MetaDataInfo::Type::HTile;
if (image.info.htile_initial_clear) {
metadata->second.clear_mask.set();
}
if (inserted && image.info.htile_initial_clear) {
metadata->second.clear_mask.set();
}
}
RefreshImage(id);
@@ -1463,27 +1548,24 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
vk::ImageAspectFlags aspect {};
ImageSubresourceRange subresources;
for (const auto id: FindImagesInRegion(address, size, false)) {
auto owner = m_slot_images.try_get(id);
if (owner == nullptr) {
continue;
}
const auto& owner = m_slot_images[id];
vk::ImageAspectFlags candidate {};
ImageId candidate_id = id;
ImageSubresourceRange candidate_subresources {
0, owner->info.resources.levels, 0, owner->info.TransferLayers()};
if (owner->depth_id && owner->info.data.address == address &&
owner->info.data.size == size) {
0, owner.info.resources.levels, 0, owner.info.TransferLayers()};
if (owner.depth_id && owner.info.data.address == address &&
owner.info.data.size == size) {
candidate = vk::ImageAspectFlagBits::eStencil;
candidate_id = owner->depth_id;
candidate_subresources = owner->stencil_subresources;
owner = m_slot_images.try_get(candidate_id);
if (owner == nullptr || owner->backing.image == nullptr || !owner->info.HasStencil()) {
candidate_id = owner.depth_id;
candidate_subresources = owner.stencil_subresources;
const auto& depth = m_slot_images[candidate_id];
if (depth.backing.image == nullptr || !depth.info.HasStencil()) {
continue;
}
} else if (!owner->depth_id && owner->info.data.address == address &&
owner->info.data.size == size) {
candidate = owner->info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor;
} else if (!owner.depth_id && owner.info.data.address == address &&
owner.info.data.size == size) {
candidate = owner.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor;
}
if (!candidate) {
continue;
@@ -1564,29 +1646,8 @@ void TextureCache::ClearImage(CommandBuffer& command, ImageId id, vk::Format for
if (format != image.backing.format || (image.info.IsVolume() && !full_image)) {
EXIT_NOT_IMPLEMENTED(range.aspectMask != vk::ImageAspectFlagBits::eColor ||
range.levelCount != 1);
ImageViewInfo view {};
view.format = format;
view.type = range.layerCount == 1 ? vk::ImageViewType::e2D : vk::ImageViewType::e2DArray;
view.base_level = range.baseMipLevel;
view.base_layer = range.baseArrayLayer;
view.layer_count = range.layerCount;
view.usage = vk::ImageUsageFlagBits::eColorAttachment;
image.Transit(vk::ImageLayout::eColorAttachmentOptimal,
vk::AccessFlagBits2::eColorAttachmentWrite, {}, command.Handle());
vk::RenderingAttachmentInfo attachment {};
attachment.imageView = image.FindView(view);
attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
attachment.loadOp = vk::AttachmentLoadOp::eClear;
attachment.storeOp = vk::AttachmentStoreOp::eStore;
attachment.clearValue = clear;
vk::RenderingInfo rendering {};
rendering.renderArea.extent = {
std::max(image.info.extent.width >> range.baseMipLevel, 1u),
std::max(image.info.extent.height >> range.baseMipLevel, 1u)};
rendering.layerCount = range.layerCount;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
command.Handle().beginRendering(&rendering);
BeginColorRendering(image, format, range.baseMipLevel, range.baseArrayLayer,
range.layerCount, &clear, command.Handle());
command.Handle().endRendering();
CommitGpuWrite(image);
return;
@@ -1609,6 +1670,40 @@ void TextureCache::ClearImage(CommandBuffer& command, ImageId id, vk::Format for
CommitGpuWrite(image);
}
vk::Rect2D TextureCache::BeginColorRendering(Image& image, vk::Format format, uint32_t level,
uint32_t layer, uint32_t layers,
const vk::ClearValue* clear,
vk::CommandBuffer command) {
ImageViewInfo view {};
view.format = format;
view.type = layers == 1 ? vk::ImageViewType::e2D : vk::ImageViewType::e2DArray;
view.base_level = level;
view.base_layer = layer;
view.layer_count = layers;
view.usage = vk::ImageUsageFlagBits::eColorAttachment;
image.Transit(vk::ImageLayout::eColorAttachmentOptimal,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite,
{}, command);
vk::RenderingAttachmentInfo attachment {};
attachment.imageView = image.FindView(view);
attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
attachment.loadOp = vk::AttachmentLoadOp::eLoad;
attachment.storeOp = vk::AttachmentStoreOp::eStore;
if (clear != nullptr) {
attachment.loadOp = vk::AttachmentLoadOp::eClear;
attachment.clearValue = *clear;
}
vk::RenderingInfo rendering {};
rendering.renderArea.extent = {std::max(image.info.extent.width >> level, 1u),
std::max(image.info.extent.height >> level, 1u)};
rendering.layerCount = layers;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
command.beginRendering(&rendering);
return rendering.renderArea;
}
void TextureCache::InvalidateMemory(uint64_t address, uint64_t size) {
if (!GuestRange {address, size}.Valid()) {
EXIT("TextureCache: invalid memory-invalidation range\n");
@@ -1742,6 +1837,7 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uin
copy_size != image.info.data.size)) {
return false;
}
ForgetColorMetadataChecks(image.info.data.address, copy_size);
m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, levels);
return true;
}
@@ -1810,23 +1906,20 @@ void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
const auto page_begin = Common::AlignDown(address, TRACKER_PAGE_SIZE);
const auto page_end = Common::AlignUp(address + size, TRACKER_PAGE_SIZE);
for (const auto id: FindImagesInRegion(address, size, true)) {
auto owner = m_slot_images.try_get(id);
if (owner == nullptr) {
continue;
}
if (owner->Overlaps(address, size)) {
owner->MarkCpuDirty();
auto& owner = m_slot_images[id];
if (owner.Overlaps(address, size)) {
owner.MarkCpuDirty();
UntrackImage(id);
continue;
}
const auto image_begin = owner->info.data.address;
const auto image_end = owner->info.data.End();
const auto image_begin = owner.info.data.address;
const auto image_end = owner.info.data.End();
if (page_end < image_end) {
UntrackImageHead(id);
} else if (image_begin < page_begin) {
UntrackImageTail(id);
} else {
MarkAsMaybeDirty(id, *owner);
MarkAsMaybeDirty(id, owner);
}
}
}
@@ -1876,7 +1969,6 @@ void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
EXIT("TextureCache: invalid unmap range\n");
}
std::scoped_lock lock {m_lock};
// HTILE belongs to guest memory, independently of native image replacements.
for (auto metadata = m_surface_metas.begin(); metadata != m_surface_metas.end();) {
const auto base = metadata->first;
if (base >= address && base < address + size) {
@@ -1926,6 +2018,11 @@ void TextureCache::RunGarbageCollector() {
if (owner == nullptr || !owner->registered || owner->depth_id) {
continue;
}
if (owner->heap_refs.load(std::memory_order_relaxed) != 0u) {
// Resident heaps sample it without touching it.
TouchImage(*owner);
continue;
}
if (owner->IsGpuModified()) {
const bool safe = owner->SafeToDownload();
if (safe && owner->info.IsTiled()) {
@@ -1957,13 +2054,12 @@ void TextureCache::RunGarbageCollector() {
}
void TextureCache::ProcessDownloadImages() {
if (!m_readback_linear_images) {
if (!m_readback_linear_images && !m_readback_tiled_images) {
return;
}
std::scoped_lock lock {m_lock};
for (const auto id: m_download_images) {
const auto owner = m_slot_images.try_get(id);
if (owner != nullptr && owner->registered && owner->IsGpuModified()) {
if (m_slot_images[id].IsGpuModified()) {
(void)DownloadImageMemory(id);
}
}
+9 -4
View File
@@ -43,6 +43,7 @@ public:
ImageInfo info;
ImageViewInfo view_info;
BindingType type = BindingType::Texture;
bool heap = false; // PS5: resolved for a resident image heap.
};
TextureCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager,
@@ -81,9 +82,6 @@ public:
private:
struct MetaDataInfo {
enum class Type : uint8_t { CMask, FMask, HTile };
Type type;
// PS5 DB_DEPTH_VIEW encodes 13-bit base and last array-slice indices.
std::bitset<8192> clear_mask;
};
@@ -153,6 +151,12 @@ private:
// Caller holds m_lock. Volume layer ranges select depth slices.
void ClearImage(CommandBuffer& command, ImageId id, vk::Format format,
const vk::ImageSubresourceRange& range, const vk::ClearValue& clear);
// Caller holds m_lock and ends rendering. Begins rendering `layers` layers of one color mip
// through a `format` view, clearing them or, when `clear` is null, loading them; returns the
// render area.
vk::Rect2D BeginColorRendering(Image& image, vk::Format format, uint32_t level,
uint32_t layer, uint32_t layers, const vk::ClearValue* clear,
vk::CommandBuffer command);
void PrepareImageCopy(Image& image);
void RefreshCopySource(ImageId id);
[[nodiscard]] bool CopyD16(Image& destination, Image& source);
@@ -181,12 +185,13 @@ private:
uint64_t m_pressure_gc_memory = 1536ull * 1024 * 1024;
uint64_t m_critical_gc_memory = 3ull * 1024 * 1024 * 1024;
uint64_t m_gc_tick = 0;
mutable uint32_t m_image_query_epoch = 0;
bool m_readback_linear_images = false;
bool m_readback_tiled_images = false;
friend struct TextureCacheTestAccess;
friend class BufferCache;
friend class RenderExecutor;
friend class ImageHeap;
friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&);
friend bool BcAlias::Publish(TextureCache&, Image&);
friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId);
@@ -72,7 +72,16 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
return;
}
const auto samples = render_sample_count(rt.attrib.num_fragments);
if (samples == 0 || rt.attrib.num_samples != rt.attrib.num_fragments) {
// TODO(PPSA24156): Replace this 2x MSAA approximation with proper 4-coverage/2-fragment EQAA.
const bool approximate_eqaa = rt.attrib.num_samples == 2 && rt.attrib.num_fragments == 1;
if (approximate_eqaa) {
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: approximating 4-coverage/2-fragment EQAA with 2x MSAA.\n");
}
}
if (samples == 0 || (rt.attrib.num_samples != rt.attrib.num_fragments && !approximate_eqaa)) {
EXIT("unsupported render-target sample configuration: samples=%u fragments=%u\n",
rt.attrib.num_samples, rt.attrib.num_fragments);
}
@@ -220,8 +229,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
}
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
!rt.info.fmask_compression_enable &&
rt.cmask.addr != 0 &&
!rt.attrib3.write_vrs_rate_hint_to_cmask;
if (has_dcc || has_cmask) {
TileSizeAlign metadata_size {};
@@ -233,7 +241,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
} else {
(void)TileGetCmaskSize(width, height, layers, levels, metadata_size);
}
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
// DCC owns the clear when both planes are enabled.
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
desc.info.metadata.clear_word =
@@ -327,7 +327,7 @@ void CommandScheduler::CheckActive() const {
}
CommandBuffer& CommandScheduler::Current() {
CheckActive();
EXIT_IF(m_command.IsInvalid());
return m_command;
}
+7 -1
View File
@@ -104,8 +104,14 @@ void CommandBuffer::BeginRendering(const RenderState& state) const {
rendering.pDepthAttachment = depth_stencil.has_depth ? &depth : nullptr;
rendering.pStencilAttachment = depth_stencil.has_stencil ? &stencil : nullptr;
Handle().beginRendering(rendering);
// The open pass holds the cleared contents, so a later clear begins a new pass.
m_render_state = state;
m_rendering = true;
for (auto& color: m_render_state.color_attachments) {
color.is_clear = false;
}
m_render_state.depth_stencil_attachment.depth_clear = false;
m_render_state.depth_stencil_attachment.stencil_clear = false;
m_rendering = true;
}
void CommandBuffer::EndRendering() const {
+3 -21
View File
@@ -294,16 +294,6 @@ static void RtCheck(const HW::RenderTarget& rt) {
EXIT_NOT_IMPLEMENTED(rt.dcc.overwrite_combiner_disable != false);
// EXIT_NOT_IMPLEMENTED(rt.dcc.data_write_on_dcc_clear_to_reg != false);
EXIT_NOT_IMPLEMENTED(rt.dcc.dcc_clear_key_enable != false);
if (rt.cmask.addr != 0x0000000000000000 || rt.fmask.addr != 0x0000000000000000 ||
rt.dcc_addr.addr != 0x0000000000000000) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: ignoring PS5 metadata addresses cmask=0x%016" PRIx64
" fmask=0x%016" PRIx64 " dcc=0x%016" PRIx64 "\n",
rt.cmask.addr, rt.fmask.addr, rt.dcc_addr.addr);
logged = true;
}
}
}
}
@@ -383,10 +373,9 @@ static void ClipPrint(const char* func, const HW::ClipControl& c) {
static void ClipCheck(const HW::ClipControl& c) {
// dx_linear_attr_clip_enable preserves linear (noperspective) attributes at clip-generated
// vertices, which Vulkan provides as part of clipping and interpolation.
EXIT_NOT_IMPLEMENTED(c.user_clip_planes != 0 || c.user_clip_plane_mode != 0 ||
c.vertex_kill_any || c.user_clip_plane_negate_y ||
c.user_clip_plane_cull_only || c.cull_on_clipping_error_disable ||
c.force_viewport_index_from_vs_enable);
EXIT_NOT_IMPLEMENTED(c.user_clip_plane_mode != 0 || c.vertex_kill_any ||
c.user_clip_plane_negate_y || c.user_clip_plane_cull_only ||
c.cull_on_clipping_error_disable || c.force_viewport_index_from_vs_enable);
}
static void RcPrint(const char* func, const HW::RenderControl& c) {
@@ -606,13 +595,6 @@ void LogDrawPhase(const char* draw_name, const char* phase) {
}
}
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id) {
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: %s VS=%" PRIu64 " PS=%" PRIu64 "\n", phase, vertex_program_id,
pixel_program_id);
}
}
static void VpPrint(const char* func, const HW::ScreenViewport& vp,
const HW::ScanModeControl& smc) {
LOGF("%s\n", func);
@@ -125,6 +125,14 @@ void DynamicState::Commit(const GraphicContext& graphics, vk::CommandBuffer cmdb
}
}
}
if (dirty_state.cull_mode) {
dirty_state.cull_mode = false;
cmdbuf.setCullMode(cull_mode);
}
if (dirty_state.front_face) {
dirty_state.front_face = false;
cmdbuf.setFrontFace(front_face);
}
if (dirty_state.blend_constants) {
dirty_state.blend_constants = false;
cmdbuf.setBlendConstants(blend_constants.data());
@@ -52,6 +52,9 @@ struct DynamicState {
bool stencil_back_write_mask : 1;
bool stencil_back_compare_mask : 1;
bool cull_mode : 1;
bool front_face : 1;
bool blend_constants : 1;
bool color_write_enables : 1;
bool line_width : 1;
@@ -86,6 +89,9 @@ struct DynamicState {
uint32_t stencil_back_write_mask {};
uint32_t stencil_back_compare_mask {};
vk::CullModeFlags cull_mode {};
vk::FrontFace front_face {};
std::array<float, 4> blend_constants {};
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
uint32_t num_color_write_enables {};
@@ -221,6 +227,20 @@ struct DynamicState {
}
}
void SetCullMode(const vk::CullModeFlags cull_mode_) {
if (cull_mode != cull_mode_) {
cull_mode = cull_mode_;
dirty_state.cull_mode = true;
}
}
void SetFrontFace(const vk::FrontFace front_face_) {
if (front_face != front_face_) {
front_face = front_face_;
dirty_state.front_face = true;
}
}
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
if (blend_constants != blend_constants_) {
blend_constants = blend_constants_;
+26 -12
View File
@@ -30,16 +30,18 @@ namespace {
}
}
[[nodiscard]] vk::ImageCreateFlags ImageCreateFlags(const GraphicContext& graphics,
const ImageInfo& info) {
[[nodiscard]] vk::ImageCreateFlags
ImageCreateFlags(const GraphicContext& graphics, const ImageInfo& info, vk::ImageUsageFlags usage) {
vk::ImageCreateFlags flags {};
if (DepthAspectTransferFormat(info.pixel_format) == vk::Format::eUndefined) {
flags |= vk::ImageCreateFlagBits::eMutableFormat;
flags |= vk::ImageCreateFlagBits::eExtendedUsage;
if (info.IsBlock() && graphics.supports_block_texel_view) {
flags |= vk::ImageCreateFlagBits::eBlockTexelViewCompatible;
}
}
if (usage & vk::ImageUsageFlagBits::eStorage) {
flags |= vk::ImageCreateFlagBits::eExtendedUsage;
}
if (info.IsVolume()) {
flags |= vk::ImageCreateFlagBits::e2DArrayCompatible;
}
@@ -59,7 +61,7 @@ namespace {
const auto storage = usage | vk::ImageUsageFlagBits::eStorage;
if (graphics.GetImageFormatProperties(info.pixel_format, HostImageType(info.type),
vk::ImageTiling::eOptimal, storage,
ImageCreateFlags(graphics, info),
ImageCreateFlags(graphics, info, storage),
nullptr) == vk::Result::eSuccess) {
usage = storage;
} else {
@@ -88,7 +90,8 @@ namespace {
if (HasFormatFeature(properties, vk::FormatFeatureFlagBits::eColorAttachment)) {
usage |= vk::ImageUsageFlagBits::eColorAttachment;
}
if (info.samples == 1) {
if (graphics.GetPhysicalDeviceProperties().limits.storageImageSampleCounts &
static_cast<vk::SampleCountFlagBits>(info.samples)) {
usage |= vk::ImageUsageFlagBits::eStorage;
}
return usage;
@@ -209,6 +212,13 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout destinat
barriers.push_back(barrier);
}
// Resident heaps sample their images in the read state; only leaving it invalidates them.
if (std::ranges::any_of(barriers, [&](const vk::ImageMemoryBarrier2& barrier) {
return barrier.oldLayout == ReadLayout() &&
barrier.srcAccessMask == vk::AccessFlagBits2::eShaderRead;
})) {
HeapChanged();
}
state = {destination_stage, destination_access, destination_layout};
return barriers;
}
@@ -353,8 +363,12 @@ void Image::CopyImage(Image& source) {
std::vector<vk::ImageCopy> copies;
copies.reserve(levels);
for (uint32_t level = 0; level < levels; level++) {
const auto width = std::max(source.backing.extent.width >> level, 1u);
const auto height = std::max(source.backing.extent.height >> level, 1u);
// Overlapping cache images can differ in size; copying the source extent into a smaller
// destination writes out of bounds and loses the device.
const auto width = std::min(std::max(source.backing.extent.width >> level, 1u),
std::max(backing.extent.width >> level, 1u));
const auto height = std::min(std::max(source.backing.extent.height >> level, 1u),
std::max(backing.extent.height >> level, 1u));
const auto depth = std::max(base_depth >> level, 1u);
const auto [source_layers, destination_layers] = SanitizeCopyLayers(source, *this, depth);
vk::ImageCopy copy {};
@@ -411,15 +425,15 @@ void Image::Resolve(Image& source, const ImageSubresourceRange& source_range,
const auto destination_height =
std::max(backing.extent.height >> destination_range.base_level, 1u);
const bool copy = source.backing.samples == 1;
EXIT_IF(layers == 0 || info.extent.width > source_width || info.extent.height > source_height ||
info.extent.width > destination_width || info.extent.height > destination_height ||
EXIT_IF(layers == 0 ||
(copy ? !ImageViewOps::FormatsCompatible(source.backing.format, backing.format)
: source.backing.format != backing.format));
auto resolved_source_range = source_range;
auto resolved_destination_range = destination_range;
resolved_source_range.layer_count = layers;
resolved_destination_range.layer_count = layers;
const vk::Extent3D resolve_extent {info.extent.width, info.extent.height, 1};
const vk::Extent3D resolve_extent {std::min(source_width, destination_width),
std::min(source_height, destination_height), 1};
m_scheduler.EndRendering();
auto command = m_scheduler.Current().Handle();
@@ -698,7 +712,8 @@ Image::Image(GraphicContext& graphics, CommandScheduler& scheduler, const ImageI
}
vk::ImageCreateInfo create {};
create.flags = ImageCreateFlags(graphics, info);
create.usage = ImageUsageFlags(graphics, info);
create.flags = ImageCreateFlags(graphics, info, create.usage);
create.imageType = HostImageType(info.type);
create.extent = info.extent;
create.mipLevels = info.resources.levels;
@@ -706,7 +721,6 @@ Image::Image(GraphicContext& graphics, CommandScheduler& scheduler, const ImageI
create.format = info.pixel_format;
create.tiling = vk::ImageTiling::eOptimal;
create.initialLayout = vk::ImageLayout::eUndefined;
create.usage = ImageUsageFlags(graphics, info);
create.samples = vulkan_sample_count(info.samples);
vk::ImageFormatProperties properties {};
+26 -2
View File
@@ -7,6 +7,7 @@
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/image/imageInfo.h"
#include <atomic>
#include <compare>
#include <limits>
#include <optional>
@@ -71,6 +72,7 @@ public:
void MarkCpuDirty() {
m_cpu_dirty = true;
m_maybe_cpu_dirty = false;
HeapChanged();
}
[[nodiscard]] bool IsCpuDirty() const { return m_cpu_dirty || m_maybe_cpu_dirty; }
@@ -80,6 +82,7 @@ public:
if (!IsCpuDirty()) {
m_maybe_cpu_hash = HashGuestEdges();
m_maybe_cpu_dirty = true;
HeapChanged();
}
}
[[nodiscard]] bool ResolveMaybeCpuHash(uint64_t hash) {
@@ -104,7 +107,10 @@ public:
void ClearGpuModified() noexcept { m_gpu_modified = false; }
[[nodiscard]] bool IsBufferModified() const noexcept { return m_buffer_modified; }
void MarkBufferModified() noexcept { m_buffer_modified = true; }
void MarkBufferModified() noexcept {
m_buffer_modified = true;
HeapChanged();
}
void ClearBufferModified() noexcept { m_buffer_modified = false; }
[[nodiscard]] bool Overlaps(uint64_t address, uint64_t size,
@@ -120,14 +126,30 @@ public:
return backing.image == nullptr ? 0 : Common::AlignUp(info.data.size, 1024);
}
[[nodiscard]] uint64_t HashGuestEdges() const;
[[nodiscard]] vk::ImageLayout ReadLayout() const noexcept {
return info.IsDepth() ? vk::ImageLayout::eDepthStencilReadOnlyOptimal
: vk::ImageLayout::eShaderReadOnlyOptimal;
}
// Resident image heaps sample an image without rediscovering it until one changes its
// contents, tracking, identity or layout.
void HeapChanged() const noexcept {
if (heap_refs.load(std::memory_order_relaxed) != 0u) {
heap_epoch.fetch_add(1u, std::memory_order_relaxed);
}
}
static inline std::atomic<uint64_t> heap_epoch {0};
static inline std::atomic<uint64_t> serials {0};
// Unique per image, unlike ImageId: freed slots are reused.
const uint64_t serial = serials.fetch_add(1u, std::memory_order_relaxed) + 1u;
ImageInfo info;
VulkanImage backing;
std::vector<CachedImageView> views;
ImageUsage usage;
ImageBinding binding;
bool registered = false;
mutable uint32_t query_epoch = 0;
mutable bool picked = false; // Set only while FindImagesInRegion collects it.
uint64_t track_addr = 0;
uint64_t track_addr_end = 0;
ImageId depth_id {};
@@ -137,6 +159,8 @@ public:
ImageSubresourceRange stencil_subresources;
uint64_t tick_accessed_last = 0;
size_t lru_id = 0;
// Resident image heap elements that select this image.
std::atomic<uint32_t> heap_refs {0};
private:
friend struct ImageTestAccess;
@@ -378,6 +378,9 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto unorm16 = [](uint64_t value) {
return static_cast<float>(value & 0xffffu) / 65535.0f;
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
@@ -403,19 +406,49 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
case vk::Format::eR16G16B16A16Unorm:
next.float32[2] = unorm16(clear_words >> 32u);
next.float32[3] = unorm16(clear_words >> 48u);
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
case vk::Format::eR16G16Unorm:
next.float32[1] = unorm16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Unorm: next.float32[0] = unorm16(packed); break;
case vk::Format::eR16G16B16A16Uint:
for (uint32_t i = 0; i < 4; i++) {
next.uint32[i] = static_cast<uint16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit float channel.
case vk::Format::eR16G16B16A16Sint:
for (uint32_t i = 0; i < 4; i++) {
next.int32[i] = static_cast<int16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit channel.
case vk::Format::eR32G32Sfloat:
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
[[fallthrough]];
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
case vk::Format::eR32G32Uint:
case vk::Format::eR32G32Sint:
next.uint32[1] = static_cast<uint32_t>(clear_words >> 32u);
[[fallthrough]];
case vk::Format::eR32Uint:
case vk::Format::eR32Sint: next.uint32[0] = packed; break;
// Narrow targets hold their channels in the low bits of the first word. UFC 6 (PPSA23566)
// fast-clears an R8_UINT map through CMASK every frame and then writes only part of it;
// without these the clear was dropped and old contents piled up.
case vk::Format::eR8Uint: next.uint32[0] = packed & 0xffu; break;
case vk::Format::eR8Unorm: next.float32[0] = unorm8(packed); break;
case vk::Format::eR8G8Uint:
next.uint32[0] = packed & 0xffu;
next.uint32[1] = (packed >> 8u) & 0xffu;
break;
case vk::Format::eR8G8Unorm:
next.float32[0] = unorm8(packed);
next.float32[1] = unorm8(packed >> 8u);
break;
case vk::Format::eR16Uint: next.uint32[0] = packed & 0xffffu; break;
case vk::Format::eR8G8B8A8Srgb:
next.float32[0] = srgb8(packed);
next.float32[1] = srgb8(packed >> 8u);
@@ -462,10 +495,16 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm ||
format == vk::Format::eR16Uint || format == vk::Format::eR8G8Unorm ||
format == vk::Format::eR8G8Uint) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if ((format == vk::Format::eR8Unorm || format == vk::Format::eR8Uint) &&
packed != (packed & 0xffu) * 0x01010101u) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -101,6 +101,7 @@ IsSupportedStorageImageResource(const ShaderRecompiler::IR::ImageResource& resou
(resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1D ||
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray ||
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D ||
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa ||
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim3D ||
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray) &&
((resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None &&
+88 -74
View File
@@ -14,6 +14,7 @@
#include "gpu_tiler_shaders/gpu_tiler_standard64_3d_spv.h"
#include "gpu_tiler_shaders/gpu_tiler_standard64_spv.h"
#include "gpu_tiler_shaders/gpu_tiler_color_transform_spv.h"
#include "gpu_tiler_shaders/gpu_tiler_color_clear_spv.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/cache/streamBuffer.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
@@ -71,20 +72,11 @@ TileManager::~TileManager() {
m_graphics.device.destroyPipeline(pipeline, nullptr);
}
}
if (m_d16_to_d24 != nullptr) {
m_graphics.device.destroyPipeline(m_d16_to_d24, nullptr);
}
if (m_d16_to_d32 != nullptr) {
m_graphics.device.destroyPipeline(m_d16_to_d32, nullptr);
}
if (m_d24_to_d16 != nullptr) {
m_graphics.device.destroyPipeline(m_d24_to_d16, nullptr);
}
if (m_d32_to_d16 != nullptr) {
m_graphics.device.destroyPipeline(m_d32_to_d16, nullptr);
}
if (m_color_transform != nullptr) {
m_graphics.device.destroyPipeline(m_color_transform, nullptr);
for (auto pipeline: {m_d16_to_d24, m_d16_to_d32, m_d24_to_d16, m_d32_to_d16, m_color_transform,
m_color_clear}) {
if (pipeline != nullptr) {
m_graphics.device.destroyPipeline(pipeline, nullptr);
}
}
if (m_pipeline_layout != nullptr) {
m_graphics.device.destroyPipelineLayout(m_pipeline_layout, nullptr);
@@ -220,42 +212,37 @@ vk::Pipeline TileManager::GetPipeline(uint32_t slot) {
if (m_pipelines[slot] != nullptr) {
return m_pipelines[slot];
}
struct Shader {
const uint32_t* code;
size_t words;
};
static constexpr std::array<Shader, FamilyCount> shaders {{
{GPU_TILER_STANDARD256_SPV, std::size(GPU_TILER_STANDARD256_SPV)},
{GPU_TILER_STANDARD4_SPV, std::size(GPU_TILER_STANDARD4_SPV)},
{GPU_TILER_STANDARD4_3D_SPV, std::size(GPU_TILER_STANDARD4_3D_SPV)},
{GPU_TILER_STANDARD64_SPV, std::size(GPU_TILER_STANDARD64_SPV)},
{GPU_TILER_STANDARD64_3D_SPV, std::size(GPU_TILER_STANDARD64_3D_SPV)},
{GPU_TILER_PRT_SPV, std::size(GPU_TILER_PRT_SPV)},
{GPU_TILER_PRT_3D_SPV, std::size(GPU_TILER_PRT_3D_SPV)},
{GPU_TILER_RENDER_TARGET_SPV, std::size(GPU_TILER_RENDER_TARGET_SPV)},
{GPU_TILER_DEPTH_SPV, std::size(GPU_TILER_DEPTH_SPV)},
}};
const uint32_t element_index = slot % BytesPerElementCount;
static constexpr std::array<std::span<const uint32_t>, FamilyCount> shaders {
GPU_TILER_STANDARD256_SPV, GPU_TILER_STANDARD4_SPV, GPU_TILER_STANDARD4_3D_SPV,
GPU_TILER_STANDARD64_SPV, GPU_TILER_STANDARD64_3D_SPV, GPU_TILER_PRT_SPV,
GPU_TILER_PRT_3D_SPV, GPU_TILER_RENDER_TARGET_SPV, GPU_TILER_DEPTH_SPV};
const uint32_t element_index = slot % BytesPerElementCount;
const uint32_t direction_index = slot / (FamilyCount * BytesPerElementCount);
const uint32_t family_index = (slot / BytesPerElementCount) % FamilyCount;
const uint32_t values[] {1u << element_index, direction_index};
const vk::SpecializationMapEntry entries[] {{0, 0, 4}, {1, 4, 4}};
const vk::SpecializationInfo specialization {2, entries, sizeof(values), values};
const auto module =
CompileSPV({shaders[family_index].code, shaders[family_index].words}, m_graphics.device);
m_pipelines[slot] = CreatePipeline(shaders[family_index], &specialization);
return m_pipelines[slot];
}
vk::Pipeline TileManager::CreatePipeline(std::span<const uint32_t> code,
const vk::SpecializationInfo* specialization) const {
const auto module = CompileSPV(code, m_graphics.device);
vk::PipelineShaderStageCreateInfo stage {};
stage.stage = vk::ShaderStageFlagBits::eCompute;
stage.module = module;
stage.pName = "main";
stage.pSpecializationInfo = &specialization;
stage.pSpecializationInfo = specialization;
vk::ComputePipelineCreateInfo create {};
create.stage = stage;
create.layout = m_pipeline_layout;
const auto result =
m_graphics.device.createComputePipelines(nullptr, 1, &create, nullptr, &m_pipelines[slot]);
create.stage = stage;
create.layout = m_pipeline_layout;
vk::Pipeline pipeline = nullptr;
const auto result =
m_graphics.device.createComputePipelines(nullptr, 1, &create, nullptr, &pipeline);
m_graphics.device.destroyShaderModule(module, nullptr);
RequireVulkanSuccess(result, "create TileManager pipeline");
return m_pipelines[slot];
return pipeline;
}
void TileManager::Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
@@ -434,39 +421,21 @@ uint32_t TileManager::ConversionRows(uint64_t offset, uint64_t row_stride, uint6
void TileManager::ConvertD16(Result source, Result target, D16Direction direction, bool d32,
const D16Layout& layout) {
vk::Pipeline* pipeline_pointer = nullptr;
vk::Pipeline* pipeline_pointer = nullptr;
std::span<const uint32_t> code;
if (direction == D16Direction::Promote) {
pipeline_pointer = d32 ? &m_d16_to_d32 : &m_d16_to_d24;
code = GPU_TILER_PROMOTE_D16_SPV;
} else {
pipeline_pointer = d32 ? &m_d32_to_d16 : &m_d24_to_d16;
code = GPU_TILER_DEMOTE_D16_SPV;
}
auto& pipeline = *pipeline_pointer;
if (pipeline == nullptr) {
const uint32_t value = d32 ? 1u : 0u;
const vk::SpecializationMapEntry entry {0, 0, sizeof(value)};
const vk::SpecializationInfo specialization {1, &entry, sizeof(value), &value};
const uint32_t* code = nullptr;
size_t words = 0;
if (direction == D16Direction::Promote) {
code = GPU_TILER_PROMOTE_D16_SPV;
words = std::size(GPU_TILER_PROMOTE_D16_SPV);
} else {
code = GPU_TILER_DEMOTE_D16_SPV;
words = std::size(GPU_TILER_DEMOTE_D16_SPV);
}
const auto module = CompileSPV({code, words}, m_graphics.device);
vk::PipelineShaderStageCreateInfo stage {};
stage.stage = vk::ShaderStageFlagBits::eCompute;
stage.module = module;
stage.pName = "main";
stage.pSpecializationInfo = &specialization;
vk::ComputePipelineCreateInfo create {};
create.stage = stage;
create.layout = m_pipeline_layout;
const auto result =
m_graphics.device.createComputePipelines(nullptr, 1, &create, nullptr, &pipeline);
m_graphics.device.destroyShaderModule(module, nullptr);
RequireVulkanSuccess(result, "create D16 conversion pipeline");
pipeline = CreatePipeline(code, &specialization);
}
const uint64_t source_element =
@@ -515,7 +484,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
vk::AccessFlagBits::eHostWrite |
vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderWrite;
command.pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 2, barriers, 0, nullptr);
@@ -599,18 +568,7 @@ void TileManager::TransformColor(Result input, Result output, ColorTransform tra
output.size < input.size);
const auto pixels = static_cast<uint32_t>(input.size / element_bytes);
if (m_color_transform == nullptr) {
const auto module = CompileSPV(GPU_TILER_COLOR_TRANSFORM_SPV, m_graphics.device);
vk::PipelineShaderStageCreateInfo stage {};
stage.stage = vk::ShaderStageFlagBits::eCompute;
stage.module = module;
stage.pName = "main";
vk::ComputePipelineCreateInfo create {};
create.stage = stage;
create.layout = m_pipeline_layout;
const auto result =
m_graphics.device.createComputePipelines(nullptr, 1, &create, nullptr, &m_color_transform);
m_graphics.device.destroyShaderModule(module, nullptr);
RequireVulkanSuccess(result, "create color transform pipeline");
m_color_transform = CreatePipeline(GPU_TILER_COLOR_TRANSFORM_SPV, nullptr);
}
const auto input_binding = BindStorage(input, input.size);
const auto output_binding = BindStorage(output, input.size);
@@ -670,4 +628,60 @@ TileManager::Result TileManager::TransformColor(Result input, ColorTransform tra
return result;
}
vk::Buffer TileManager::CheckColorClear(Result metadata, uint32_t slices, uint32_t keys,
bool expand) {
struct Check {
uint32_t metadata_base;
uint32_t words;
uint32_t keys;
uint32_t expand;
};
static_assert(sizeof(Check) <= sizeof(Conversion));
const uint32_t max_slices =
m_graphics.GetPhysicalDeviceProperties().limits.maxFramebufferLayers;
EXIT_IF(slices == 0 || slices > max_slices || metadata.size % (4ull * slices) != 0 ||
metadata.size / 4 > UINT32_MAX);
if (m_color_clear == nullptr) {
m_color_clear = CreatePipeline(GPU_TILER_COLOR_CLEAR_SPV, nullptr);
m_clear_predicates = std::make_unique<Buffer>(
m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eConditionalRenderingEXT,
uint64_t {max_slices} * ColorClearSlots * sizeof(uint32_t));
}
const auto source = BindStorage(metadata, metadata.size);
const vk::DescriptorBufferInfo infos[] {source.info,
{m_clear_predicates->Handle(), 0, VK_WHOLE_SIZE}};
std::array<vk::WriteDescriptorSet, 2> writes {};
for (uint32_t index = 0; index < writes.size(); index++) {
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType = vk::DescriptorType::eStorageBuffer;
writes[index].pBufferInfo = &infos[index];
}
// Orders the metadata writers and the previous check's predicate reads before this check.
vk::MemoryBarrier barrier {
.srcAccessMask = vk::AccessFlagBits::eMemoryWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite};
m_scheduler.EndRendering();
auto command = m_scheduler.Current().Handle();
command.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
vk::PipelineStageFlagBits::eComputeShader, {}, 1, &barrier, 0, nullptr,
0, nullptr);
command.bindPipeline(vk::PipelineBindPoint::eCompute, m_color_clear);
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
const Check push {source.base, static_cast<uint32_t>(metadata.size / 4 / slices), keys,
expand};
command.pushConstants(m_pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0, sizeof(push),
&push);
command.dispatch(slices, 1, 1);
barrier.srcAccessMask = vk::AccessFlagBits::eShaderWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite;
command.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
vk::PipelineStageFlagBits::eAllCommands, {}, 1, &barrier, 0, nullptr,
0, nullptr);
return m_clear_predicates->Handle();
}
} // namespace Libs::Graphics
@@ -54,6 +54,13 @@ public:
const D16Layout& layout);
[[nodiscard]] Result TransformColor(Result input, ColorTransform transform, bool to_host);
void TransformColor(Result input, Result output, ColorTransform transform, bool to_host);
// Color-clear keys are multiples of 0x20, so key >> 5 selects one of these slots.
static constexpr uint32_t ColorClearSlots = 8;
// Returns the buffer whose dword slice * ColorClearSlots + (key >> 5) is set when every byte
// of that metadata slice equals a key whose bit `keys` selects; a hit expands to 0xff when
// `expand` is set.
[[nodiscard]] vk::Buffer CheckColorClear(Result metadata, uint32_t slices, uint32_t keys,
bool expand);
private:
friend struct TileManagerTestAccess;
@@ -117,6 +124,8 @@ private:
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
const Tiling& tiling, bool clear_target);
[[nodiscard]] vk::Pipeline GetPipeline(uint32_t slot);
[[nodiscard]] vk::Pipeline CreatePipeline(std::span<const uint32_t> code,
const vk::SpecializationInfo* specialization) const;
GraphicContext& m_graphics;
CommandScheduler& m_scheduler;
@@ -130,6 +139,8 @@ private:
vk::Pipeline m_d24_to_d16 = nullptr;
vk::Pipeline m_d32_to_d16 = nullptr;
vk::Pipeline m_color_transform = nullptr;
vk::Pipeline m_color_clear = nullptr;
std::unique_ptr<Buffer> m_clear_predicates;
};
} // namespace Libs::Graphics
@@ -0,0 +1,82 @@
#include "graphics/host_gpu/renderer/occlusionQueries.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include <algorithm>
namespace Libs::Graphics {
// Queries no dump has read yet; past them a draw counts one sample.
constexpr uint32_t QUERY_COUNT = 16384;
OcclusionQueries::OcclusionQueries(GraphicContext& graphics, CommandScheduler& scheduler)
: m_scheduler(scheduler) {
if (!graphics.occlusion_queries_enabled) {
return;
}
m_device = graphics.device;
const vk::QueryPoolCreateInfo info {.queryType = vk::QueryType::eOcclusion,
.queryCount = QUERY_COUNT};
RequireVulkanSuccess(m_device.createQueryPool(&info, nullptr, &m_pool), "vkCreateQueryPool");
m_device.resetQueryPool(m_pool, 0, QUERY_COUNT);
m_results.resize(QUERY_COUNT);
if (graphics.occlusion_query_precise_enabled) {
m_control = vk::QueryControlFlagBits::ePrecise;
}
}
OcclusionQueries::~OcclusionQueries() {
if (m_pool) {
m_device.destroyQueryPool(m_pool, nullptr);
}
}
int32_t OcclusionQueries::BeginQuery(vk::CommandBuffer command) {
if (m_begun - m_read == QUERY_COUNT) {
m_uncounted++;
return -1;
}
const auto query = static_cast<uint32_t>(m_begun++ % QUERY_COUNT);
command.beginQuery(m_pool, query, m_control);
return static_cast<int32_t>(query);
}
void OcclusionQueries::Dump(uint64_t address) {
// Begins dump to the results, ends 8 bytes into them.
if ((address & 8u) == 0) {
m_open++;
} else if (m_open != 0) {
m_open--;
}
m_dumps.push_back({address, m_scheduler.CurrentTick(), m_begun, m_uncounted});
}
void OcclusionQueries::PublishDumps() {
if (const auto last = m_dumps.back().queries; last != m_read) {
m_scheduler.Wait(m_dumps.back().tick);
// The unread queries wrap around the pool at most once.
for (uint64_t read = m_read; read < last;) {
const auto first = static_cast<uint32_t>(read % QUERY_COUNT);
const auto count =
static_cast<uint32_t>(std::min<uint64_t>(last - read, QUERY_COUNT - first));
RequireVulkanSuccess(
m_device.getQueryPoolResults(m_pool, first, count, count * sizeof(uint64_t),
m_results.data() + first, sizeof(uint64_t),
vk::QueryResultFlagBits::e64),
"vkGetQueryPoolResults");
m_device.resetQueryPool(m_pool, first, count);
read += count;
}
}
for (const auto& dump: m_dumps) {
for (; m_read < dump.queries; m_read++) {
m_samples += m_results[m_read % QUERY_COUNT];
}
WriteZPassCounters(reinterpret_cast<volatile uint64_t*>(dump.address),
m_samples + dump.uncounted, (dump.address & 8u) == 0);
}
m_dumps.clear();
}
} // namespace Libs::Graphics
@@ -0,0 +1,85 @@
#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_OCCLUSIONQUERIES_H_
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_OCCLUSIONQUERIES_H_
#include "common/common.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <cstdint>
#include <vector>
namespace Libs::Graphics {
class CommandScheduler;
struct GraphicContext;
// The guest's query results (16-byte aligned): a begin/end pair of running z-pass counters per DB,
// 16 bytes apart; bit 63 marks a written counter. A query's samples are the sum of end - begin.
constexpr uint32_t OCCLUSION_QUERY_DBS = 16;
constexpr uint64_t OCCLUSION_QUERY_READY = 1ull << 63u;
// A dump of the running count, all of it in DB 0. A begin also clears its end counters.
inline void WriteZPassCounters(volatile uint64_t* results, uint64_t samples, bool begin) {
for (uint32_t db = 0; db < OCCLUSION_QUERY_DBS; db++) {
results[db * 2] = OCCLUSION_QUERY_READY | (db == 0 ? samples : 0);
if (begin) {
results[db * 2 + 1] = 0;
}
}
}
// The guest's z-pass counters from host occlusion queries: a draw counts its samples while a guest
// query is open, and the dumps' counters are written before the guest can see the GPU ran them.
class OcclusionQueries {
public:
OcclusionQueries(GraphicContext& graphics, CommandScheduler& scheduler);
~OcclusionQueries();
KYTY_CLASS_NO_COPY(OcclusionQueries);
[[nodiscard]] bool Enabled() const { return m_pool != nullptr; }
// In the draw's rendering: the query that counts its samples, or -1.
[[nodiscard]] int32_t Begin(vk::CommandBuffer command) {
return m_open == 0 ? -1 : BeginQuery(command);
}
void End(vk::CommandBuffer command, int32_t query) const {
if (query >= 0) {
command.endQuery(m_pool, static_cast<uint32_t>(query));
}
}
// A PIXEL_PIPE_STAT_DUMP to address, after the draws recorded so far.
void Dump(uint64_t address);
// Waits for the GPU to run the dumps and writes their counters.
void Publish() {
if (!m_dumps.empty()) {
PublishDumps();
}
}
private:
struct PendingDump {
uint64_t address;
uint64_t tick;
uint64_t queries;
uint64_t uncounted;
};
int32_t BeginQuery(vk::CommandBuffer command);
void PublishDumps();
CommandScheduler& m_scheduler;
vk::Device m_device;
vk::QueryPool m_pool;
vk::QueryControlFlags m_control;
std::vector<PendingDump> m_dumps;
std::vector<uint64_t> m_results;
uint32_t m_open = 0;
uint64_t m_begun = 0;
uint64_t m_read = 0;
uint64_t m_samples = 0;
uint64_t m_uncounted = 0;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_OCCLUSIONQUERIES_H_
@@ -126,13 +126,9 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
if (size > graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange) {
EXIT("storage buffer range is unsupported\n");
}
const bool metadata_read = resource.read && Config::SupportZmaskEnabled() &&
(address & 0x7fffu) == 0 &&
context.GetTextureCache().IsMeta(address);
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
address, size, resource.written || metadata_read,
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
address, size, resource.written,
resource.formatted || (resource.read && Config::SyncRawImageBuffersEnabled()), id);
if (metadata_read) buffer->Fill(offset, size, 0xf); // HTILE ZMask::kUncompressed.
const auto aligned_offset = Common::AlignDown(offset, alignment);
const auto adjustment = offset - aligned_offset;
const auto max_range = graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange;
@@ -147,7 +143,8 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
return result;
}
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
// Reserved and unsupported T# fields must be zero; perf_mod only scales sampler state.
static bool IsCanonicalTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
const uint32_t field3_expected = descriptor.DstSelXYZW() |
@@ -158,17 +155,25 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
const uint32_t field4_expected = descriptor.Depth() | (descriptor.BaseArray5() << 16u);
const uint32_t field5_expected = (static_cast<uint32_t>(descriptor.PerfMod5()) << 20u) |
(static_cast<uint32_t>(descriptor.MaxMip()) << 4u);
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
descriptor.fields[3] == field3_expected;
if (r128) {
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
if ((descriptor.fields[1] & field1_reserved_mask) != 0 ||
(descriptor.fields[2] & field2_reserved_mask) != 0 || descriptor.fields[3] != field3_expected) {
return false;
}
const bool full = common && descriptor.fields[4] == field4_expected &&
descriptor.fields[5] == field5_expected;
if (!full ||
(descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type()))) {
if (r128) {
return descriptor.fields[4] == 0 && descriptor.fields[5] == 0 && descriptor.fields[6] == 0 &&
descriptor.fields[7] == 0;
}
return descriptor.fields[4] == field4_expected && descriptor.fields[5] == field5_expected;
}
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
if (!IsCanonicalTextureEncoding(descriptor, r128)) {
return false;
}
if (r128) {
return true;
}
if (descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type())) {
return false;
}
const auto metadata_control = descriptor.fields[6] & 0x00ffffffu;
@@ -249,6 +254,10 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
const bool is_3d = resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim3D &&
descriptor.Type() == Prospero::ImageType::kColor3D &&
descriptor.BaseArray5() == 0;
const bool is_2d_msaa =
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa &&
descriptor.Type() == Prospero::ImageType::kColor2DMsaa && descriptor.Depth() == 0 &&
descriptor.BaseArray5() == 0;
TileTextureBlockLayout tile_layout {};
bool supported_tile = false;
switch (tile) {
@@ -279,42 +288,18 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
const bool supported_swizzle =
IsValidImageSwizzle(swizzle) &&
(swizzle == DstSel(4, 5, 6, 7) || !resource.read || resource.atomic);
return (is_1d || is_1d_array || is_2d || is_2d_array || is_3d) && supported_tile &&
descriptor.BaseLevel() <= descriptor.LastLevel() &&
return (is_1d || is_1d_array || is_2d || is_2d_array || is_3d || is_2d_msaa) &&
supported_tile && descriptor.BaseLevel() <= descriptor.LastLevel() &&
descriptor.MinLod() == 0 && supported_swizzle && descriptor.BCSwizzle() == 0 &&
!descriptor.MsaaDepth();
}
static bool IsSupportedStorageTextureEncoding(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor) {
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
constexpr uint32_t field5_expected = 0x00700000u;
constexpr uint32_t field5_max_mip_mask = 0x000000f0u;
const uint32_t expected_field3 = descriptor.DstSelXYZW() |
(static_cast<uint32_t>(descriptor.BaseLevel()) << 12u) |
(static_cast<uint32_t>(descriptor.LastLevel()) << 16u) |
(static_cast<uint32_t>(descriptor.TileMode()) << 20u) |
(static_cast<uint32_t>(descriptor.Type()) << 28u);
const uint32_t expected_field4 =
descriptor.Depth() | (static_cast<uint32_t>(descriptor.BaseArray5()) << 16u);
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
descriptor.fields[3] == expected_field3;
if (resource.r128) {
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
}
return common && descriptor.fields[4] == expected_field4 &&
(descriptor.fields[5] & ~field5_max_mip_mask) == field5_expected;
}
void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor, uint64_t size) {
const auto format = descriptor.Format();
const bool resource_ok = IsSupportedStorageImageResource(resource);
const bool descriptor_ok = IsSupportedStorageTextureDescriptor(resource, descriptor);
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
const bool encoding_ok = IsCanonicalTextureEncoding(descriptor, resource.r128);
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
const bool raw_sint_storage = uint_resource &&
ShaderRecompiler::IR::RawSignedStorageFormat(resource, format) != Prospero::BufferFormat::kInvalid;
@@ -375,7 +360,7 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
}
view.usage = storage ? vk::ImageUsageFlagBits::eStorage : vk::ImageUsageFlagBits::eSampled;
view.mapping =
storage || surface_format.conversion_format != Prospero::BufferFormat::kInvalid
storage || resource.conversion_format != Prospero::BufferFormat::kInvalid
? vk::ComponentMapping {}
: TextureGetComponentMapping(descriptor.DstSelXYZW(), surface_format.host_to_storage);
switch (resource.dimension) {
@@ -534,7 +519,9 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
}
const auto samples = multisampled ? 1u << last_level : 1u;
const auto depth = static_cast<uint32_t>(descriptor.Depth()) + 1u;
const auto format = descriptor.Format();
const auto format = ShaderRecompiler::IR::ReadsDepthBits(resource)
? Prospero::BufferFormat::k16UNorm
: descriptor.Format();
const auto surface_format = TextureGetSurfaceFormatInfo(format);
const bool shader_conversion =
surface_format.conversion_format != Prospero::BufferFormat::kInvalid;
@@ -598,6 +585,10 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
const auto alignment = desc.info.DataAlignment();
EXIT_NOT_IMPLEMENTED(desc.info.data.size == 0 || alignment == 0 ||
(address & (static_cast<uint64_t>(alignment) - 1u)) != 0);
// Image heaps keep the descriptors of unmapped textures, which PS5 never samples.
if (resource.heap && !m_context.IsMapped(address, desc.info.data.size)) {
return {.layout = vk::ImageLayout::eGeneral};
}
if (storage) {
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
}
@@ -613,8 +604,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
desc.heap = resource.heap;
auto id = texture_cache.FindImage(desc, shader_conversion);
auto id = texture_cache.FindImage(desc, shader_conversion);
if (!id) {
// Another extent at the address holds it; the heap retries once an image is created.
return {.desc = std::move(desc), .layout = vk::ImageLayout::eGeneral};
}
auto* image = &texture_cache.GetImage(id);
const bool stencil_association = static_cast<bool>(image->depth_id);
if (stencil_association) {
@@ -681,9 +677,7 @@ void RenderExecutor::BindRenderTarget(ImageId id) {
void RenderExecutor::ResetBindings() {
for (const auto id: m_bound_images) {
if (auto* image = m_context.GetTextureCache().m_slot_images.try_get(id); image != nullptr) {
image->binding = {};
}
m_context.GetTextureCache().m_slot_images[id].binding = {};
}
m_bound_images.clear();
}
@@ -692,7 +686,7 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
PreparedBindings& prepared) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(!runtime);
const auto& program = *runtime.program;
const auto& program = runtime.module->info;
const auto& snapshot = *runtime.resources;
prepared.runtime = &runtime;
prepared.gds = {nullptr, 0, VK_WHOLE_SIZE};
@@ -703,6 +697,10 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
prepared.samplers.clear();
prepared.shader_data.clear();
for (uint32_t i = 0; i < program.info.images.size(); i++) {
if (program.info.images[i].heap) {
prepared.images[i] = {};
continue;
}
auto binding = ResolveTexture(program.info.images[i], snapshot.images[i]);
BindImage(binding.image_id, binding.desc.type == TextureCache::BindingType::Storage);
binding.mip_views.swap(prepared.images[i].mip_views);
@@ -730,7 +728,7 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
for (auto* stage: stages) {
auto& prepared = *stage;
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& program = prepared.runtime->module->info;
const auto& snapshot = *prepared.runtime->resources;
const auto& layout = program.bindings;
prepared.buffer_sources.resize(layout.descriptor_counts[static_cast<size_t>(BindingKind::Buffers)]);
@@ -744,6 +742,12 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
auto size = descriptor.GetSize();
if (size != 0 && descriptor.Stride() == 0 && descriptor.OutOfBounds() == 2 &&
!metadata.scalar) {
// OOB_SELECT=2 only tests NumRecords != 0; a zero-stride fetch
// still needs its full payload when NumRecords is smaller than it.
size = std::max(size, uint64_t {metadata.max_byte_extent});
}
if (address == 0 || size == 0) {
source = {};
continue;
@@ -761,14 +765,18 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
}
}
}
} else {
const auto& graphics = m_context.GetGraphics();
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
(graphics.StorageMinAlignment() - 1);
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
// PPSA15246
const auto& graphics = m_context.GetGraphics();
const auto limit =
uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
(graphics.StorageMinAlignment() - 1);
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
// Finite GPU descriptors can span sparse PRT holes; clamp only oversized/sentinel ranges.
if (size >= (uint64_t {1} << 30u) || descriptor.NumRecords() == UINT32_MAX) {
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
}
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
source = {address, size, cache.FindBuffer(address, size)};
}
}
@@ -777,7 +785,7 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& program = prepared.runtime->module->info;
const auto& snapshot = *prepared.runtime->resources;
const auto& layout = program.bindings;
EXIT_IF(prepared.buffer_sources.size() != layout.descriptor_counts[static_cast<size_t>(BindingKind::Buffers)]);
@@ -798,6 +806,9 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
prepared.buffers[index] = NativeStorageBuffer(m_context, prepared.buffer_sources[index],
resource, buffer_offset);
pack_memory_offset(index, buffer_offset);
// Null bindings allow 0 bytes, not the shared null buffer's 16, so no store reaches it.
prepared.shader_data[layout.MemorySizeDword() + index] =
static_cast<uint32_t>(prepared.buffer_sources[index].size + buffer_offset);
}
if (layout.descriptor_counts[static_cast<size_t>(BindingKind::FlattenedSrt)] != 0) {
prepared.flattened_srt = NativeUpload(m_context, snapshot.flattened_srt);
@@ -810,7 +821,7 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
void RenderExecutor::RebindImages(PreparedBindings& prepared) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& program = prepared.runtime->module->info;
const auto& snapshot = *prepared.runtime->resources;
auto& images = prepared.images;
EXIT_IF(images.size() != program.info.images.size());
@@ -818,12 +829,9 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
// Acquire each view before a later overlapping descriptor can replace its image.
for (uint32_t i = 0; i < program.info.images.size(); i++) {
if (!images[i].image_id) continue;
const auto old_image = texture_cache.m_slot_images.try_get(images[i].image_id);
if (old_image == nullptr || !old_image->registered ||
old_image->binding.needs_rebind) {
if (old_image != nullptr) {
old_image->binding = {};
}
auto& old_image = texture_cache.m_slot_images[images[i].image_id];
if (!old_image.registered || old_image.binding.needs_rebind) {
old_image.binding = {};
images[i] = ResolveTexture(program.info.images[i], snapshot.images[i]);
BindImage(images[i].image_id,
images[i].desc.type == TextureCache::BindingType::Storage);
@@ -855,26 +863,28 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
std::span<RenderColorInfo> colors) {
bool uses_dma = false;
bool uses_dma = false;
bool uses_image_heap = false;
FindBuffers(stages);
for (auto* stage: stages) {
uses_dma |= stage->runtime->program->info.uses_dma;
uses_dma |= stage->runtime->module->info.info.uses_dma;
uses_image_heap |= stage->runtime->module->info.info.uses_image_heap;
}
if (uses_dma) {
m_context.PrepareBda();
}
if (uses_image_heap) {
m_context.GetImageHeap().Rebind(*this);
}
for (auto* stage: stages) {
RebindImages(*stage);
}
auto& cache = m_context.GetTextureCache();
for (auto& target: colors) {
EXIT_IF(!target.image_id);
const auto old_image = cache.m_slot_images.try_get(target.image_id);
if (old_image == nullptr || !old_image->registered ||
old_image->binding.needs_rebind) {
if (old_image != nullptr) {
old_image->binding = {};
}
auto& old_image = cache.m_slot_images[target.image_id];
if (!old_image.registered || old_image.binding.needs_rebind) {
old_image.binding = {};
target.desc.view_info.base_level = target.guest_mip_level;
target.desc.view_info.base_layer = target.guest_array_layer;
target.image_id = cache.FindImage(target.desc);
@@ -907,7 +917,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
: vk::ShaderStageFlags {};
for (const auto* prepared: prepared_bindings) {
EXIT_IF(prepared == nullptr || prepared->runtime == nullptr || !*prepared->runtime);
const auto& program = *prepared->runtime->program;
const auto& program = prepared->runtime->module->info;
write_count += std::popcount(program.bindings.descriptor_mask);
for (auto mask = program.bindings.descriptor_mask; mask != 0; mask &= mask - 1) {
const auto index = std::countr_zero(mask);
@@ -928,17 +938,16 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
const auto& reads = reader->runtime->resources->specialization_reads;
if (reads.empty()) continue;
for (const auto* writer: prepared_bindings) {
if (writer->runtime->program->has_address_writes) {
if (writer->runtime->module->info.has_address_writes) {
EXIT("scalar resource reads cannot be proven disjoint from shader address writes\n");
}
}
for (const auto [address, size]: reads) {
for (const auto id: m_bound_images) {
const auto* image = m_context.GetTextureCache().m_slot_images.try_get(id);
if (image == nullptr ||
(!image->binding.shader_write && !image->binding.is_target)) continue;
for (const auto written: {image->info.data, image->info.stencil,
image->info.metadata.range}) {
const auto& image = m_context.GetTextureCache().m_slot_images[id];
if (!image.binding.shader_write && !image.binding.is_target) continue;
for (const auto written: {image.info.data, image.info.stencil,
image.info.metadata.range}) {
if (written.size != 0 && ImageRangeOverlaps(address, size,
written.address, written.size)) {
EXIT("scalar resource reads overlap an image or attachment write\n");
@@ -946,18 +955,25 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
}
}
for (const auto* writer: prepared_bindings) {
const auto& program = *writer->runtime->program;
const auto& program = writer->runtime->module->info;
for (const auto& resource: program.info.buffers) {
if (!resource.written || resource.descriptor_index == UINT32_MAX) continue;
const auto& written = writer->buffer_sources[resource.descriptor_index];
if (written.size != 0 && ImageRangeOverlaps(address, size,
written.address, written.size)) {
// ADD_TID and swizzle move fixed-offset writes past write_extent.
const bool linear = (resource.packed_stride & ((1u << 14u) | (1u << 20u))) == 0u;
const auto written_size =
linear ? std::min<uint64_t>(written.size, resource.write_extent) : written.size;
if (written_size != 0 && ImageRangeOverlaps(address, size,
written.address, written_size)) {
EXIT("scalar resource reads overlap a shader buffer write\n");
}
}
}
}
}
if (pipeline.uses_image_heap) {
m_context.GetImageHeap().Commit(buffer, pipeline_bind_point, pipeline.pipeline_layout);
}
m_descriptor_images.resize(image_descriptor_count);
m_descriptor_writes.clear();
m_descriptor_writes.reserve(write_count);
@@ -965,7 +981,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
std::array<vk::DescriptorBufferInfo, 2> address_buffers;
for (auto* prepared: prepared_bindings) {
const auto& program = *prepared->runtime->program;
const auto& program = prepared->runtime->module->info;
auto& descriptors = *prepared;
const auto shader_stage = NativeShaderStage(program.stage);
const auto shader_stages = ShaderPipelineStages(shader_stage);
@@ -1015,9 +1031,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite,
range, vk_buffer);
} else {
image.Transit(image.info.IsDepth() ? vk::ImageLayout::eDepthStencilReadOnlyOptimal
: vk::ImageLayout::eShaderReadOnlyOptimal,
vk::AccessFlagBits2::eShaderRead, range, vk_buffer);
image.Transit(image.ReadLayout(), vk::AccessFlagBits2::eShaderRead, range, vk_buffer);
}
binding.layout = image.backing.state.layout;
}
@@ -1088,6 +1102,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
for (uint32_t i = 0; i < program.info.images.size(); ++i) {
const auto& resource = program.info.images[i];
const auto& texture = descriptors.images[i];
if (resource.heap) continue;
EXIT_IF(texture.image_id &&
resource.mip_count != (texture.mip_views.empty() ? 1u : texture.mip_views.size()));
const auto kind = *ShaderRecompiler::IR::DescriptorBindingForImage(resource);
@@ -0,0 +1,638 @@
#include "graphics/host_gpu/renderer/pipeline/imageHeap.h"
#include "common/alignment.h"
#include "common/assert.h"
#include "common/profiler.h"
#include "graphics/host_gpu/pageManager.h"
#include "graphics/host_gpu/regionDefinitions.h"
#include "graphics/host_gpu/renderer/cache/streamBuffer.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "kernel/memory.h"
#include <algorithm>
#include <bit>
#include <xxhash.h>
namespace Libs::Graphics {
namespace {
namespace IR = ShaderRecompiler::IR;
// Smaller tables are often written for one draw, so they enter on a second draw of a frame.
constexpr uint64_t MinTableBytes = 64ull * 1024ull;
constexpr uint32_t ElementMask = (1u << IR::ImageHeapKindShift) - 1u;
template <typename Pred, typename Fn>
void ForEachRun(size_t begin, size_t end, Pred&& pred, Fn&& fn) {
while (begin < end) {
if (!pred(begin)) {
++begin;
continue;
}
auto run = begin + 1u;
while (run < end && pred(run)) ++run;
fn(begin, run);
begin = run;
}
}
// Grows [begin, end) to cover [first, last).
void Widen(size_t& begin, size_t& end, size_t first, size_t last) {
const bool empty = begin == end;
begin = empty ? first : std::min(begin, first);
end = empty ? last : std::max(end, last);
}
// Probes at first + k * step whose first dword lies within a table of size bytes.
uint64_t Probes(uint64_t size, uint64_t first, uint64_t step) {
return size >= first + 4u ? (size - first - 4u) / step + 1u : 0u;
}
// Tables grow a little on most frames, so their slot words get room to double.
std::unique_ptr<Buffer> SlotBuffer(RenderContext& context, uint64_t size) {
return std::make_unique<Buffer>(
context.GetGraphics(), context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress,
std::bit_ceil(size + 4u));
}
// Guest bytes of a table that its tracker pages [begin, end) cover.
std::pair<uint64_t, uint64_t> PageRange(uint64_t base, uint64_t size, size_t begin, size_t end) {
const auto origin = Common::AlignDown(base, TRACKER_PAGE_SIZE);
return {std::max(base, origin + begin * TRACKER_PAGE_SIZE),
std::min(base + size, origin + end * TRACKER_PAGE_SIZE)};
}
} // namespace
size_t ImageHeap::DescriptorHash::operator()(const DescriptorValue& value) const noexcept {
return XXH3_64bits(value.dwords.data(), sizeof(value.dwords));
}
ImageHeap::ImageHeap(RenderContext& context, PageManager& page_manager)
: m_context(context), m_page_manager(page_manager) {
auto& graphics = context.GetGraphics();
vk::PhysicalDeviceVulkan12Properties limits {};
vk::PhysicalDeviceProperties2 properties {};
properties.pNext = &limits;
graphics.physical_device.getProperties2(&properties);
EXIT_NOT_IMPLEMENTED(std::min({limits.maxDescriptorSetUpdateAfterBindSampledImages,
limits.maxPerStageDescriptorUpdateAfterBindSampledImages,
limits.maxPerStageUpdateAfterBindResources}) <
IR::ImageHeapCapacity);
const vk::DescriptorBindingFlags binding_flags =
vk::DescriptorBindingFlagBits::ePartiallyBound |
vk::DescriptorBindingFlagBits::eUpdateAfterBind |
vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending;
vk::DescriptorSetLayoutBindingFlagsCreateInfo flags_info {};
flags_info.bindingCount = 1;
flags_info.pBindingFlags = &binding_flags;
const vk::DescriptorSetLayoutBinding binding {0, vk::DescriptorType::eSampledImage,
IR::ImageHeapCapacity,
vk::ShaderStageFlagBits::eAll, nullptr};
vk::DescriptorSetLayoutCreateInfo layout_info {};
layout_info.pNext = &flags_info;
layout_info.flags = vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool;
layout_info.bindingCount = 1;
layout_info.pBindings = &binding;
EXIT_IF(graphics.device.createDescriptorSetLayout(&layout_info, nullptr, &m_layout) !=
vk::Result::eSuccess);
const vk::DescriptorPoolSize pool_size {vk::DescriptorType::eSampledImage,
IR::ImageHeapCapacity};
vk::DescriptorPoolCreateInfo pool_info {};
pool_info.flags = vk::DescriptorPoolCreateFlagBits::eUpdateAfterBind;
pool_info.maxSets = 1;
pool_info.poolSizeCount = 1;
pool_info.pPoolSizes = &pool_size;
EXIT_IF(graphics.device.createDescriptorPool(&pool_info, nullptr, &m_pool) !=
vk::Result::eSuccess);
vk::DescriptorSetAllocateInfo allocate {};
allocate.descriptorPool = m_pool;
allocate.descriptorSetCount = 1;
allocate.pSetLayouts = &m_layout;
EXIT_IF(graphics.device.allocateDescriptorSets(&allocate, &m_set) != vk::Result::eSuccess);
graphics.image_heap_layout = m_layout;
// Slot word 0 selects element 0, which stays a null image.
m_elements.push_back({.binding = {.layout = vk::ImageLayout::eShaderReadOnlyOptimal}});
m_writes.push_back(0u);
WriteDescriptors();
}
ImageHeap::~ImageHeap() {
for (const auto& memory: m_memories) Unwatch(*memory);
auto& graphics = m_context.GetGraphics();
graphics.image_heap_layout = nullptr;
graphics.device.destroyDescriptorPool(m_pool, nullptr);
graphics.device.destroyDescriptorSetLayout(m_layout, nullptr);
}
bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& result) {
KYTY_PROFILER_FUNCTION();
const auto size = request.size & ~uint64_t {3};
if (request.count == 0u || request.step == 0u ||
((request.base | request.first | request.step) & 3u) != 0u) {
return false;
}
auto memory = std::ranges::find_if(
m_memories, [&](const auto& candidate) { return candidate->base == request.base; });
if (memory == m_memories.end() && size < MinTableBytes &&
m_sightings.try_emplace(request.base, m_draw).first->second == m_draw) {
return false;
}
// A table follows its descriptor's size, as the first program of a draw acquires it.
if (memory == m_memories.end() || ((*memory)->size != size && (*memory)->draw != m_draw)) {
if (!m_context.IsMapped(request.base, size)) return false;
if (memory == m_memories.end()) {
std::scoped_lock lock {m_mutex};
memory = m_memories.insert(m_memories.end(),
std::make_unique<Memory>(Memory {.base = request.base}));
}
Resize(**memory, size);
}
// A draw refreshes a table and a view takes its changes once, so all programs of the draw see
// one snapshot and one set of kinds.
if ((*memory)->draw != m_draw) {
if (!Refresh(**memory)) return false;
(*memory)->draw = m_draw;
}
auto& views = (*memory)->views;
const auto bytes = (*memory)->size;
const auto profile = IR::ImageHeapResource(*request.root, 0u);
const auto probes = Probes(size, request.first, request.step);
const auto limit = request.count < probes ? request.count : UINT64_MAX;
auto view = std::ranges::find_if(views, [&](const auto& candidate) {
return candidate->step == request.step && candidate->first == request.first &&
candidate->limit == limit && candidate->profile == profile;
});
if (view == views.end()) {
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto count = std::min(limit, Probes(bytes, request.first, request.step));
auto created = std::make_unique<View>(View {.memory = **memory,
.step = request.step,
.first = request.first,
.count = count,
.limit = limit,
.profile = profile,
.resolve_epoch = epoch,
.layout_epoch = epoch});
created->slots.resize(bytes / 4u + 1u);
created->buffer = SlotBuffer(m_context, bytes);
UpdateProbes(*created, 0u, bytes);
// The first upload also clears the slots no probe selects.
created->dirty_begin = 0;
created->dirty_end = created->slots.size();
view = views.insert(views.end(), std::move(created));
} else {
// Empty after the first acquire in a draw, since the table refreshes once per draw.
UpdateProbes(**view, (*view)->changed_begin, (*view)->changed_end);
(*view)->changed_begin = (*view)->changed_end = 0;
}
if (std::ranges::find(m_active, view->get()) == m_active.end()) m_active.push_back(view->get());
const auto& selected = **view;
result = {.slots = selected.buffer->BufferDeviceAddress(),
.slot_count = static_cast<uint32_t>(selected.slots.size() - 1u),
.kinds = selected.kinds};
return true;
}
bool ImageHeap::Refresh(Memory& memory) {
// GPU writes leave no fault; reading the table downloads them.
const bool gpu_dirty = m_context.GetBufferCache().HasGpuDirtyBytes(memory.base, memory.size);
const auto pages = memory.dirty_pages.size();
m_runs.clear();
{
std::scoped_lock lock {m_mutex};
// Watch before reading, so a write during the read faults the page again.
ForEachRun(
0, pages, [&](size_t page) { return memory.dirty_pages[page] != 0u; },
[&](size_t begin, size_t end) {
std::fill(memory.dirty_pages.begin() + begin, memory.dirty_pages.begin() + end,
uint8_t {0});
Watch<true>(memory, begin, end);
m_runs.emplace_back(begin, end);
});
}
if (gpu_dirty) m_runs.assign(1u, {0u, pages});
for (size_t run = 0; run < m_runs.size(); ++run) {
const auto [address, end] = PageRange(memory.base, memory.size, m_runs[run].first,
m_runs[run].second);
m_scratch.resize((end - address) / 4u);
if (!LibKernel::Memory::TryReadBufferBacking(address, m_scratch.data(), end - address)) {
std::scoped_lock lock {m_mutex};
for (; run < m_runs.size(); ++run) {
MarkDirty(memory, m_runs[run].first, m_runs[run].second);
}
return false;
}
const auto words = std::span(memory.words).subspan((address - memory.base) / 4u,
m_scratch.size());
size_t first = 0;
size_t last = words.size();
while (first < last && words[first] == m_scratch[first]) ++first;
while (first < last && words[last - 1u] == m_scratch[last - 1u]) --last;
if (first == last) continue;
std::copy(m_scratch.begin() + first, m_scratch.begin() + last, words.begin() + first);
const auto offset = address - memory.base;
for (auto& view: memory.views) {
Widen(view->changed_begin, view->changed_end, offset + first * 4u, offset + last * 4u);
}
}
return true;
}
void ImageHeap::MarkDirty(Memory& memory, size_t begin, size_t end) {
ForEachRun(
begin, end, [&](size_t page) { return memory.dirty_pages[page] == 0u; },
[&](size_t first, size_t last) {
std::fill(memory.dirty_pages.begin() + first, memory.dirty_pages.begin() + last,
uint8_t {1});
Watch<false>(memory, first, last);
});
}
template <bool track>
void ImageHeap::Watch(const Memory& memory, size_t begin, size_t end) {
const auto [address, limit] = PageRange(memory.base, memory.size, begin, end);
m_page_manager.UpdatePageWatchers<track>(address, limit - address);
}
void ImageHeap::Unwatch(const Memory& memory) {
ForEachRun(
0, memory.dirty_pages.size(), [&](size_t page) { return memory.dirty_pages[page] == 0u; },
[&](size_t begin, size_t end) { Watch<false>(memory, begin, end); });
}
void ImageHeap::InvalidateMemory(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_mutex};
for (auto& memory: m_memories) {
// Watches cover whole pages, so a write next to the table must unwatch its page too.
const auto origin = Common::AlignDown(memory->base, TRACKER_PAGE_SIZE);
const auto begin = std::max(address, origin);
const auto end = std::min(address + size,
origin + memory->dirty_pages.size() * TRACKER_PAGE_SIZE);
if (begin >= end) continue;
MarkDirty(*memory, (begin - origin) / TRACKER_PAGE_SIZE,
(end - 1u - origin) / TRACKER_PAGE_SIZE + 1u);
}
}
void ImageHeap::UpdateProbes(View& view, uint64_t begin, uint64_t end) {
// A probe reads its descriptor from first + probe * step.
const uint64_t bytes = view.profile.r128 ? 16u : 32u;
auto probe = begin < view.first + bytes ? 0u : (begin - view.first - bytes) / view.step + 1u;
for (; probe < view.count && view.first + probe * view.step < end; ++probe) {
UpdateProbe(view, probe);
}
}
void ImageHeap::UpdateProbe(View& view, uint64_t probe) {
const auto& words = view.memory.words;
const auto offset = (view.first + probe * view.step) / 4u;
DescriptorValue value {.dword_count = 8u};
std::copy_n(words.begin() + offset,
std::min<size_t>(view.profile.r128 ? 4u : 8u, words.size() - offset),
value.dwords.begin());
// A buried word stays null in every view until its descriptor changes.
auto& dead = view.memory.dead[offset];
if (dead != 0u && dead != DescriptorHash {}(value)) dead = 0u;
const auto kind = dead != 0u ? 0u : IR::ImageHeapKind(view.profile, value);
Assign(view, offset, kind == 0u ? 0u : Intern(view, kind, value));
}
void ImageHeap::Assign(View& view, size_t slot, uint32_t entry) {
const auto previous = std::exchange(view.slots[slot], entry);
Release(view, previous);
if (previous != entry) Widen(view.dirty_begin, view.dirty_end, slot, slot + 1u);
}
void ImageHeap::Bury() {
if (m_unmapped.empty()) return;
for (const auto& memory: m_memories) {
for (const auto& view: memory->views) {
bool buried = false;
for (const auto& [value, index]: view->elements) {
const auto& binding = m_elements[index].binding;
if (binding.image_id && std::ranges::any_of(m_unmapped, [&](GuestRange range) {
return ImageRangeOverlaps(binding.desc.info.data, range);
})) {
m_elements[index].stale = buried = true;
}
}
if (!buried) continue;
for (uint64_t probe = 0; probe < view->count; ++probe) {
const auto slot = (view->first + probe * view->step) / 4u;
const auto& element = m_elements[view->slots[slot] & ElementMask];
if (!element.stale) continue;
memory->dead[slot] = DescriptorHash {}(element.value);
Assign(*view, slot, 0u);
}
}
}
m_unmapped.clear();
}
uint32_t ImageHeap::Intern(View& view, uint32_t kind, const DescriptorValue& value) {
const auto arm = static_cast<uint32_t>(std::ranges::find(view.kind_keys, kind) -
view.kind_keys.begin());
if (arm == view.kind_keys.size()) {
view.kind_keys.push_back(kind);
view.kinds.push_back(value);
}
auto [found, inserted] = view.elements.try_emplace(value, 0u);
if (inserted) {
found->second = Allocate();
auto& element = m_elements[found->second];
element.value = value;
element.kind = kind;
view.pending.push_back(found->second);
}
++m_elements[found->second].refs;
return arm << IR::ImageHeapKindShift | found->second;
}
void ImageHeap::Release(View& view, uint32_t entry) {
const auto index = entry & ElementMask;
if (index == 0u || --m_elements[index].refs != 0u) return;
auto& element = m_elements[index];
if (const auto found = view.elements.find(element.value);
found != view.elements.end() && found->second == index) {
view.elements.erase(found);
}
Unreference(element);
view.released.push_back(index);
}
void ImageHeap::Unreference(Element& element) {
if (auto* image = ImageOf(element)) {
image->heap_refs.fetch_sub(1u, std::memory_order_relaxed);
image->tick_accessed_last = m_context.GetCommandScheduler().CurrentTick();
}
}
Image* ImageHeap::ImageOf(const Element& element) const {
auto* image = m_context.GetTextureCache().m_slot_images.try_get(element.binding.image_id);
return image != nullptr && image->serial == element.serial ? image : nullptr;
}
uint32_t ImageHeap::Allocate() {
auto& scheduler = m_context.GetCommandScheduler();
while (!m_retired.empty() && scheduler.IsFree(m_retired.front().first)) {
m_free.push_back(m_retired.front().second);
m_retired.pop_front();
}
if (m_free.empty()) {
if (m_elements.size() == IR::ImageHeapCapacity) {
EXIT("image heaps exceed %u resident images\n", IR::ImageHeapCapacity);
}
m_elements.emplace_back();
return static_cast<uint32_t>(m_elements.size() - 1u);
}
const auto index = m_free.back();
m_free.pop_back();
m_elements[index] = {};
return index;
}
void ImageHeap::Retire(View& view) {
if (view.released.empty()) return;
// A retired index is reused once its tick ends, so no held entry may keep it.
std::erase_if(view.held, [&](uint32_t index) { return m_elements[index].refs == 0u; });
const auto tick = m_context.GetCommandScheduler().CurrentTick();
for (const auto index: view.released) m_retired.emplace_back(tick, index);
view.released.clear();
}
void ImageHeap::Resize(Memory& memory, uint64_t size) {
{
std::scoped_lock lock {m_mutex};
Unwatch(memory);
memory.size = size;
// The next refresh rereads the whole table.
memory.dirty_pages.assign((Common::AlignUp(memory.base + size, TRACKER_PAGE_SIZE) -
Common::AlignDown(memory.base, TRACKER_PAGE_SIZE)) /
TRACKER_PAGE_SIZE,
uint8_t {1});
}
memory.words.resize(size / 4u);
memory.dead.resize(size / 4u);
auto& scheduler = m_context.GetCommandScheduler();
for (auto& view: memory.views) {
const auto count = std::min(view->limit, Probes(size, view->first, view->step));
for (auto probe = count; probe < view->count; ++probe) {
Assign(*view, (view->first + probe * view->step) / 4u, 0u);
}
view->count = count;
const auto slots = view->slots.size();
view->slots.resize(size / 4u + 1u);
view->dirty_end = std::min(view->dirty_end, view->slots.size());
view->dirty_begin = std::min(view->dirty_begin, view->dirty_end);
if (view->slots.size() * sizeof(uint32_t) > view->buffer->Size()) {
if (scheduler.Active()) {
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
}
view->buffer = SlotBuffer(m_context, size);
Widen(view->dirty_begin, view->dirty_end, 0u, view->slots.size());
} else if (slots < view->slots.size()) {
// Device words past the old end may hold stale slots.
Widen(view->dirty_begin, view->dirty_end, slots, view->slots.size());
}
}
}
void ImageHeap::Rebind(RenderExecutor& executor) {
KYTY_PROFILER_FUNCTION();
auto& textures = m_context.GetTextureCache();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto serial = Image::serials.load(std::memory_order_relaxed);
for (auto* view: m_active) {
bool stale = false;
const auto mark = [&](uint32_t index) { m_elements[index].stale = stale = true; };
if (view->resolve_epoch != epoch) {
view->resolve_epoch = epoch;
for (const auto& [value, index]: view->elements) {
const auto& binding = m_elements[index].binding;
if (!binding.image_id) continue;
const auto* image = ImageOf(m_elements[index]);
if (image == nullptr || !image->registered || image->depth_id ||
image->binding.needs_rebind) {
mark(index);
} else {
(void)textures.FindTexture(binding.image_id, binding.desc);
}
}
}
// Discovery applies the color metadata clears written since the last draw.
std::erase_if(view->dcc, [&](uint32_t index) {
const auto& element = m_elements[index];
if (element.refs == 0u || element.stale) return true;
if (executor.ResolveTexture(IR::ImageHeapResource(view->profile, element.kind),
element.value).image_id == element.binding.image_id) {
return false;
}
mark(index);
return true;
});
// Held elements retry once an image is created, such as a render target of their extent.
if (view->held_serial != serial) {
view->held_serial = serial;
std::erase_if(view->held, [&](uint32_t index) {
const auto& element = m_elements[index];
if (element.refs == 0u || element.stale) return true;
const auto binding = executor.ResolveTexture(
IR::ImageHeapResource(view->profile, element.kind), element.value);
if (binding.image_id) mark(index);
// An unmapped texture stays null.
return binding.image_id || binding.desc.info.data.Empty();
});
}
if (stale) {
// A changed image needs a new element; pending draws still read the old one. The new
// element keeps the old value, since a view takes table changes only when acquired.
std::erase_if(view->elements,
[&](const auto& item) { return m_elements[item.second].stale; });
for (uint64_t probe = 0; probe < view->count; ++probe) {
const auto slot = (view->first + probe * view->step) / 4u;
const auto& element = m_elements[view->slots[slot] & ElementMask];
if (!element.stale) continue;
const auto value = element.value; // Intern may grow m_elements.
Assign(*view, slot, Intern(*view, element.kind, value));
}
}
for (const auto index: view->pending) {
if (m_elements[index].refs == 0u) continue;
auto binding = executor.ResolveTexture(
IR::ImageHeapResource(view->profile, m_elements[index].kind),
m_elements[index].value);
if (binding.image_id) {
auto& image = textures.GetImage(binding.image_id);
image.heap_refs.fetch_add(1u, std::memory_order_relaxed);
m_elements[index].serial = image.serial;
binding.layout = image.ReadLayout();
binding.image_view = textures.FindTexture(binding.image_id, binding.desc);
if (binding.desc.info.metadata.kind == ImageMetadataKind::Dcc) {
view->dcc.push_back(index);
}
view->away.push_back(index);
} else if (!binding.desc.info.data.Empty()) {
view->held.push_back(index);
}
m_elements[index].binding = std::move(binding);
m_writes.push_back(index);
}
view->pending.clear();
}
WriteDescriptors();
}
void ImageHeap::WriteDescriptors() {
if (m_writes.empty()) return;
std::vector<vk::DescriptorImageInfo> infos;
std::vector<vk::WriteDescriptorSet> writes(m_writes.size());
infos.reserve(m_writes.size());
for (size_t write = 0; write < m_writes.size(); ++write) {
const auto& binding = m_elements[m_writes[write]].binding;
infos.push_back({nullptr, binding.image_view, binding.layout});
writes[write].dstSet = m_set;
writes[write].dstArrayElement = m_writes[write];
writes[write].descriptorCount = 1;
writes[write].descriptorType = vk::DescriptorType::eSampledImage;
writes[write].pImageInfo = &infos.back();
}
m_context.GetGraphics().device.updateDescriptorSets(static_cast<uint32_t>(writes.size()),
writes.data(), 0, nullptr);
m_writes.clear();
}
void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
vk::PipelineLayout layout) {
KYTY_PROFILER_FUNCTION();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
for (auto* view: m_active) {
if (view->dirty_begin != view->dirty_end) {
const auto slots = std::span(view->slots).subspan(view->dirty_begin,
view->dirty_end - view->dirty_begin);
auto& stream = m_context.GetBufferCache().GetUtilityBuffer(MemoryUsage::Stream);
const auto offset = stream.Copy(slots.data(), slots.size_bytes(), sizeof(uint32_t));
view->buffer->CopyFrom(buffer, stream, offset, view->dirty_begin * sizeof(uint32_t),
slots.size_bytes());
view->dirty_begin = view->dirty_end = 0;
}
Retire(*view);
if (view->layout_epoch != epoch) {
view->layout_epoch = epoch;
view->away.clear();
for (const auto& [value, index]: view->elements) view->away.push_back(index);
}
// This draw's own bindings own the layouts of its attachments and storage images. An image
// this draw's discovery replaced lives until the tick ends, and its slots still select it.
std::erase_if(view->away, [&](uint32_t index) {
const auto& element = m_elements[index];
auto* image = ImageOf(element);
if (element.refs == 0u || image == nullptr) return true;
if (image->binding.is_target || image->binding.shader_write ||
image->binding.force_general) {
return false;
}
const auto& view_info = element.binding.desc.view_info;
image->Transit(image->ReadLayout(), vk::AccessFlagBits2::eShaderRead,
ImageSubresourceRange {view_info.base_level, view_info.level_count,
view_info.base_layer, view_info.layer_count},
buffer.Handle());
return true;
});
}
m_active.clear();
buffer.Handle().bindDescriptorSets(bind_point, layout, IR::ImageHeapSet, 1, &m_set, 0,
nullptr);
}
void ImageHeap::UnmapMemory(uint64_t address, uint64_t size) {
// Descriptors of textures in the range stay null even once it is mapped again.
if (!m_unmapped.empty() && m_unmapped.back().End() == address) {
m_unmapped.back().size += size;
} else {
m_unmapped.push_back({address, size});
}
std::scoped_lock lock {m_mutex};
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
if (address >= memory->base + memory->size || memory->base >= address + size) {
return false;
}
Drop(*memory);
return true;
});
}
void ImageHeap::RunGarbageCollector() {
// A smaller table rebuilt each frame moves to a new base. Larger ones keep their dead words.
std::scoped_lock lock {m_mutex};
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
if (memory->size >= MinTableBytes || memory->draw > m_suspend_draw) return false;
Drop(*memory);
return true;
});
m_suspend_draw = m_draw;
m_sightings.clear();
}
void ImageHeap::Drop(Memory& memory) {
auto& scheduler = m_context.GetCommandScheduler();
Unwatch(memory);
for (auto& view: memory.views) {
std::erase(m_active, view.get());
for (const auto& [value, index]: view->elements) {
Unreference(m_elements[index]);
view->released.push_back(index);
}
Retire(*view);
if (scheduler.Active()) {
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
}
}
}
} // namespace Libs::Graphics
@@ -0,0 +1,150 @@
#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_IMAGEHEAP_H_
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_IMAGEHEAP_H_
#include "common/common.h"
#include "graphics/host_gpu/renderer/pipeline/descriptors.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include <deque>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Libs::Graphics {
class Buffer;
class CommandBuffer;
class PageManager;
class RenderContext;
class RenderExecutor;
struct ImageHeapTestAccess;
// Guest image tables that shaders index by GPU-computed offsets, kept resident in one bindless
// sampled-image array. A table is keyed by its base and follows its descriptor's size. Each view
// of a table owns device slot words, one per table dword, that select an array element. CPU
// writes to a table fault in its snapshot, which each draw refreshes once; a view takes the
// changes when a draw first acquires it. Changes to the images behind elements bump
// Image::heap_epoch. A smaller table leaves at a suspend point if the frame did not acquire it.
class ImageHeap final: public ShaderRecompiler::IR::ImageHeapSource {
public:
ImageHeap(RenderContext& context, PageManager& page_manager);
~ImageHeap();
KYTY_CLASS_NO_COPY(ImageHeap);
// Starts a draw or dispatch, before its programs acquire views.
void BeginDraw() {
++m_draw;
Bury();
}
bool Acquire(const ShaderRecompiler::IR::ImageHeapRequest& request,
ShaderRecompiler::IR::ImageHeapView& result) override;
// Image discovery: resolves new elements and rediscovers changed images.
void Rebind(RenderExecutor& executor);
// Recording: uploads slots, moves resident images to their read layout and binds the array.
void Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point, vk::PipelineLayout layout);
// CPU write fault, on any thread.
void InvalidateMemory(uint64_t address, uint64_t size);
void UnmapMemory(uint64_t address, uint64_t size);
// At a suspend point.
void RunGarbageCollector();
private:
using DescriptorValue = ShaderRecompiler::IR::DescriptorValue;
struct DescriptorHash {
size_t operator()(const DescriptorValue& value) const noexcept;
};
struct Element {
DescriptorValue value;
TextureBinding binding;
uint32_t kind = 0;
uint32_t refs = 0; // Slots of the owning view that select it.
bool stale = false;
uint64_t serial = 0; // Image::serial of binding.image_id.
};
struct Memory;
struct View {
Memory& memory;
uint64_t step = 0;
uint64_t first = 0;
uint64_t count = 0;
uint64_t limit = 0; // Probes a key domain allows, if fewer.
ShaderRecompiler::IR::ImageResource profile;
std::vector<uint32_t> slots; // Plus a trailing null slot.
std::vector<uint32_t> kind_keys;
std::vector<DescriptorValue> kinds; // One representative per kind_keys entry.
std::unordered_map<DescriptorValue, uint32_t, DescriptorHash> elements;
std::vector<uint32_t> pending; // Not yet resolved.
std::vector<uint32_t> released; // No slot selects them after the next upload.
std::vector<uint32_t> away; // Not yet moved to the read layout.
std::vector<uint32_t> dcc; // Color metadata clears apply on each discovery.
std::vector<uint32_t> held; // Another extent holds their address.
std::unique_ptr<Buffer> buffer;
size_t dirty_begin = 0; // Slots to upload.
size_t dirty_end = 0;
size_t changed_begin = 0; // Table bytes changed since it was probed.
size_t changed_end = 0;
uint64_t resolve_epoch = 0;
uint64_t layout_epoch = 0;
uint64_t held_serial = 0; // Image::serials when held was last retried.
};
struct Memory {
uint64_t base = 0;
uint64_t size = 0;
std::vector<uint32_t> words;
std::vector<uint8_t> dirty_pages; // Unwatched, so unread since a write.
std::vector<uint64_t> dead; // Per word, a buried descriptor's hash.
uint64_t draw = 0; // Last draw that refreshed it.
std::vector<std::unique_ptr<View>> views;
};
[[nodiscard]] bool Refresh(Memory& memory);
void MarkDirty(Memory& memory, size_t begin, size_t end);
template <bool track>
void Watch(const Memory& memory, size_t begin, size_t end);
void Unwatch(const Memory& memory);
void Drop(Memory& memory);
void UpdateProbes(View& view, uint64_t begin, uint64_t end);
void UpdateProbe(View& view, uint64_t probe);
void Assign(View& view, size_t slot, uint32_t entry);
void Bury();
uint32_t Intern(View& view, uint32_t kind, const DescriptorValue& value);
void Release(View& view, uint32_t entry);
void Unreference(Element& element);
Image* ImageOf(const Element& element) const;
uint32_t Allocate();
void Retire(View& view);
void Resize(Memory& memory, uint64_t size);
void WriteDescriptors();
RenderContext& m_context;
PageManager& m_page_manager;
std::mutex m_mutex; // Fault-visible Memory state and m_memories.
std::vector<std::unique_ptr<Memory>> m_memories;
std::vector<View*> m_active; // Acquired since the last commit.
uint64_t m_draw = 1;
uint64_t m_suspend_draw = 0; // m_draw at the last suspend point.
std::vector<Element> m_elements;
std::vector<uint32_t> m_free;
std::unordered_map<uint64_t, uint64_t> m_sightings; // Smaller tables: base, first draw.
std::deque<std::pair<uint64_t, uint32_t>> m_retired; // Tick, element.
std::vector<std::pair<size_t, size_t>> m_runs;
std::vector<uint32_t> m_scratch;
std::vector<uint32_t> m_writes; // Elements whose descriptors changed.
std::vector<GuestRange> m_unmapped; // Since the last Bury.
vk::DescriptorSetLayout m_layout = nullptr;
vk::DescriptorPool m_pool = nullptr;
vk::DescriptorSet m_set = nullptr;
friend struct ImageHeapTestAccess;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_IMAGEHEAP_H_
File diff suppressed because it is too large. Load diff
@@ -4,31 +4,48 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/file.h"
#include "common/uniqueFunction.h"
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
#include "graphics/host_gpu/renderer/depthRenderTarget.h"
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <filesystem>
#include <memory>
#include <mutex>
#include <queue>
#include <span>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace Libs::Graphics {
struct GraphicContext;
struct RenderColorInfo;
struct RenderDepthInfo;
struct ShaderParams;
class CommandBuffer;
namespace HW {
class Context;
class Shader;
class UserConfig;
struct ComputeShaderInfo;
} // namespace HW
namespace ShaderRecompiler {
struct CompileOptions;
struct ShaderSource;
struct TranslateResult;
} // namespace ShaderRecompiler
namespace ShaderRecompiler::IR {
class ImageHeapSource;
} // namespace ShaderRecompiler::IR
namespace ShaderRecompiler::Spirv {
struct Profile;
} // namespace ShaderRecompiler::Spirv
#pragma pack(push, 1)
@@ -36,13 +53,13 @@ struct PipelineStaticParameters {
bool negative_one_to_one = false;
bool depth_clip_enable = true;
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
// 0 without tessellation, 3 for a rectangle list, else the input control points.
uint32_t patch_control_points = 0;
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool alpha_to_coverage_enable = false;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
bool face = false;
bool provoking_vtx_last = false;
vk::PolygonMode polygon_mode = vk::PolygonMode::eFill;
uint8_t color_srcblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
@@ -53,57 +70,110 @@ struct PipelineStaticParameters {
uint8_t alpha_destblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool separate_alpha_blend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool blend_enable[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool operator==(const PipelineStaticParameters& other) const noexcept;
};
#pragma pack(pop)
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
static_assert(alignof(PipelineStaticParameters) == 1);
static_assert(sizeof(PipelineStaticParameters) == 116);
struct PipelineRenderingState {
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
vk::Format depth_format = vk::Format::eUndefined;
vk::Format stencil_format = vk::Format::eUndefined;
uint32_t color_count = 0;
bool operator==(const PipelineRenderingState&) const = default;
};
struct PipelineVertexInputState {
struct Binding {
uint32_t stride = 0;
bool instance = false;
bool operator==(const Binding&) const = default;
uint32_t stride = 0;
uint8_t instance = 0;
};
struct Attribute {
uint32_t offset = 0;
uint8_t binding = 0;
bool operator==(const Attribute&) const = default;
uint32_t offset = 0;
uint8_t binding = 0;
};
std::array<Binding, ShaderVertexInputInfo::RES_MAX> bindings {};
std::array<Attribute, ShaderVertexInputInfo::RES_MAX> attributes {};
uint8_t binding_count = 0;
uint8_t attribute_count = 0;
bool operator==(const PipelineVertexInputState&) const = default;
};
struct ShaderProgram {
uint64_t id = 0;
vk::ShaderModule module = nullptr;
// Everything a graphics pipeline is made of besides the stage modules its ids name. Aligned so
// that rendering.color_formats can be passed to Vulkan.
struct alignas(4) GraphicsPipelineKey {
// VS, LS or mesh; HS; TES; PS. 0 marks an absent stage.
std::array<uint64_t, 4> stage_ids {};
PipelineRenderingState rendering;
PipelineVertexInputState vertex_input;
PipelineStaticParameters static_params;
explicit operator bool() const { return id != 0 && module != nullptr; }
bool operator==(const GraphicsPipelineKey& other) const;
};
#pragma pack(pop)
// Keys are compared and hashed as bytes.
static_assert(std::has_unique_object_representations_v<GraphicsPipelineKey>);
// A compiled stage with what pipeline creation needs from it besides the key.
struct ShaderModule {
uint64_t id = 0;
vk::ShaderModule module = nullptr;
ShaderRecompiler::IR::CompiledShaderInfo info;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<uint32_t> function_code;
// VS or LS: the host format of each vertex input attribute.
std::vector<vk::Format> vertex_formats;
// Rectangle-list PS: the parameters it reads.
std::vector<RectListParameter> rect_parameters;
};
// A draw's topology, shader stages (from PipelineCache::GetGraphicsPrograms) and targets.
struct DrawRenderState {
RenderDepthInfo depth_info;
RenderColorInfo color_info[RENDER_COLOR_ATTACHMENTS_MAX] = {};
uint32_t color_count = 0;
// Color targets the pixel program writes.
uint32_t mrt_mask = 0;
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
bool primitive_restart_enable = false;
bool ps_active = false;
std::array<ShaderVertexInputInfo, 3> vertex_info;
ShaderPixelInputInfo ps_input_info;
// Input: the user-data slots a GPU-read indirect draw patches.
ShaderIndirectOffsets indirect_offsets;
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
}
};
// Writes the shader store on its own thread, running jobs in the order they were posted.
class ShaderStoreWriter {
public:
// Keeps the first `kept` bytes of the store at `path`, or starts it over with `header`. The
// thread runs only if that succeeded.
void Open(const std::filesystem::path& path, std::span<const uint8_t> header, uint64_t kept);
explicit operator bool() const { return m_thread.joinable(); }
// Compresses `record` and appends it on the store thread, in call order.
void Append(std::vector<uint8_t>&& record);
private:
friend struct PipelineCacheTestAccess;
void Post(Common::UniqueFunction<void>&& job);
Common::File m_file;
std::mutex m_mutex;
std::condition_variable_any m_ready;
std::queue<Common::UniqueFunction<void>> m_jobs;
// Declared last, so it drains the queue and joins before the rest is destroyed.
std::jthread m_thread;
};
// The owning renderer serializes access, including saves while the GPU is running.
class PipelineCache {
public:
explicit PipelineCache(GraphicContext& graphics);
// Without a shader store path nothing is kept across runs.
PipelineCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap,
const std::filesystem::path& shader_store = {});
~PipelineCache();
KYTY_CLASS_NO_COPY(PipelineCache);
void Save();
@@ -113,89 +183,109 @@ public:
vk::Pipeline pipeline = nullptr;
vk::DescriptorSetLayout descriptor_set_layout = nullptr;
bool uses_push_descriptors = false;
bool uses_image_heap = false;
};
struct GraphicsPrograms {
std::array<ShaderProgram, 3> vertex;
ShaderProgram pixel;
// Reads the shaders and registers bound in `command` into the state's shader stages.
void GetGraphicsPrograms(const CommandBuffer& command, DrawRenderState& state);
// Null skips the dispatch.
const ShaderModule* GetComputeProgram(const HW::ComputeShaderInfo& regs,
ShaderComputeInputInfo& input_info);
[[nodiscard]] uint32_t VertexStageCount() const { return vertex[1] ? 3u : 1u; }
};
GraphicsPrograms
GetGraphicsPrograms(const HW::VertexShaderInfo& vertex_regs,
const HW::PixelShaderInfo& pixel_regs, const HW::ShaderRegisters& sh,
const HW::Context& context, const HW::UserConfig& user_config,
std::span<const Prospero::ColorComponentMapping, 8> target_export_mapping,
bool pixel_active, std::array<ShaderVertexInputInfo, 3>& vertex_info,
ShaderPixelInputInfo& pixel_info);
ShaderProgram GetComputeProgram(const HW::ComputeShaderInfo& regs,
const HW::ShaderRegisters& sh,
ShaderComputeInputInfo& input_info);
Pipeline& GetGraphicsPipeline(std::span<const RenderColorInfo> colors,
const RenderDepthInfo& depth,
std::span<const ShaderVertexInputInfo> vertex_info,
CommandBuffer& command, const ShaderPixelInputInfo* ps_input_info,
vk::PrimitiveTopology topology, bool primitive_restart_enable,
const GraphicsPrograms& programs);
Pipeline& GetComputePipeline(const ShaderComputeInputInfo& input_info,
const ShaderProgram& compute_program);
Pipeline& GetGraphicsPipeline(const CommandBuffer& command, const DrawRenderState& state);
Pipeline& GetComputePipeline(const ShaderComputeInputInfo& input_info);
private:
struct ProgramCache;
friend struct PipelineCacheTestAccess;
struct GraphicsPipelineKey {
PipelineRenderingState rendering;
std::array<uint64_t, 3> vertex_shader_ids {};
uint64_t ps_shader_id = 0;
PipelineVertexInputState vertex_input;
PipelineStaticParameters static_params;
struct Program {
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
ShaderRecompiler::IR::ResourcePlan resource_plan;
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<ShaderModule> modules;
bool skip_dispatch = false;
bool operator==(const GraphicsPipelineKey& other) const {
return rendering == other.rendering && vertex_shader_ids == other.vertex_shader_ids &&
ps_shader_id == other.ps_shader_id && vertex_input == other.vertex_input &&
static_params == other.static_params;
}
[[nodiscard]] std::span<const uint32_t> FunctionCode() const;
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime);
template <bool HasCalls>
ShaderModule* FindModule(const ShaderRecompiler::IR::SrtRuntime& runtime,
uint32_t push_data_cursor);
};
struct PipelineKeyHash {
static void Mix(std::size_t& hash, std::size_t value) {
hash ^= value + static_cast<std::size_t>(0x9e3779b97f4a7c15ull) + (hash << 6u) +
(hash >> 2u);
}
struct ProgramKey {
ShaderType stage = ShaderType::Unknown;
uint64_t hash = 0;
uint32_t user_data_count = 0;
std::vector<uint32_t> static_state;
bool operator==(const ProgramKey&) const = default;
};
// Variants of one source share a bucket; ProgramKey equality compares the state.
struct ProgramKeyHash {
std::size_t operator()(const ProgramKey& key) const { return key.hash; }
};
struct GraphicsPipelineKeyHash {
std::size_t operator()(const GraphicsPipelineKey& key) const;
};
GraphicContext& m_graphics;
std::unique_ptr<ProgramCache> m_program_cache;
using ModuleMap = std::unordered_map<uint64_t, const ShaderModule*>;
GraphicContext& m_graphics;
ShaderRecompiler::IR::ImageHeapSource* m_image_heap;
std::unordered_map<ProgramKey, Program, ProgramKeyHash> m_program_cache;
// Modules compiled per stage.
std::array<uint32_t, static_cast<size_t>(ShaderType::TessellationEvaluation) + 1> m_compiled {};
ProgramKey m_lookup_key;
vk::PipelineCache m_driver_cache = nullptr;
std::filesystem::path m_driver_cache_path;
std::chrono::steady_clock::time_point m_next_driver_cache_save;
bool m_driver_cache_dirty = false;
std::unordered_map<GraphicsPipelineKey, std::unique_ptr<Pipeline>, GraphicsPipelineKeyHash>
m_graphics_pipelines;
std::unordered_map<uint64_t, std::unique_ptr<Pipeline>> m_compute_pipelines;
std::unordered_map<GraphicsPipelineKey, Pipeline, GraphicsPipelineKeyHash> m_graphics_pipelines;
std::unordered_map<uint64_t, Pipeline> m_compute_pipelines;
std::unordered_map<uint64_t, ShaderTessellationInputInfo> m_tess_layouts;
// Records everything first created; closed without a shader store.
ShaderStoreWriter m_store;
template <typename InputInfo>
const ShaderModule* GetProgram(const ShaderParams& params, InputInfo& input_info,
uint32_t& push_data_cursor);
ShaderModule& CompileModule(Program& program, const ProgramKey& key,
ShaderRecompiler::CompileOptions options,
ShaderRecompiler::TranslateResult translated,
uint32_t push_data_cursor);
void InitializeDriverCache();
void CheckpointDriverCache();
// pipelineSerialization.cpp
void WarmUp(const std::filesystem::path& path);
bool LoadRecord(std::span<const uint8_t>& records, std::vector<GraphicsPipelineKey>& pipelines);
void StoreModule(const ProgramKey& key, const Program& program, const ShaderModule& module,
const std::vector<uint32_t>& spirv);
void StorePipeline(const GraphicsPipelineKey& key);
void StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout);
// Every module by id, with id 0 naming an absent stage.
[[nodiscard]] ModuleMap MapModules() const;
// False when a stage id of the key is not in `modules`.
bool CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key,
const ModuleMap& modules);
};
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id);
// The shader store of the running title; empty unless --shader-cache is on in a Release build
// without debug dumps, which change the SPIR-V.
std::filesystem::path ShaderStorePath();
// Host capabilities that change the emitted SPIR-V.
ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics);
// Stages are indexed like GraphicsPipelineKey::stage_ids.
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const PipelineRenderingState& rendering,
const PipelineVertexInputState& vertex_input,
std::span<const ShaderVertexInputInfo> vertex_info,
const ShaderPixelInputInfo* ps_input_info,
const PipelineCache::GraphicsPrograms& programs,
const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache);
const GraphicsPipelineKey& key,
const std::array<const ShaderModule*, 4>& stages,
vk::PipelineCache driver_cache);
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const ShaderComputeInputInfo& input_info,
vk::ShaderModule compute_module, vk::PipelineCache driver_cache);
const ShaderModule& module, vk::PipelineCache driver_cache);
} // namespace Libs::Graphics
@@ -0,0 +1,329 @@
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <fmt/format.h>
#include <ranges>
#include <xxhash.h>
#include <zstd.h>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__APPLE__)
#include <mach-o/dyld.h>
#endif
// The shader store: a header, then zstd-compressed records of everything a run created first.
// Records are only appended, so a store cut short by a crash keeps every complete record.
namespace Libs::Graphics {
namespace {
using ShaderRecompiler::IR::IoAll;
using ShaderRecompiler::IR::Reader;
using ShaderRecompiler::IR::Writer;
enum class Record : uint8_t { Module, GraphicsPipeline, TessellationLayout };
// Precedes each record's zstd frame.
struct Frame {
uint32_t compressed_size = 0;
uint32_t raw_size = 0;
uint64_t checksum = 0; // XXH3 of the compressed bytes.
};
// Changes with every build, so records never meet code that encodes them differently. Zero when
// the executable cannot be found.
uint64_t BuildStamp() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
std::array<wchar_t, 32768> name {};
GetModuleFileNameW(nullptr, name.data(), static_cast<DWORD>(name.size()));
const std::filesystem::path path = name.data();
#elif defined(__APPLE__)
std::array<char, 4096> name {};
auto size = static_cast<uint32_t>(name.size());
_NSGetExecutablePath(name.data(), &size);
const std::filesystem::path path = name.data();
#else
const std::filesystem::path path = "/proc/self/exe";
#endif
std::error_code error;
const auto time = std::filesystem::last_write_time(path, error).time_since_epoch().count();
return error ? 0 : static_cast<uint64_t>(time);
}
// What every record depends on besides its own fields; empty when the build is unknown.
std::vector<uint8_t> StoreHeader(const GraphicContext& graphics) {
const auto stamp = BuildStamp();
if (stamp == 0) {
return {};
}
Writer header;
IoAll(header, stamp, SpirvProfile(graphics),
graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize);
return std::move(header.bytes);
}
// Record fields, shared by writing and reading.
template <class S, class Key> void IoKey(S& io, Key& key) {
auto& [stage, hash, user_data_count, static_state] = key;
IoAll(io, stage, hash, user_data_count, static_state);
}
template <class S, class Module> void IoModule(S& io, Module& stage) {
// The handle comes from the SPIR-V; stored programs have no function code.
auto& [id, handle, info, specialization, function_code, vertex_formats, rect_parameters] = stage;
IoAll(io, id, info, specialization, vertex_formats, rect_parameters);
}
} // namespace
void ShaderStoreWriter::Open(const std::filesystem::path& path, std::span<const uint8_t> header,
uint64_t kept) {
Common::File::CreateDirectories(path.parent_path());
if (kept == 0) {
if (m_file.Create(path)) {
m_file.Write(header.data(), static_cast<uint32_t>(header.size()));
}
} else if (m_file.Open(path, Common::File::Mode::ReadWrite)) {
m_file.Truncate(kept);
m_file.Seek(kept);
}
if (m_file.IsInvalid()) {
return;
}
m_thread = std::jthread([this](std::stop_token stop) {
std::unique_lock lock(m_mutex);
// Once stop is requested the wait returns false only when the queue is empty.
while (m_ready.wait(lock, stop, [this] { return !m_jobs.empty(); })) {
const auto job = std::move(m_jobs.front());
m_jobs.pop();
lock.unlock();
job();
lock.lock();
}
});
}
void ShaderStoreWriter::Append(std::vector<uint8_t>&& record) {
Post([this, record = std::move(record)] {
std::vector<uint8_t> bytes(sizeof(Frame) + ZSTD_compressBound(record.size()));
const auto size = ZSTD_compress(bytes.data() + sizeof(Frame), bytes.size() - sizeof(Frame),
record.data(), record.size(), 3);
EXIT_IF(ZSTD_isError(size));
const Frame frame {.compressed_size = static_cast<uint32_t>(size),
.raw_size = static_cast<uint32_t>(record.size()),
.checksum = XXH3_64bits(bytes.data() + sizeof(Frame), size)};
std::memcpy(bytes.data(), &frame, sizeof(frame));
// A failed write leaves a partial record; the next run drops it and everything after it.
m_file.Write(bytes.data(), static_cast<uint32_t>(sizeof(Frame) + size));
});
}
void ShaderStoreWriter::Post(Common::UniqueFunction<void>&& job) {
{
std::scoped_lock lock(m_mutex);
m_jobs.push(std::move(job));
}
m_ready.notify_one();
}
void PipelineCache::WarmUp(const std::filesystem::path& path) {
if (path.empty()) {
return;
}
const auto header = StoreHeader(m_graphics);
if (header.empty()) {
return;
}
const auto start = std::chrono::steady_clock::now();
std::vector<uint8_t> bytes;
if (Common::File file;
Common::File::IsFileExisting(path) && file.Open(path, Common::File::Mode::Read)) {
bytes.resize(file.Size());
uint32_t read = 0;
file.Read(bytes.data(), static_cast<uint32_t>(bytes.size()), &read);
bytes.resize(read);
}
Log::WriteToConsoleAndLog(
fmt::format("Shader cache: compiling shaders and pipelines from {} ({:.1f} MB)\n",
Common::PathToString(path), static_cast<double>(bytes.size()) / 1'000'000.0));
std::vector<GraphicsPipelineKey> pipelines;
auto records = std::span<const uint8_t>(bytes);
const bool reusable =
records.size() >= header.size() && std::ranges::equal(records.first(header.size()), header);
if (reusable) {
records = records.subspan(header.size());
while (LoadRecord(records, pipelines)) {
}
}
// A damaged record ends the store; the records before it stay.
m_store.Open(path, header, reusable ? bytes.size() - records.size() : 0);
const auto modules = MapModules();
for (const auto& key: pipelines) {
Pipeline pipeline;
if (CreateGraphicsPipeline(pipeline, key, modules)) {
m_graphics_pipelines.emplace(key, pipeline);
}
}
// A compute pipeline depends only on its module.
for (const auto* module: modules | std::views::values) {
if (module != nullptr && module->info.stage == ShaderType::Compute) {
CreatePipelineInternal(m_graphics, m_compute_pipelines[module->id], *module,
m_driver_cache);
}
}
if (!m_graphics_pipelines.empty() || !m_compute_pipelines.empty()) {
CheckpointDriverCache();
}
Log::WriteToConsoleAndLog(fmt::format(
"Shader cache: loaded {} programs, {} modules, {} pipelines in {} ms\n",
m_program_cache.size(), modules.size() - 1,
m_graphics_pipelines.size() + m_compute_pipelines.size(),
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() -
start)
.count()));
}
bool PipelineCache::LoadRecord(std::span<const uint8_t>& records,
std::vector<GraphicsPipelineKey>& pipelines) {
Frame frame;
if (records.size() < sizeof(frame)) {
return false;
}
std::memcpy(&frame, records.data(), sizeof(frame));
if (records.size() - sizeof(frame) < frame.compressed_size) {
return false;
}
const auto compressed = records.subspan(sizeof(frame), frame.compressed_size);
if (XXH3_64bits(compressed.data(), compressed.size()) != frame.checksum ||
ZSTD_getFrameContentSize(compressed.data(), compressed.size()) != frame.raw_size) {
return false;
}
std::vector<uint8_t> raw(frame.raw_size);
if (ZSTD_decompress(raw.data(), raw.size(), compressed.data(), compressed.size()) != raw.size()) {
return false;
}
Reader reader {raw};
auto type = Record::Module;
const auto complete = [&] { return reader.ok && reader.bytes.empty(); };
IoAll(reader, type);
switch (type) {
case Record::Module: {
ProgramKey key;
std::vector<uint8_t> plan;
ShaderModule module;
std::vector<uint32_t> spirv;
IoKey(reader, key);
IoAll(reader, plan);
IoModule(reader, module);
IoAll(reader, spirv);
if (!complete()) {
return false;
}
const auto [program, inserted] = m_program_cache.try_emplace(std::move(key));
if (inserted &&
!ShaderRecompiler::IR::LoadResourcePlan(plan, program->second.resource_plan)) {
m_program_cache.erase(program);
return false;
}
module.module = CompileSPV(spirv, m_graphics.device);
if (module.module == nullptr) {
return false;
}
program->second.modules.push_back(std::move(module));
break;
}
case Record::GraphicsPipeline: {
GraphicsPipelineKey key;
reader.Raw(&key, sizeof(key));
if (!complete()) {
return false;
}
pipelines.push_back(key);
break;
}
case Record::TessellationLayout: {
uint64_t key = 0;
ShaderTessellationInputInfo layout;
IoAll(reader, key, layout);
if (!complete()) {
return false;
}
m_tess_layouts.try_emplace(key, layout);
break;
}
default: return false;
}
records = records.subspan(sizeof(frame) + compressed.size());
return true;
}
// Records are encoded here, since programs and modules change later.
void PipelineCache::StoreModule(const ProgramKey& key, const Program& program,
const ShaderModule& module, const std::vector<uint32_t>& spirv) {
if (!m_store) {
return;
}
Writer record;
IoAll(record, Record::Module);
IoKey(record, key);
IoAll(record, ShaderRecompiler::IR::SaveResourcePlan(program.resource_plan));
IoModule(record, module);
IoAll(record, spirv);
m_store.Append(std::move(record.bytes));
}
void PipelineCache::StorePipeline(const GraphicsPipelineKey& key) {
if (!m_store) {
return;
}
Writer record;
IoAll(record, Record::GraphicsPipeline);
record.Raw(&key, sizeof(key));
m_store.Append(std::move(record.bytes));
}
void PipelineCache::StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout) {
if (!m_store) {
return;
}
Writer record;
IoAll(record, Record::TessellationLayout, key, layout);
m_store.Append(std::move(record.bytes));
}
PipelineCache::ModuleMap PipelineCache::MapModules() const {
ModuleMap modules {{0, nullptr}};
for (const auto& program: m_program_cache | std::views::values) {
for (const auto& module: program.modules) {
modules.emplace(module.id, &module);
}
}
return modules;
}
bool PipelineCache::CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key,
const ModuleMap& modules) {
std::array<const ShaderModule*, 4> stages {};
for (size_t i = 0; i < stages.size(); i++) {
const auto found = modules.find(key.stage_ids[i]);
if (found == modules.end()) {
return false;
}
stages[i] = found->second;
}
CreatePipelineInternal(m_graphics, pipeline, key, stages, m_driver_cache);
return true;
}
} // namespace Libs::Graphics
@@ -1,6 +1,7 @@
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
#include "common/assert.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "graphics/shader/shader.h"
#include "graphics/shader/shaderBindings.h"
@@ -52,6 +53,7 @@ vk::MemoryBarrier MakeShaderWriteDependency() {
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite |
vk::AccessFlagBits::eVertexAttributeRead |
vk::AccessFlagBits::eIndexRead | vk::AccessFlagBits::eUniformRead |
vk::AccessFlagBits::eIndirectCommandRead |
vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite;
@@ -89,10 +91,11 @@ vk::BufferMemoryBarrier MakeGdsDependency(vk::Buffer buffer) {
bool HasShaderBufferWrites(const ShaderStageRuntime& runtime) {
EXIT_IF(!runtime);
const auto& program = *runtime.program;
const auto& program = runtime.module->info;
const auto& resources = *runtime.resources;
EXIT_IF(resources.buffers.size() != program.info.buffers.size());
bool has_writes = false;
// Address writes reach memory outside the bound buffer descriptors.
bool has_writes = program.has_address_writes;
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
if (!program.info.buffers[i].written) {
continue;
@@ -13,7 +13,6 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
@@ -21,124 +20,11 @@
#include <algorithm>
#include <array>
#include <bit>
#include <limits>
#include <span>
#include <vector>
namespace Libs::Graphics {
// IDK: maybe we can remove it?
constexpr uint8_t kTemporaryVertexAttribFormat113 =
static_cast<uint8_t>(Prospero::VertexAttribFormat::k16_16SInt);
constexpr uint32_t kTemporaryPs5BufferFormat121 = 121u;
static bool NarrowInputFormat(vk::Format& format, uint32_t& size, uint32_t used_components) {
if (used_components == 0 || used_components >= size) {
return false;
}
switch (format) {
case vk::Format::eR32G32B32A32Sfloat:
switch (used_components) {
case 1: format = vk::Format::eR32Sfloat; break;
case 2: format = vk::Format::eR32G32Sfloat; break;
case 3: format = vk::Format::eR32G32B32Sfloat; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR32G32B32Sfloat:
switch (used_components) {
case 1: format = vk::Format::eR32Sfloat; break;
case 2: format = vk::Format::eR32G32Sfloat; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR16G16B16A16Sfloat:
switch (used_components) {
case 1: format = vk::Format::eR16Sfloat; break;
case 2: format = vk::Format::eR16G16Sfloat; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR8G8B8A8Unorm:
switch (used_components) {
case 1: format = vk::Format::eR8Unorm; break;
case 2: format = vk::Format::eR8G8Unorm; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR8G8B8A8Snorm:
if (used_components != 2) {
return false;
}
format = vk::Format::eR8G8Snorm;
size = 2;
return true;
case vk::Format::eR8G8B8A8Uint:
switch (used_components) {
case 1: format = vk::Format::eR8Uint; break;
case 2: format = vk::Format::eR8G8Uint; break;
default: return false;
}
size = used_components;
return true;
default: break;
}
return false;
}
static void GetInputFormat(const ShaderBufferResource& res, vk::Format& format, uint32_t& size,
uint32_t used_components) {
const auto fmt = res.Format();
const auto raw_format = res.RawFormat();
if (raw_format == kTemporaryVertexAttribFormat113) {
static bool logged_113 = false;
if (!logged_113) {
LOGF("InputFormat: temporary: accepting invalid PS5 buffer format 113 as "
"vk::Format::eR32G32B32A32Sfloat\n");
logged_113 = true;
}
format = vk::Format::eR32G32B32A32Sfloat;
size = 4;
if (NarrowInputFormat(format, size, used_components)) {
LOGF("InputFormat: narrowing fmt=%u to %s for used_components=%u\n", raw_format,
vk::to_string(format).c_str(), used_components);
}
return;
}
if (raw_format == kTemporaryPs5BufferFormat121) {
static bool logged_121 = false;
if (!logged_121) {
LOGF("InputFormat: accepting PS5 buffer format 121 as vk::Format::eR16G16Sfloat\n");
logged_121 = true;
}
format = vk::Format::eR16G16Sfloat;
size = 2;
return;
}
format = VulkanFormat(fmt);
size = ShaderRecompiler::Format::GetFormatInfo(fmt).component_count;
if (format == vk::Format::eUndefined || size == 0) {
EXIT("unknown vertex format: fmt = %u\n", raw_format);
}
if (NarrowInputFormat(format, size, used_components)) {
static std::atomic<uint64_t> log_count = 0;
auto log_id = log_count.fetch_add(1);
if (log_id < 32) {
LOGF("VertexInput: narrowed vertex format to %" PRIu32
" component(s) for shader fetch\n",
used_components);
}
}
}
static vk::BlendFactor GetBlendFactor(uint32_t factor) {
switch (static_cast<Prospero::BlendFactor>(factor)) {
case Prospero::BlendFactor::kZero: return vk::BlendFactor::eZero;
@@ -196,21 +82,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
const auto& limits = graphics.GetPhysicalDeviceProperties().limits;
uint64_t descriptor_count = 0;
uint64_t storage_buffers = 0;
std::array<uint64_t, 32> stage_buffers {}, stage_resources {};
uint64_t samplers = 0;
std::array<uint64_t, 32> stage_buffers {}, stage_resources {}, stage_samplers {};
for (const auto& binding: bindings) {
descriptor_count += binding.descriptorCount;
const bool storage = binding.descriptorType == vk::DescriptorType::eStorageBuffer;
const bool sampler = binding.descriptorType == vk::DescriptorType::eSampler;
if (storage) storage_buffers += binding.descriptorCount;
if (sampler) samplers += binding.descriptorCount;
for (auto stages = static_cast<uint32_t>(binding.stageFlags); stages != 0; stages &= stages - 1u) {
const auto stage = std::countr_zero(stages);
if (storage) stage_buffers[stage] += binding.descriptorCount;
if (binding.descriptorType != vk::DescriptorType::eSampler)
if (sampler) stage_samplers[stage] += binding.descriptorCount;
else
stage_resources[stage] += binding.descriptorCount;
if (stage_buffers[stage] > limits.maxPerStageDescriptorStorageBuffers ||
stage_resources[stage] > limits.maxPerStageResources) {
stage_resources[stage] > limits.maxPerStageResources ||
stage_samplers[stage] > limits.maxPerStageDescriptorSamplers) {
EXIT("native descriptor stage %u exceeds device limits: buffers=%" PRIu64
"/%u resources=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources);
"/%u resources=%" PRIu64 "/%u samplers=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources,
stage_samplers[stage], limits.maxPerStageDescriptorSamplers);
}
}
}
@@ -218,6 +110,10 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
EXIT("native storage-buffer descriptors exceed device limit: %" PRIu64 "/%u\n",
storage_buffers, limits.maxDescriptorSetStorageBuffers);
}
if (samplers > limits.maxDescriptorSetSamplers) {
EXIT("native sampler descriptors exceed device limit: %" PRIu64 "/%u\n",
samplers, limits.maxDescriptorSetSamplers);
}
pipeline.uses_push_descriptors = descriptor_count <= graphics.max_push_descriptors;
vk::DescriptorSetLayoutCreateInfo create {};
@@ -232,33 +128,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const PipelineRenderingState& rendering,
const PipelineVertexInputState& vertex_input,
std::span<const ShaderVertexInputInfo> vertex_info,
const ShaderPixelInputInfo* ps_input_info,
const PipelineCache::GraphicsPrograms& programs,
const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache) {
const auto& vs_input_info = vertex_info.front();
const auto& vertex_program = programs.vertex[0];
const auto& pixel_program = programs.pixel;
const bool tessellation = vertex_info.size() == 3;
const bool ps_active = ps_input_info != nullptr;
EXIT_IF(!vertex_program || (ps_active && !pixel_program));
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
rendering.stencil_format != vk::Format::eUndefined;
EXIT_IF(!vs_input_info.stage);
const bool mesh = vs_input_info.stage.program->stage == ShaderType::Mesh;
const GraphicsPipelineKey& key,
const std::array<const ShaderModule*, 4>& stages,
vk::PipelineCache driver_cache) {
const auto& rendering = key.rendering;
const auto& vertex_input = key.vertex_input;
const auto& static_params = key.static_params;
const auto& vertex = *stages[0];
const auto* pixel = stages[3];
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
rendering.stencil_format != vk::Format::eUndefined;
const bool mesh = vertex.info.stage == ShaderType::Mesh;
EXIT_NOT_IMPLEMENTED(mesh && !graphics.mesh_shader_enabled);
const bool rect_list =
!mesh && !tessellation && static_params.topology == vk::PrimitiveTopology::ePatchList;
const bool rect_list = static_params.patch_control_points != 0 && stages[1] == nullptr;
vk::ShaderModule tess_control_shader_module = nullptr;
vk::ShaderModule tess_eval_shader_module = nullptr;
if (rect_list) {
const auto shaders =
BuildRectListShaders(vs_input_info, ps_active ? ps_input_info : nullptr);
std::span<const RectListParameter> parameters;
if (pixel != nullptr) parameters = pixel->rect_parameters;
const auto shaders = BuildRectListShaders(parameters, vertex.info.param_export_mask);
tess_control_shader_module = CompileSPV(shaders.control, graphics.device);
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done module=%p\n",
@@ -277,11 +167,12 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::PipelineShaderStageCreateInfo shader_stages[4] {};
uint32_t shader_stage_count = 0;
for (uint32_t i = 0; i < vertex_info.size(); i++) {
shader_stages[shader_stage_count++] = {.stage =
NativeShaderStage(vertex_info[i].logical_stage),
.module = programs.vertex[i].module,
.pName = "main"};
for (const auto* stage: stages) {
if (stage != nullptr) {
shader_stages[shader_stage_count++] = {.stage = NativeShaderStage(stage->info.stage),
.module = stage->module,
.pName = "main"};
}
}
if (rect_list) {
shader_stages[shader_stage_count++] = {.stage =
@@ -293,11 +184,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
.module = tess_eval_shader_module,
.pName = "main"};
}
if (ps_active) {
shader_stages[shader_stage_count++] = {.stage = vk::ShaderStageFlagBits::eFragment,
.module = pixel_program.module,
.pName = "main"};
}
vk::VertexInputAttributeDescription input_attr[ShaderVertexInputInfo::RES_MAX] {};
vk::VertexInputBindingDescription input_desc[ShaderVertexInputInfo::RES_MAX] {};
@@ -313,46 +199,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
input_attr[index].binding = vertex_input.attributes[index].binding;
input_attr[index].location = index;
input_attr[index].offset = vertex_input.attributes[index].offset;
uint32_t attr_size = 4;
const auto registers_num = vs_input_info.resources_dst[index].registers_num;
const auto compiled_components =
vs_input_info.stage.program->info.vertex_fetch_components[index];
const auto used_components =
compiled_components > 0 ? static_cast<int>(compiled_components) : registers_num;
GetInputFormat(vs_input_info.resources[index], input_attr[index].format, attr_size,
static_cast<uint32_t>(used_components));
if (graphics_debug_dump_enabled()) {
static std::atomic_uint log_count = 0;
const auto log_id = log_count.fetch_add(1, std::memory_order_relaxed);
if (log_id < 128) {
LOGF("VertexInputState[%u]: attr=%u binding=%u offset=%u stride=%u fmt=%d "
"src_fmt=%u dst=v%u regs=%u"
" fetched_components=%u attr_size=%u swizzle=%u,%u,%u,%u\n",
log_id, index, input_attr[index].binding, input_attr[index].offset,
input_desc[input_attr[index].binding].stride,
static_cast<int>(input_attr[index].format),
static_cast<uint32_t>(vs_input_info.resources[index].Format()),
static_cast<uint32_t>(vs_input_info.resources_dst[index].register_start),
static_cast<uint32_t>(registers_num), static_cast<uint32_t>(used_components),
attr_size, static_cast<uint32_t>(vs_input_info.resources[index].DstSelX()),
static_cast<uint32_t>(vs_input_info.resources[index].DstSelY()),
static_cast<uint32_t>(vs_input_info.resources[index].DstSelZ()),
static_cast<uint32_t>(vs_input_info.resources[index].DstSelW()));
}
}
if (vs_input_info.resources[index].OutOfBounds() != 0) {
static bool logged = false;
if (!logged) {
LOGF("VertexInput: temporary: accepting PS5 out-of-bounds behavior %" PRIu8 "\n",
vs_input_info.resources[index].OutOfBounds());
logged = true;
}
}
EXIT_IF(registers_num < 1 || registers_num > 4);
input_attr[index].format = vertex.vertex_formats[index];
}
vk::PipelineVertexInputStateCreateInfo vertex_input_info {};
@@ -372,17 +219,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::PipelineViewportStateCreateInfo viewport_state {};
viewport_state.pNext = &depth_clip_control;
vk::CullModeFlags cull_mode = vk::CullModeFlagBits::eNone;
if (static_params.cull_back) {
cull_mode |= vk::CullModeFlagBits::eBack;
}
if (static_params.cull_front) {
cull_mode |= vk::CullModeFlagBits::eFront;
}
vk::FrontFace front_face =
(static_params.face ? vk::FrontFace::eClockwise : vk::FrontFace::eCounterClockwise);
vk::PipelineRasterizationDepthClipStateCreateInfoEXT clip_ext {};
clip_ext.depthClipEnable = static_params.depth_clip_enable ? VK_TRUE : VK_FALSE;
@@ -403,8 +239,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
provoking_vertex.pNext = rasterizer.pNext;
rasterizer.pNext = &provoking_vertex;
}
rasterizer.cullMode = cull_mode;
rasterizer.frontFace = front_face;
rasterizer.polygonMode = static_params.polygon_mode;
rasterizer.lineWidth = 1.0f;
@@ -412,6 +246,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
multisampling.sampleShadingEnable = static_params.sample_shading_enable ? VK_TRUE : VK_FALSE;
multisampling.rasterizationSamples = vulkan_sample_count(static_params.samples);
multisampling.minSampleShading = 1.0f;
multisampling.alphaToCoverageEnable =
static_params.alpha_to_coverage_enable ? VK_TRUE : VK_FALSE;
vk::PipelineColorBlendAttachmentState color_blend_attachment[RENDER_COLOR_ATTACHMENTS_MAX] = {};
for (uint32_t i = 0; i < rendering.color_count; i++) {
@@ -458,23 +294,22 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
vk::ShaderStageFlags graphics_stages = vk::ShaderStageFlagBits::eFragment;
for (const auto& stage: vertex_info) {
const auto native_stage = NativeShaderStage(stage.logical_stage);
AddLayoutBindings(descriptor_bindings, *stage.stage.program, native_stage);
graphics_stages |= native_stage;
}
if (ps_active) {
EXIT_IF(!ps_input_info->stage);
AddLayoutBindings(descriptor_bindings, *ps_input_info->stage.program,
vk::ShaderStageFlagBits::eFragment);
for (const auto* stage: stages) {
if (stage != nullptr) {
const auto native_stage = NativeShaderStage(stage->info.stage);
AddLayoutBindings(descriptor_bindings, stage->info, native_stage);
pipeline.uses_image_heap |= stage->info.info.uses_image_heap;
graphics_stages |= native_stage;
}
}
CreateDescriptorLayout(graphics, pipeline, descriptor_bindings);
const vk::PushConstantRange push_constants {graphics_stages, 0,
ShaderRecompiler::IR::NativePushConstantSize};
const std::array set_layouts {pipeline.descriptor_set_layout, graphics.image_heap_layout};
vk::PipelineLayoutCreateInfo pipeline_layout_info {};
pipeline_layout_info.setLayoutCount = 1;
pipeline_layout_info.pSetLayouts = &pipeline.descriptor_set_layout;
pipeline_layout_info.setLayoutCount = pipeline.uses_image_heap ? 2u : 1u;
pipeline_layout_info.pSetLayouts = set_layouts.data();
pipeline_layout_info.pushConstantRangeCount = 1;
pipeline_layout_info.pPushConstantRanges = &push_constants;
@@ -483,7 +318,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreatePipelineLayout begin VS=%" PRIu64 " PS=%" PRIu64
" set_layouts=1 push_constants=%" PRIu32 "\n",
vertex_program.id, ps_active ? pixel_program.id : 0, 1u);
key.stage_ids[0], key.stage_ids[3], 1u);
}
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
&pipeline.pipeline_layout);
@@ -493,14 +328,14 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
std::vector<vk::DynamicState> dynamic_states {
vk::DynamicState::eViewportWithCount,
vk::DynamicState::eScissorWithCount,
vk::DynamicState::eLineWidth,
vk::DynamicState::eCullMode,
vk::DynamicState::eFrontFace,
vk::DynamicState::eDepthTestEnable,
vk::DynamicState::eDepthWriteEnable,
vk::DynamicState::eDepthCompareOp,
@@ -540,9 +375,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
pipeline_info.pVertexInputState = mesh ? nullptr : &vertex_input_info;
pipeline_info.pInputAssemblyState = mesh ? nullptr : &input_assembly;
vk::PipelineTessellationStateCreateInfo tessellation_state {};
tessellation_state.patchControlPoints =
tessellation ? vs_input_info.tess.input_control_points : 3u;
pipeline_info.pTessellationState = (rect_list || tessellation) ? &tessellation_state : nullptr;
tessellation_state.patchControlPoints = static_params.patch_control_points;
pipeline_info.pTessellationState =
static_params.patch_control_points != 0 ? &tessellation_state : nullptr;
pipeline_info.pViewportState = &viewport_state;
pipeline_info.pRasterizationState = &rasterizer;
pipeline_info.pMultisampleState = &multisampling;
@@ -558,7 +393,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
LOGF("PipelineTrace: vkCreateGraphicsPipelines begin VS=%" PRIu64 " PS=%" PRIu64
" topology=%" PRIu32 " color_mask=0x%08" PRIx32
" depth=%s blend=%s dyn_states=%" PRIu32 "\n",
vertex_program.id, ps_active ? pixel_program.id : 0,
key.stage_ids[0], key.stage_ids[3],
static_cast<uint32_t>(static_params.topology), static_params.color_mask[0],
(with_depth ? "true" : "false"), (static_params.blend_enable[0] ? "true" : "false"),
dynamic_state.dynamicStateCount);
@@ -571,29 +406,18 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
if (tess_control_shader_module != nullptr) {
graphics.device.destroyShaderModule(tess_control_shader_module, nullptr);
}
if (tess_eval_shader_module != nullptr) {
graphics.device.destroyShaderModule(tess_eval_shader_module, nullptr);
}
graphics.device.destroyShaderModule(tess_control_shader_module, nullptr);
graphics.device.destroyShaderModule(tess_eval_shader_module, nullptr);
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const ShaderComputeInputInfo& input_info,
vk::ShaderModule compute_module, vk::PipelineCache driver_cache) {
EXIT_IF(compute_module == nullptr);
const ShaderModule& module, vk::PipelineCache driver_cache) {
vk::PipelineShaderStageCreateInfo comp_shader_stage_info {};
vk::PipelineShaderStageRequiredSubgroupSizeCreateInfo comp_subgroup_size {};
comp_shader_stage_info.stage = vk::ShaderStageFlagBits::eCompute;
comp_shader_stage_info.module = compute_module;
comp_shader_stage_info.module = module.module;
comp_shader_stage_info.pName = "main";
EXIT_IF(!input_info.stage);
const auto wave_size = input_info.stage.program->wave_size;
const auto wave_size = module.info.wave_size;
if (graphics.compute_subgroup_size_control_enabled &&
wave_size >= graphics.min_subgroup_size && wave_size <= graphics.max_subgroup_size) {
comp_subgroup_size.requiredSubgroupSize = wave_size;
@@ -601,46 +425,38 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
AddLayoutBindings(descriptor_bindings, *input_info.stage.program,
vk::ShaderStageFlagBits::eCompute);
AddLayoutBindings(descriptor_bindings, module.info, vk::ShaderStageFlagBits::eCompute);
pipeline.uses_image_heap = module.info.info.uses_image_heap;
CreateDescriptorLayout(graphics, pipeline, descriptor_bindings);
const vk::PushConstantRange push_constants {vk::ShaderStageFlagBits::eCompute, 0,
ShaderRecompiler::IR::NativePushConstantSize};
const std::array set_layouts {pipeline.descriptor_set_layout, graphics.image_heap_layout};
vk::PipelineLayoutCreateInfo pipeline_layout_info {};
pipeline_layout_info.setLayoutCount = 1;
pipeline_layout_info.pSetLayouts = &pipeline.descriptor_set_layout;
pipeline_layout_info.setLayoutCount = pipeline.uses_image_heap ? 2u : 1u;
pipeline_layout_info.pSetLayouts = set_layouts.data();
pipeline_layout_info.pushConstantRangeCount = 1;
pipeline_layout_info.pPushConstantRanges = &push_constants;
EXIT_IF(pipeline.pipeline_layout != nullptr);
LOGF("PipelineTrace: vkCreatePipelineLayout CS begin set_layouts=1 push_constants=%u\n",
1u);
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
&pipeline.pipeline_layout);
LOGF("PipelineTrace: vkCreatePipelineLayout CS done result=%s layout=%p\n",
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline_layout));
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::ComputePipelineCreateInfo info {};
if (module.info.info.uses_gds) {
info.flags = vk::PipelineCreateFlagBits::eDispatchBase;
}
info.stage = comp_shader_stage_info;
info.layout = pipeline.pipeline_layout;
info.basePipelineIndex = -1;
EXIT_IF(pipeline.pipeline != nullptr);
LOGF("PipelineTrace: vkCreateComputePipelines begin layout=%p\n",
static_cast<void*>(pipeline.pipeline_layout));
result = graphics.device.createComputePipelines(driver_cache, 1, &info, nullptr,
&pipeline.pipeline);
LOGF("PipelineTrace: vkCreateComputePipelines done result=%s pipeline=%p\n",
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline));
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
}
} // namespace Libs::Graphics
+23 -17
View File
@@ -23,14 +23,9 @@ class Shader;
} // namespace HW
struct GraphicContext;
struct ShaderBufferResource;
struct ShaderComputeInputInfo;
struct RenderDepthInfo;
struct RenderColorInfo;
struct DrawCallInfo;
struct DrawEmitInfo;
struct DrawIndexBufferSource;
struct DrawRenderState;
class RenderContext;
class CommandScheduler;
struct RenderExecutorTestAccess;
@@ -48,6 +43,14 @@ enum class DrawOffsetSource : uint8_t {
IndirectArgs,
};
// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart.
struct DrawIndirectArgs {
uint64_t address = 0;
uint32_t count = 0;
uint32_t stride = 0;
ShaderIndirectOffsets offsets;
};
struct DrawIndexArgs {
uint32_t index_count = 0;
const void* index_addr = nullptr;
@@ -57,6 +60,8 @@ struct DrawIndexArgs {
uint32_t first_instance = 0;
DrawOffsetSource offset_source = DrawOffsetSource::DrawState;
uint32_t render_target_slice_offset = 0;
// With indirect records, index_count is INDEX_BUFFER_SIZE.
DrawIndirectArgs indirect;
};
struct DrawAutoArgs {
@@ -66,6 +71,7 @@ struct DrawAutoArgs {
uint32_t first_instance = 0;
DrawOffsetSource offset_source = DrawOffsetSource::DrawState;
uint32_t render_target_slice_offset = 0;
DrawIndirectArgs indirect;
};
struct SubmitInfo {
@@ -161,18 +167,19 @@ public:
void CommitBindings(CommandBuffer& buffer, vk::PipelineBindPoint pipeline_bind_point,
const PipelineCache::Pipeline& pipeline,
std::span<PreparedBindings* const> bindings);
[[nodiscard]] TextureBinding ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderRecompiler::IR::DescriptorValue& value);
private:
void DrawIndex(uint64_t submit_id, CommandBuffer& buffer, const DrawIndexArgs& args);
void DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args);
// False, with nothing drawn, when the draw's indirect records need CPU counts.
bool DrawIndex(uint64_t submit_id, CommandBuffer& buffer, const DrawIndexArgs& args);
bool DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args);
struct GraphicsBindings {
std::array<PreparedBindings, 3> vertex;
std::optional<PreparedBindings> pixel;
};
[[nodiscard]] TextureBinding ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderRecompiler::IR::DescriptorValue& value);
void PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
std::span<RenderColorInfo> colors);
void ResolveRenderColorTarget(CommandBuffer& buffer, RenderColorInfo& target,
@@ -187,9 +194,8 @@ private:
uint32_t render_target_slice_offset,
DrawRenderState& state);
void ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer, const DrawCallInfo& draw,
DrawRenderState& state, vk::PrimitiveTopology topology,
const DrawEmitInfo& emit, const DrawIndexBufferSource& index_source,
bool primitive_restart_enable);
DrawRenderState& state, const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source);
[[nodiscard]] RenderState AcquireRenderTargets(CommandBuffer& buffer, RenderColorInfo* colors,
uint32_t color_count, RenderDepthInfo& depth,
vk::ImageAspectFlags& feedback_aspects,
@@ -201,11 +207,11 @@ private:
void BindImage(ImageId id, bool storage);
void BindRenderTarget(ImageId id);
void ResetBindings();
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
RenderContext& m_context;
GraphicsBindings m_graphics_bindings;
@@ -51,9 +51,9 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
return true;
}
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = input.stage.module->info;
const auto& resources = *input.stage.resources;
if (resources.buffers.size() != program.info.buffers.size()) {
EXIT("compute runtime buffer count does not match shader metadata\n");
@@ -62,9 +62,10 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
const auto& resource = program.info.buffers[i];
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
// Reads and transforms of metadata do not establish a clear state.
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
return;
return false;
}
}
@@ -74,10 +75,13 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
if (resource.written) {
const auto descriptor =
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
(void)cache.ClearMeta(descriptor.Base48());
if (cache.ClearMeta(descriptor.Base48())) {
return true;
}
}
}
}
return false;
}
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
@@ -128,10 +132,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
return true;
}
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
const auto& program = *input.stage.program;
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
const auto& program = input.stage.module->info;
const auto& resources = *input.stage.resources;
const auto& fill = resources.uniform_fill;
auto& cache = command.GetContext().GetTextureCache();
@@ -186,21 +190,33 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
size)) {
return false;
}
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
return false;
const bool image_clear =
cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear);
if (!image_clear) {
// CPU fills invalidate overlapping cached images. Preserve the native dispatch for
// partial or ambiguous image backings, including pooled metadata aliases.
if ((descriptor.Base48() & 3u) != 0 ||
!m_context.IsMapped(descriptor.Base48(), size) ||
cache.IsRegionRegistered(descriptor.Base48(), size)) {
return false;
}
// Metadata need not have an image backing yet. Keep proven fills in guest memory
// when CPU-owned, so color-clear discovery can read them without a GPU download.
m_context.GetBufferCache().FillBuffer(descriptor.Base48(), size, packed_clear, false);
}
static std::atomic<uint32_t> logged_clears {0};
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
LOGF("GraphicsRenderDispatchDirect: compute image clear shader=0x%016" PRIx64
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
image_clear ? "image clear" : "buffer fill", input.stage.module->info.shader_hash,
descriptor.Base48(), size, packed_clear);
}
return true;
}
static void BindSharedMemory(RenderContext& context, ShaderComputeInputInfo& input,
PreparedBindings& bindings, uint64_t indirect_args = 0) {
if (input.stage.program->bindings.descriptor_counts[
if (input.stage.module->info.bindings.descriptor_counts[
static_cast<size_t>(ShaderRecompiler::IR::DescriptorBindingKind::SharedMemory)] == 0) {
return;
}
@@ -232,7 +248,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
uint32_t thread_group_z, uint32_t mode, bool async_compute) {
EXIT_IF(buffer.IsInvalid());
m_context.GetCommandScheduler().PopPendingOperations();
auto& ctx = buffer.GetRegisters();
auto& sh_ctx = buffer.GetShaders();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::DispatchDirect), submit_id,
@@ -277,7 +292,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
const auto& cs_regs = sh_ctx.GetCs();
const auto& sh_regs = ctx.GetShaderRegisters();
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
@@ -295,24 +309,22 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
input_info.workgroup_counts[axis] = threads / size + (threads % size != 0u);
}
}
const auto compute_program =
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
m_context.GetImageHeap().BeginDraw();
if (!m_context.GetPipelineCache().GetComputeProgram(cs_regs, input_info)) return;
if (use_thread_dimensions) {
input_info.dispatch_threads_num[0] = thread_group_x;
input_info.dispatch_threads_num[1] = thread_group_y;
input_info.dispatch_threads_num[2] = thread_group_z;
}
const auto& program = *input_info.stage.program;
const auto& program = input_info.stage.module->info;
const auto& resources = *input_info.stage.resources;
if (resources.specialization_reads.empty()) {
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
UpdateComputeMetaClears(input_info, buffer);
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode)) {
ResetBindings();
return;
}
if (resources.specialization_reads.empty() &&
(TryConsumeComputeMetaClear(input_info, buffer) ||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode))) {
ResetBindings();
return;
}
if (use_thread_dimensions) {
@@ -335,8 +347,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
buffer.EndRendering();
auto& pipeline =
m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info);
auto& bindings = m_compute_bindings;
PrepareBindings(input_info.stage, bindings);
if (program.bindings.dispatch_thread_dword != ShaderRecompiler::IR::PushData::NoStart) {
@@ -348,6 +359,9 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
if (program.info.uses_dma) {
m_context.PrepareBda();
}
if (program.info.uses_image_heap) {
m_context.GetImageHeap().Rebind(*this);
}
RebindImages(bindings);
BindSharedMemory(m_context, input_info, bindings);
RebindBuffers(bindings);
@@ -432,7 +446,27 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
constexpr uint32_t DISPATCH_INITIATOR_ORDERED_APPEND_ENABLE = 1u << 3u;
const uint64_t group_threads = uint64_t {std::max(input_info.threads_num[0], 1u)} *
std::max(input_info.threads_num[1], 1u) *
std::max(input_info.threads_num[2], 1u);
if ((mode & DISPATCH_INITIATOR_ORDERED_APPEND_ENABLE) != 0 && program.info.uses_gds &&
group_threads <= input_info.wave_size && !input_info.lds_storage) {
// A single guest wave per group can run in launch order without a device-wide
// spinlock. Keep the guest workgroup IDs when splitting the dispatch.
for (uint32_t z = 0; z < thread_group_z; ++z) {
for (uint32_t y = 0; y < thread_group_y; ++y) {
for (uint32_t x = 0; x < thread_group_x; ++x) {
if ((x | y | z) != 0u) {
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
}
vk_buffer.dispatchBase(x, y, z, 1, 1, 1);
}
}
}
} else {
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
// The removed host fence also ordered read-only dispatches before later writers.
@@ -462,19 +496,22 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
input_info.indirect_args_address = args_addr;
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
m_context.GetImageHeap().BeginDraw();
if (!m_context.GetPipelineCache().GetComputeProgram(cs_regs, input_info)) return;
buffer.EndRendering();
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info);
auto& bindings = m_compute_bindings;
PrepareBindings(input_info.stage, bindings);
PreparedBindings* descriptor_stage = &bindings;
FindBuffers(std::span {&descriptor_stage, 1u});
const auto& program = *input_info.stage.program;
const auto& program = input_info.stage.module->info;
if (program.info.uses_dma) {
m_context.PrepareBda();
}
BindSharedMemory(m_context, input_info, bindings, args_addr);
if (program.info.uses_image_heap) {
m_context.GetImageHeap().Rebind(*this);
}
RebindImages(bindings);
// Acquiring arguments can merge cache buffers; finalize shader bindings afterward.
const auto [args_buffer, args_offset] = m_context.GetBufferCache().ObtainBuffer(
@@ -10,12 +10,13 @@
namespace Libs::Graphics {
RenderContext::RenderContext(GraphicContext& graphics)
RenderContext::RenderContext(GraphicContext& graphics, const std::filesystem::path& shader_store)
: m_graphics(graphics), m_render_executor(*this), m_command_scheduler(*this, graphics),
m_descriptor_heap(graphics, m_command_scheduler.GetMasterSemaphore()),
m_pipeline_cache(graphics), m_sampler_cache(graphics),
m_pipeline_cache(graphics, &m_image_heap, shader_store), m_sampler_cache(graphics),
m_buffer_cache(graphics, m_command_scheduler, m_page_manager, m_texture_cache),
m_texture_cache(graphics, m_command_scheduler, m_page_manager, m_buffer_cache) {
m_texture_cache(graphics, m_command_scheduler, m_page_manager, m_buffer_cache),
m_image_heap(*this, m_page_manager), m_occlusion_queries(graphics, m_command_scheduler) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
}
@@ -64,6 +65,7 @@ bool RenderContext::HandleFault(PageFaultAccess access, uint64_t fault_vaddr) no
if (access == PageFaultAccess::Write) {
m_buffer_cache.InvalidateMemory(fault_vaddr, fault_size);
m_texture_cache.InvalidateMemory(fault_vaddr, fault_size);
m_image_heap.InvalidateMemory(fault_vaddr, fault_size);
} else {
m_buffer_cache.ReadMemory(fault_vaddr, fault_size);
}
@@ -76,6 +78,7 @@ bool RenderContext::InvalidateMemory(uint64_t vaddr, uint64_t size) {
}
m_buffer_cache.InvalidateMemory(vaddr, size);
m_texture_cache.InvalidateMemory(vaddr, size);
m_image_heap.InvalidateMemory(vaddr, size);
return true;
}
@@ -112,6 +115,7 @@ void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
}
m_buffer_cache.InvalidateMemory(vaddr, size);
m_texture_cache.UnmapMemory(vaddr, size);
m_image_heap.UnmapMemory(vaddr, size);
std::lock_guard lock(m_mapped_ranges_mutex);
m_mapped_ranges.Subtract(vaddr, size);
};
@@ -130,7 +134,11 @@ void RenderContext::PrepareBda() {
m_bda_logged = true;
}
std::shared_lock lock(m_mapped_ranges_mutex);
m_buffer_cache.SynchronizeBda(m_mapped_ranges);
m_buffer_cache.TakeBdaSync().ForEach([this](uint64_t begin, uint64_t end) {
m_mapped_ranges.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
m_buffer_cache.SynchronizeBuffersInRange(start, finish - start);
});
});
m_fault_process_pending = true;
}
@@ -139,6 +147,7 @@ void RenderContext::RunGarbageCollector() {
m_fault_process_pending = false;
m_buffer_cache.ProcessFaultBuffer();
}
m_image_heap.RunGarbageCollector();
m_texture_cache.ProcessDownloadImages();
m_texture_cache.RunGarbageCollector();
m_buffer_cache.RunGarbageCollector();
@@ -11,7 +11,9 @@
#include "graphics/host_gpu/renderer/cache/samplerCache.h"
#include "graphics/host_gpu/renderer/cache/textureCache.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include "graphics/host_gpu/renderer/occlusionQueries.h"
#include "graphics/host_gpu/renderer/pipeline/descriptorHeap.h"
#include "graphics/host_gpu/renderer/pipeline/imageHeap.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "kernel/eventQueue.h"
@@ -29,7 +31,8 @@ class GuestGpu;
class RenderContext {
public:
explicit RenderContext(GraphicContext& graphics);
explicit RenderContext(GraphicContext& graphics,
const std::filesystem::path& shader_store = ShaderStorePath());
~RenderContext();
KYTY_CLASS_NO_COPY(RenderContext);
@@ -43,10 +46,12 @@ public:
CommandScheduler& GetCommandScheduler() { return m_command_scheduler; }
PipelineCache& GetPipelineCache() { return m_pipeline_cache; }
DescriptorHeap& GetDescriptorHeap() { return m_descriptor_heap; }
ImageHeap& GetImageHeap() { return m_image_heap; }
SamplerCache& GetSamplerCache() { return m_sampler_cache; }
BufferCache& GetBufferCache() { return m_buffer_cache; }
TextureCache& GetTextureCache() { return m_texture_cache; }
RenderExecutor& GetRenderExecutor() { return m_render_executor; }
OcclusionQueries& GetOcclusionQueries() { return m_occlusion_queries; }
[[nodiscard]] bool HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noexcept;
[[nodiscard]] bool InvalidateMemory(uint64_t vaddr, uint64_t size);
@@ -76,6 +81,8 @@ private:
PageManager m_page_manager;
BufferCache m_buffer_cache;
TextureCache m_texture_cache;
ImageHeap m_image_heap;
OcclusionQueries m_occlusion_queries;
mutable std::shared_mutex m_mapped_ranges_mutex;
RangeSet m_mapped_ranges;
std::unique_ptr<GuestGpu> m_gpu;
+184 -182
View File
@@ -17,7 +17,6 @@
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
#include "graphics/host_gpu/renderer/debug.h"
#include "graphics/host_gpu/renderer/depthRenderTarget.h"
#include "graphics/host_gpu/renderer/image/textureCommon.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
#include "graphics/host_gpu/renderer/render.h"
@@ -58,7 +57,7 @@ std::pair<int32_t, uint32_t> ResolveDrawOffsets(uint32_t index_offset,
}
EXIT_IF(!vs_input_info.stage);
const auto& program = *vs_input_info.stage.program;
const auto& program = vs_input_info.stage.module->info;
const auto& resources = *vs_input_info.stage.resources;
if (index_offset == 0 &&
program.info.vertex_offset_sgpr >= static_cast<int32_t>(program.user_data_base)) {
@@ -198,7 +197,7 @@ static void LogDrawTargetState(const char* draw_name, const RenderColorInfo& col
const auto& dc = ctx.GetDepthControl();
const auto& vp = ctx.GetScreenViewport();
const auto& vp0 = vp.viewports[0];
const auto& ps_resources = ps_input_info.stage.program->info;
const auto& ps_resources = ps_input_info.stage.module->info.info;
const auto sampled_images = std::count_if(
ps_resources.images.begin(), ps_resources.images.end(), [](const auto& image) {
return image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled;
@@ -321,7 +320,7 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
const auto& ctx = buffer.GetRegisters();
const auto& vp = ctx.GetScreenViewport();
const vk::Extent2D framebuffer_extent {rendering.width, rendering.height};
const auto& outputs = vs_input_info.stage.program->info.outputs;
const auto& outputs = vs_input_info.stage.module->info.info.outputs;
const bool indexed_viewports =
std::any_of(outputs.begin(), outputs.end(), [](const auto& output) {
return output.kind == ShaderRecompiler::IR::StageOutputKind::ViewportIndex;
@@ -404,6 +403,20 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
}
// Rect lists are drawn without culling.
vk::CullModeFlags cull_mode {};
if (!Prospero::IsRectList(buffer.GetUserConfig().GetPrimType())) {
if (mode.cull_front) {
cull_mode |= vk::CullModeFlagBits::eFront;
}
if (mode.cull_back) {
cull_mode |= vk::CullModeFlagBits::eBack;
}
}
dynamic_state.SetCullMode(cull_mode);
dynamic_state.SetFrontFace(mode.face ? vk::FrontFace::eClockwise
: vk::FrontFace::eCounterClockwise);
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
if (depth.stencil_test_enable) {
const auto& front = depth.stencil_front;
@@ -433,22 +446,6 @@ static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
return vs.es_regs.data_addr != 0;
}
static bool PixelShaderHasDepthOrCoverageSideEffects(const HW::ShaderRegisters& sh_regs) {
const auto& db = sh_regs.db_shader_control;
return db.shader_kill_enable || db.shader_z_export_enable || db.shader_mask_export_enable ||
db.shader_dual_export_enable || db.shader_execute_on_noop;
}
struct DrawRenderState {
RenderDepthInfo depth_info;
RenderColorInfo color_info[RENDER_COLOR_ATTACHMENTS_MAX] = {};
uint32_t color_count = 0;
bool ps_active = true;
std::array<ShaderVertexInputInfo, 3> vertex_info;
ShaderPixelInputInfo ps_input_info;
PipelineCache::GraphicsPrograms programs;
};
struct DrawCallInfo {
CommandBufferDebugOp debug_op = CommandBufferDebugOp::DrawIndex;
uint32_t index_count = 0;
@@ -474,9 +471,8 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
for (uint32_t i = 0; i < color_count; i++) {
auto& target = colors[i];
EXIT_IF(!target.image_id);
const auto owner = cache.m_slot_images.try_get(target.image_id);
if (owner == nullptr || (!owner->registered && !owner->info.data.Empty()) ||
owner->binding.needs_rebind) {
const auto& owner = cache.m_slot_images[target.image_id];
if ((!owner.registered && !owner.info.data.Empty()) || owner.binding.needs_rebind) {
EXIT("color target changed after render-state discovery\n");
}
const auto image_view = cache.FindRenderTarget(target.image_id, target.desc);
@@ -502,8 +498,8 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
attachment.image_layout = layout;
}
if (depth.image_id) {
const auto owner = cache.m_slot_images.try_get(depth.image_id);
if (owner == nullptr || !owner->registered || owner->binding.needs_rebind) {
const auto& owner = cache.m_slot_images[depth.image_id];
if (!owner.registered || owner.binding.needs_rebind) {
EXIT("depth target changed after render-state discovery\n");
}
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
@@ -589,23 +585,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
return state;
}
static uint32_t DrawColorOutputMask(const HW::Context& ctx) {
const auto& sh_regs = ctx.GetShaderRegisters();
const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask;
uint32_t output_mask = ctx.HasObjectPrimitiveIdOutput() ? 1u << 7u : 0;
for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) {
const auto slot = ShaderPixelExportTarget(sh_regs.m_cbShaderMask, index);
if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) {
break;
}
if (sh_regs.target_output_mode[index] != 0 &&
render_target_mask_slot(write_mask, slot) != 0) {
output_mask |= 1u << slot;
}
}
return output_mask;
}
enum class CbColorMode : uint8_t {
Disable = 0,
Normal = 1,
@@ -631,10 +610,11 @@ bool RenderExecutor::ConsumeMetadataColorOperation(CommandBuffer& buffer,
mode == static_cast<uint8_t>(CbColorMode::DccDecompress);
}
// vertex_offset is the first vertex of non-indexed draws.
struct DrawEmitInfo {
int32_t vertex_offset = 0;
uint32_t first_vertex = 0;
uint32_t first_instance = 0;
int32_t vertex_offset = 0;
uint32_t first_instance = 0;
DrawIndirectArgs indirect;
};
struct DrawIndexBufferSource {
@@ -869,61 +849,24 @@ static bool ResolvePrimitiveRestart(const CommandBuffer& buffer,
return false;
}
static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
uint32_t color_output_mask, DrawRenderState& state) {
auto& ctx = buffer.GetRegisters();
auto& sh_ctx = buffer.GetShaders();
const auto& vertex_shader_info = sh_ctx.GetVs();
const auto& pixel_shader_info = sh_ctx.GetPs();
const auto& shader_regs = ctx.GetShaderRegisters();
state.programs = {};
state.ps_input_info = {};
std::array<Prospero::ColorComponentMapping, RENDER_COLOR_ATTACHMENTS_MAX>
target_export_mapping {};
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
const auto& rt = ctx.GetRenderTarget(slot);
if ((color_output_mask & (1u << slot)) != 0 && rt.base.addr != 0) {
target_export_mapping[slot] =
TextureGetRenderTargetFormat(rt.info.format, rt.info.channel_type,
rt.info.channel_order)
.export_mapping;
}
}
auto& pipeline_cache = buffer.GetContext().GetPipelineCache();
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
}
state.programs = pipeline_cache.GetGraphicsPrograms(
vertex_shader_info, pixel_shader_info, shader_regs, ctx, buffer.GetUserConfig(),
target_export_mapping, state.ps_active, state.vertex_info, state.ps_input_info);
}
bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCallInfo& draw,
uint32_t render_target_slice_offset,
DrawRenderState& state) {
const auto& shader_regs = buffer.GetRegisters().GetShaderRegisters();
const auto color_output_mask = DrawColorOutputMask(buffer.GetRegisters());
state.ps_active = buffer.GetShaders().GetPs().ps_regs.data_addr != 0 &&
(color_output_mask != 0 ||
PixelShaderHasDepthOrCoverageSideEffects(shader_regs));
RefreshShaders(buffer, draw, color_output_mask, state);
uint32_t mrt_mask = 0;
if (state.ps_active) {
for (const auto& output: state.ps_input_info.stage.program->info.outputs) {
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt ||
output.kind == ShaderRecompiler::IR::StageOutputKind::ObjectPrimitiveId) {
mrt_mask |= 1u << output.location;
}
}
m_context.GetImageHeap().BeginDraw();
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
}
m_context.GetPipelineCache().GetGraphicsPrograms(buffer, state);
// Vertex programs beyond the host mesh limits or with unsupported call targets are not
// compiled.
if (state.vertex_info[0].stage.module == nullptr) {
return false;
}
mrt_mask &= color_output_mask;
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
}
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
if ((mrt_mask & (1u << slot)) != 0) {
if ((state.mrt_mask & (1u << slot)) != 0) {
ResolveRenderColorTarget(buffer, state.color_info[state.color_count],
render_target_slice_offset, slot);
if (state.color_info[state.color_count].image_id) {
@@ -935,6 +878,29 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
LogDrawPhase(draw.Name(), "ResolveRenderDepthTarget");
}
ResolveRenderDepthTarget(buffer, state.depth_info);
// TODO(PPSA09243): Vulkan passes take one sample count. Where PS5 attachments mix counts,
// keep those matching the raster count instead of resolving multisampled shadows.
const auto colors = std::span {state.color_info, state.color_count};
const auto pass_samples = state.depth_info.image_id ? state.depth_info.desc.info.samples
: state.color_info[0].desc.info.samples;
if (std::ranges::any_of(colors, [&](const RenderColorInfo& color) {
return color.desc.info.samples != pass_samples;
})) {
const auto raster =
render_sample_count(buffer.GetRegisters().GetAaConfig().msaa_num_samples);
const auto dropped = std::ranges::remove_if(colors, [&](const RenderColorInfo& color) {
return color.desc.info.samples != raster;
});
state.color_count -= static_cast<uint32_t>(dropped.size());
if (state.depth_info.image_id && state.depth_info.desc.info.samples != raster) {
state.depth_info = {};
}
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: dropping attachments whose sample count differs from the raster.\n");
}
}
if (state.color_count == 0 && !state.depth_info.image_id && !state.ps_active) {
LogFramebufferSkip(draw.Name(), state.color_info[0], state.depth_info, buffer,
@@ -1020,8 +986,8 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset,
emit.first_instance);
} else {
vk_buffer.draw(draw.index_count, draw.instance_count, emit.first_vertex,
emit.first_instance);
vk_buffer.draw(draw.index_count, draw.instance_count,
static_cast<uint32_t>(emit.vertex_offset), emit.first_instance);
}
break;
case Prospero::PrimitiveType::kQuadListLegacy:
@@ -1031,7 +997,8 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
vk_buffer.drawIndexed(4, draw.instance_count, i, emit.vertex_offset,
emit.first_instance);
} else {
vk_buffer.draw(4, draw.instance_count, i + emit.first_vertex,
vk_buffer.draw(4, draw.instance_count,
i + static_cast<uint32_t>(emit.vertex_offset),
emit.first_instance);
}
}
@@ -1052,7 +1019,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().maintenance8_enabled ||
!Prospero::IsRectList(ucfg.GetPrimType()) ||
draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 ||
emit.first_vertex != 0 || emit.first_instance != 0 ||
emit.vertex_offset != 0 || emit.first_instance != 0 ||
state.color_count != 1 || color.target_slot != 0 || !depth.image_id ||
color.desc.info.samples != 1 ||
depth.desc.info.extent.width != color.Extent().width ||
@@ -1098,16 +1065,15 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer,
const DrawCallInfo& draw, DrawRenderState& state,
vk::PrimitiveTopology topology, const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source,
bool primitive_restart_enable) {
auto& ucfg = buffer.GetUserConfig();
const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source) {
auto& ucfg = buffer.GetUserConfig();
const bool primitive_restart_enable = state.primitive_restart_enable;
if (CopyDepthToColor(buffer, draw, emit, state)) {
return;
}
const auto vertex_stages =
std::span {state.vertex_info.data(), state.programs.VertexStageCount()};
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
const auto vertex_stages = state.VertexStages();
const bool mesh_active = state.vertex_info[0].stage.module->info.stage == ShaderType::Mesh;
uint32_t mesh_groups = 0;
if (mesh_active) {
const auto& mesh = state.vertex_info[0].mesh;
@@ -1166,13 +1132,18 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
vertex_bindings = AcquireVertexBuffers(buffer, state.vertex_info[0]);
index_binding = PrepareIndexBuffer(buffer, index_source);
}
std::pair<Buffer*, uint64_t> args_buffer {};
if (emit.indirect.address != 0) {
const auto size = static_cast<uint64_t>(emit.indirect.count - 1) * emit.indirect.stride +
(draw.IsIndexed() ? sizeof(vk::DrawIndexedIndirectCommand)
: sizeof(vk::DrawIndirectCommand));
args_buffer = m_context.GetBufferCache().ObtainBuffer(emit.indirect.address, size, false);
EXIT_IF((args_buffer.second & 3u) != 0);
}
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "CreatePipeline");
}
auto& pipeline = m_context.GetPipelineCache().GetGraphicsPipeline(
std::span {state.color_info, state.color_count}, state.depth_info, vertex_stages, buffer,
state.ps_active ? &state.ps_input_info : nullptr, topology, primitive_restart_enable,
state.programs);
auto& pipeline = m_context.GetPipelineCache().GetGraphicsPipeline(buffer, state);
vk::ImageAspectFlags feedback_aspects;
const auto rendering =
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info,
@@ -1193,26 +1164,32 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!mesh_active) {
CommitIndexBuffer(vk_buffer, index_binding);
}
const auto draw_dwords = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, state.ps_input_info.object_primitive_id);
using ShaderRecompiler::IR::PushData;
const auto draw_stages =
vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT : vk::ShaderStageFlagBits::eVertex);
const auto draw_dwords =
PushData::GraphicsDrawDwords(mesh_active, state.ps_input_info.object_primitive_id);
if (draw_dwords != 0) {
std::array<uint32_t, ShaderRecompiler::IR::PushData::MeshDrawDwordCount> draw_data {};
const auto draw_stages = vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT
: vk::ShaderStageFlagBits::eVertex);
std::array<uint32_t, PushData::MeshDrawDwordCount> draw_data {};
if (mesh_active) {
draw_data = {
draw.index_count,
draw.IsIndexed() ? static_cast<uint32_t>(emit.vertex_offset) : emit.first_vertex,
emit.first_instance, index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(index_source.address >> 32u)};
draw_data = {draw.index_count,
static_cast<uint32_t>(emit.vertex_offset),
index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(index_source.address >> 32u)};
} else {
draw_data[0] = buffer.GetUserConfig().GetObjectId();
}
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages, 0,
draw_dwords * sizeof(uint32_t), draw_data.data());
}
if (const auto planes = vertex_stages.back().user_clip_planes; planes != 0) {
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages,
PushData::UserClipPlaneDword * sizeof(uint32_t),
std::bit_width(planes) * 4u * sizeof(uint32_t),
buffer.GetRegisters().GetUserClipPlanes().data());
}
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
@@ -1220,6 +1197,20 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
dynamic_state.SetAttachmentFeedbackLoopAspects(feedback_aspects);
dynamic_state.Commit(m_context.GetGraphics(), vk_buffer);
vk::PipelineStageFlags shader_write_stages = {};
for (const auto& stage: vertex_stages) {
if (HasShaderBufferWrites(stage.stage)) {
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
}
}
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
}
if (shader_write_stages) {
// As for dispatches, buffer writes wait for earlier accesses to the memory.
m_context.GetCommandScheduler().EndRendering();
ShaderWriteHazardBarrier(vk_buffer, shader_write_stages);
}
LogDrawPhase(draw.Name(), "BeginRendering");
if (!draw.IsIndexed()) {
SetDrawDebugPhase(buffer, submit_id, draw, 0x400u);
@@ -1229,26 +1220,26 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!draw.IsIndexed()) {
SetDrawDebugPhase(buffer, submit_id, draw, 0x500u);
}
auto& occlusion_queries = m_context.GetOcclusionQueries();
const int32_t occlusion_query = occlusion_queries.Begin(vk_buffer);
if (mesh_active) {
vk_buffer.drawMeshTasksEXT(mesh_groups, draw.instance_count, 1);
} else {
} else if (args_buffer.first == nullptr) {
EmitDrawPrimitives(ucfg, vk_buffer, draw, emit);
} else if (draw.IsIndexed()) {
vk_buffer.drawIndexedIndirect(args_buffer.first->Handle(), args_buffer.second,
emit.indirect.count, emit.indirect.stride);
} else {
vk_buffer.drawIndirect(args_buffer.first->Handle(), args_buffer.second,
emit.indirect.count, emit.indirect.stride);
}
occlusion_queries.End(vk_buffer, occlusion_query);
Common::DebugCounters::Add(Common::DebugCounters::Counter::Draws);
Common::DebugCounters::Add(Common::DebugCounters::Counter::DrawInstances, draw.instance_count);
if (!draw.IsIndexed()) {
SetDrawDebugPhase(buffer, submit_id, draw, 0x600u);
}
vk::PipelineStageFlags shader_write_stages = {};
for (const auto& stage: vertex_stages) {
if (HasShaderBufferWrites(stage.stage)) {
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
}
}
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
}
if (shader_write_stages) {
m_context.GetCommandScheduler().EndRendering();
ShaderWriteBarrier(vk_buffer, shader_write_stages);
@@ -1259,7 +1250,32 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
}
}
void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh
// and quad-list draws for counts, tessellation draws for patched SGPRs. Indirect records carry
// their own offsets.
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
const ShaderVertexInputInfo& vs,
DrawOffsetSource source,
int32_t first_vertex,
uint32_t first_instance,
const DrawIndirectArgs& indirect) {
if (indirect.address != 0) {
if (vs.stage.module->info.stage == ShaderType::Mesh ||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy ||
vs.indirect_offsets != indirect.offsets) {
return std::nullopt;
}
return DrawEmitInfo {.indirect = indirect};
}
if (source == DrawOffsetSource::IndirectArgs) {
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
return vs.fetch_embedded ? DrawEmitInfo {first_vertex, first_instance} : DrawEmitInfo {};
}
const auto [vertex_offset, instance_offset] = ResolveDrawOffsets(ucfg.GetIndexOffset(), vs);
return DrawEmitInfo {vertex_offset + first_vertex, instance_offset};
}
bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
const DrawIndexArgs& args) {
KYTY_PROFILER_FUNCTION();
@@ -1274,19 +1290,19 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
reinterpret_cast<uint64_t>(args.index_addr));
Common::LockGuard lock(m_context.GetMutex());
if (args.index_count == 0 || args.instance_count == 0) {
return;
if (args.index_count == 0 || (args.instance_count == 0 && args.indirect.address == 0)) {
return true;
}
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
DepthStencilCopy(buffer) ||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
ResetBindings();
return;
return true;
}
if (!DrawHasValidVertexShader(sh_ctx)) {
return;
return true;
}
if (graphics_debug_dump_enabled()) {
@@ -1310,9 +1326,9 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer);
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, topology)) {
return;
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) {
return true;
}
DrawIndexBufferSource index_source {};
@@ -1332,7 +1348,7 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
default: EXIT("unknown index_type_and_size: %u\n", args.index_type_and_size);
}
index_source.size = static_cast<uint64_t>(args.index_count) * index_source.guest_element_size;
const bool primitive_restart = ResolvePrimitiveRestart(buffer, index_source);
state.primitive_restart_enable = ResolvePrimitiveRestart(buffer, index_source);
std::vector<uint16_t> expanded_indices;
if (index_source.guest_element_size == 1) {
@@ -1340,7 +1356,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
const auto* src = static_cast<const uint8_t*>(args.index_addr);
expanded_indices.resize(args.index_count);
for (uint32_t i = 0; i < args.index_count; i++) {
expanded_indices[i] = primitive_restart && src[i] == 0xffu ? 0xffffu : src[i];
expanded_indices[i] =
state.primitive_restart_enable && src[i] == 0xffu ? 0xffffu : src[i];
}
index_source.host_data = expanded_indices.data();
index_source.size = expanded_indices.size() * sizeof(uint16_t);
@@ -1348,33 +1365,24 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndex, args.index_count,
args.instance_count, args.first_instance};
DrawRenderState state {};
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
ResetBindings();
return;
return true;
}
LogDrawStateIfNeeded(buffer, draw, state, args.index_type_and_size,
args.index_addr);
const bool indirect = args.offset_source == DrawOffsetSource::IndirectArgs;
const auto [vertex_offset, instance_offset] =
indirect ? std::pair<int32_t, uint32_t> {0, args.first_instance}
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded;
emit.vertex_offset = native_indirect ? 0 : vertex_offset + args.base_vertex;
emit.first_instance = native_indirect ? 0 : instance_offset;
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source,
primitive_restart);
const auto emit = ResolveDrawEmit(ucfg, state.vertex_info[0], args.offset_source,
args.base_vertex, args.first_instance, args.indirect);
if (emit) {
ExecutePreparedDraw(submit_id, buffer, draw, state, *emit, index_source);
}
ResetBindings();
return emit.has_value();
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args) {
bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
@@ -1388,19 +1396,19 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
args.first_instance);
Common::LockGuard lock(m_context.GetMutex());
if (args.vertex_count == 0 || args.instance_count == 0) {
return;
if (args.indirect.address == 0 && (args.vertex_count == 0 || args.instance_count == 0)) {
return true;
}
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
DepthStencilCopy(buffer) ||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
ResetBindings();
return;
return true;
}
if (!DrawHasValidVertexShader(sh_ctx)) {
return;
return true;
}
if (graphics_debug_dump_enabled()) {
@@ -1423,20 +1431,19 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
args.vertex_count, args.instance_count, args.first_instance};
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, topology)) {
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) {
ResetBindings();
return;
return true;
}
DrawRenderState state {};
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
ResetBindings();
return;
return true;
}
const bool rect_list = Prospero::IsRectList(ucfg.GetPrimType());
if (rect_list && state.vertex_info[0].buffers_num == 0 &&
state.vertex_info[0].stage.program->param_export_mask == 0 &&
state.vertex_info[0].stage.module->info.param_export_mask == 0 &&
state.ps_input_info.input_num != 0) {
if (graphics_debug_dump_enabled()) {
LOGF("DrawIndexAuto: skipping rect-list draw with no VS param exports and PS inputs: "
@@ -1445,24 +1452,18 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
sh_ctx.GetVs().es_regs.data_addr, sh_ctx.GetVs().gs_regs.data_addr);
}
ResetBindings();
return;
return true;
}
LogDrawStateIfNeeded(buffer, draw, state, 0, nullptr);
const bool indirect = args.offset_source == DrawOffsetSource::IndirectArgs;
const auto [vertex_offset, instance_offset] =
indirect ? std::pair<int32_t, uint32_t> {0, args.first_instance}
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded;
emit.first_vertex = native_indirect ? 0 :
static_cast<uint32_t>(vertex_offset + static_cast<int32_t>(args.first_vertex));
emit.first_instance = native_indirect ? 0 : instance_offset;
DrawIndexBufferSource index_source {};
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false);
const auto emit = ResolveDrawEmit(ucfg, state.vertex_info[0], args.offset_source,
static_cast<int32_t>(args.first_vertex), args.first_instance,
args.indirect);
if (emit) {
ExecutePreparedDraw(submit_id, buffer, draw, state, *emit, {});
}
ResetBindings();
return emit.has_value();
}
bool RenderExecutor::ResolveColorTargets(CommandBuffer& buffer, uint32_t render_target_slice_offset) {
@@ -1494,8 +1495,9 @@ bool RenderExecutor::ResolveColorTargets(CommandBuffer& buffer, uint32_t render_
cache.MarkGpuWritten(dst.image_id);
auto& source = cache.GetImage(src.image_id);
auto& destination = cache.GetImage(dst.image_id);
destination.Resolve(source, {src.guest_mip_level, 1, src.guest_array_layer, 1},
{dst.guest_mip_level, 1, dst.guest_array_layer, 1});
destination.Resolve(source,
{src.desc.view_info.base_level, 1, src.desc.view_info.base_layer, 1},
{dst.desc.view_info.base_level, 1, dst.desc.view_info.base_layer, 1});
return true;
}
@@ -0,0 +1,50 @@
#version 450
// One workgroup per metadata slice; see TileManager::CheckColorClear.
layout(local_size_x = 256) in;
layout(set = 0, binding = 0, std430) buffer Metadata { uint data[]; } metadata;
layout(set = 0, binding = 1, std430) writeonly buffer Predicates { uint data[]; } predicates;
layout(push_constant, std430) uniform Check {
uint metadata_base;
uint words;
uint keys;
uint expand;
} params;
const uint Slots = 8u; // TileManager::ColorClearSlots
shared uint key;
shared uint mismatch;
void main() {
const uint slice = gl_WorkGroupID.x;
const uint base = (params.metadata_base >> 2u) + slice * params.words;
// Each invocation reads, and may rewrite, only its own words.
if (gl_LocalInvocationIndex == 0u) {
key = metadata.data[base] & 0xffu;
mismatch = 0u;
}
barrier();
const uint pattern = key * 0x01010101u;
bool same = true;
for (uint i = gl_LocalInvocationIndex; same && i < params.words; i += gl_WorkGroupSize.x) {
same = metadata.data[base + i] == pattern;
}
if (!same) {
atomicOr(mismatch, 1u);
}
barrier();
const uint slot = key >> 5u;
const bool hit = mismatch == 0u && (key & 0x1fu) == 0u && (params.keys & (1u << slot)) != 0u;
if (gl_LocalInvocationIndex < Slots) {
predicates.data[slice * Slots + gl_LocalInvocationIndex] =
uint(hit && slot == gl_LocalInvocationIndex);
}
if (hit && params.expand != 0u) {
for (uint i = gl_LocalInvocationIndex; i < params.words; i += gl_WorkGroupSize.x) {
metadata.data[base + i] = 0xffffffffu;
}
}
}
@@ -1,10 +1,11 @@
#version 450
#extension GL_EXT_shader_16bit_storage : require
layout(constant_id = 0) const uint D32 = 0u;
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint16_t data[]; } output_buffer;
layout(push_constant, std430) uniform Conversion {
uint src_base;
@@ -32,9 +33,5 @@ void main() {
}
uint src = params.src_base + y * params.source_pitch + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
uint shift = (dst & 2u) * 8u;
uint mask = 0xffffu << shift;
uint word = dst >> 2u;
atomicAnd(output_buffer.data[word], ~mask);
atomicOr(output_buffer.data[word], decode(input_buffer.data[src >> 2u]) << shift);
output_buffer.data[dst >> 1u] = uint16_t(decode(input_buffer.data[src >> 2u]));
}
+762
View File
@@ -0,0 +1,762 @@
#include "graphics/presentation/openxrHeadset.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "common/threads.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/presentation/vrInput.h"
#include "libs/hmd2.h"
#define XR_USE_GRAPHICS_API_VULKAN
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fmt/format.h>
#include <mutex>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <string>
#include <thread>
namespace Libs::Vr {
namespace {
using namespace Libs::Graphics;
enum Action { TRIGGER, SQUEEZE, STICK, STICK_CLICK, PRIMARY, SECONDARY, MENU, GRIP, ACTION_COUNT };
// Touch controller bindings: the path under /user/hand/<side>/input/ per hand, or null.
constexpr struct {
const char* name;
XrActionType type;
const char* path[2];
} ACTIONS[ACTION_COUNT] = {
{"trigger", XR_ACTION_TYPE_FLOAT_INPUT, {"trigger/value", "trigger/value"}},
{"squeeze", XR_ACTION_TYPE_FLOAT_INPUT, {"squeeze/value", "squeeze/value"}},
{"thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT, {"thumbstick", "thumbstick"}},
{"thumbstick_click", XR_ACTION_TYPE_BOOLEAN_INPUT, {"thumbstick/click", "thumbstick/click"}},
{"primary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"x/click", "a/click"}},
{"secondary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"y/click", "b/click"}},
{"menu", XR_ACTION_TYPE_BOOLEAN_INPUT, {"menu/click", nullptr}},
{"grip_pose", XR_ACTION_TYPE_POSE_INPUT, {"grip/pose", "grip/pose"}},
};
constexpr const char* HANDS[2] = {"left", "right"};
// Eye images move from the render thread to the headset thread through these. One is being written,
// one is the latest recorded frame and one may be held by the headset thread until the GPU is done.
constexpr int STAGING_COUNT = 3;
struct Staging {
VulkanImage image;
uint64_t tick = 0;
VrPose head; // The head pose the game last read, so the picture can be placed in the world.
};
struct State {
XrInstance instance = XR_NULL_HANDLE;
XrSystemId system = XR_NULL_SYSTEM_ID;
XrSession session = XR_NULL_HANDLE;
XrSpace view_space = XR_NULL_HANDLE;
XrSpace local_space = XR_NULL_HANDLE;
XrSwapchain swapchain = XR_NULL_HANDLE; // Two array layers, one per eye.
std::vector<XrSwapchainImageVulkanKHR> swapchain_images;
vk::Extent2D eye {};
std::vector<std::string> extensions[2]; // Vulkan instance and device extensions.
PFN_xrGetVulkanGraphicsDeviceKHR get_graphics_device = nullptr;
GraphicContext* graphics = nullptr;
std::thread thread;
std::atomic_bool running {false};
// Headset thread only.
XrSessionState session_state = XR_SESSION_STATE_UNKNOWN;
bool session_running = false;
uint64_t shown_tick = 0;
VrPose shown_head;
vk::CommandPool pool;
vk::CommandBuffer command;
vk::Fence fence;
std::mutex mutex;
Staging staging[STAGING_COUNT];
int latest = -1;
int held = -1;
MasterSemaphore* semaphore = nullptr;
XrActionSet action_set = XR_NULL_HANDLE;
XrAction actions[ACTION_COUNT] {};
XrPath hand_path[2] {};
XrSpace grip_space[2] {};
std::mutex input_mutex;
VrControllerInput input[2];
VrPose head;
VrPose last_head_read;
};
State g;
bool Ok(XrResult result, const char* what) {
if (XR_SUCCEEDED(result)) {
return true;
}
char text[XR_MAX_RESULT_STRING_SIZE] = {};
xrResultToString(g.instance, result, text);
Log::WriteToConsoleAndLog(fmt::format("OpenXR: {} failed: {}\n", what, text));
return false;
}
// Destroying the instance also destroys the session, spaces, swapchain and actions.
void DestroyInstance(const char* reason) {
if (reason != nullptr) {
Log::WriteToConsoleAndLog(
fmt::format("OpenXR: {}; showing the eye image in the window only\n", reason));
}
if (g.instance != XR_NULL_HANDLE) {
xrDestroyInstance(g.instance);
}
g.instance = XR_NULL_HANDLE;
g.session = XR_NULL_HANDLE;
}
struct Access {
vk::PipelineStageFlags2 stage;
vk::AccessFlags2 access;
};
constexpr Access NO_ACCESS {vk::PipelineStageFlagBits2::eNone, vk::AccessFlagBits2::eNone};
constexpr Access TRANSFER {vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone};
constexpr Access TRANSFER_READ {vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead};
constexpr Access TRANSFER_WRITE {vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite};
constexpr Access COLOR_ATTACHMENT {vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite};
void Barrier(vk::CommandBuffer command, vk::Image image, vk::ImageLayout from, vk::ImageLayout to,
uint32_t layers, Access src, Access dst) {
vk::ImageMemoryBarrier2 barrier {};
barrier.srcStageMask = src.stage;
barrier.srcAccessMask = src.access;
barrier.dstStageMask = dst.stage;
barrier.dstAccessMask = dst.access;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, layers};
vk::DependencyInfo dependency {};
dependency.imageMemoryBarrierCount = 1;
dependency.pImageMemoryBarriers = &barrier;
command.pipelineBarrier2(dependency);
}
XrQuaternionf Multiply(const XrQuaternionf& a, const XrQuaternionf& b) {
return {a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z};
}
XrVector3f Rotate(const XrQuaternionf& q, const XrVector3f& v) {
const XrVector3f t {2.0f * (q.y * v.z - q.z * v.y), 2.0f * (q.z * v.x - q.x * v.z),
2.0f * (q.x * v.y - q.y * v.x)};
return {v.x + q.w * t.x + (q.y * t.z - q.z * t.y), v.y + q.w * t.y + (q.z * t.x - q.x * t.z),
v.z + q.w * t.z + (q.x * t.y - q.y * t.x)};
}
VrPose Locate(XrSpace space, XrTime time) {
VrPose pose;
XrSpaceLocation location {XR_TYPE_SPACE_LOCATION};
if (XR_SUCCEEDED(xrLocateSpace(space, g.local_space, time, &location)) &&
(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0) {
const auto& [q, p] = location.pose;
pose = {true, {q.x, q.y, q.z, q.w}, {p.x, p.y, p.z}};
}
return pose;
}
// The swapchain stores the eyes in a gamma-encoded format, so a raw copy of the display-encoded
// game image comes out right. Staging uses the same layout with UNORM, which a blit writes from the
// raw UNORM or float presentation frame without conversion.
bool CreateSwapchain() {
uint32_t count = 0;
xrEnumerateSwapchainFormats(g.session, 0, &count, nullptr);
std::vector<int64_t> formats(count);
xrEnumerateSwapchainFormats(g.session, count, &count, formats.data());
constexpr std::pair<vk::Format, vk::Format> candidates[] = {
{vk::Format::eR8G8B8A8Srgb, vk::Format::eR8G8B8A8Unorm},
{vk::Format::eB8G8R8A8Srgb, vk::Format::eB8G8R8A8Unorm},
{vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm},
{vk::Format::eB8G8R8A8Unorm, vk::Format::eB8G8R8A8Unorm},
};
const auto* format =
std::find_if(std::begin(candidates), std::end(candidates), [&](const auto& c) {
return std::find(formats.begin(), formats.end(), static_cast<int64_t>(c.first)) !=
formats.end();
});
if (format == std::end(candidates)) {
return false;
}
XrSwapchainCreateInfo info {XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
info.format = static_cast<int64_t>(format->first);
info.sampleCount = 1;
info.width = g.eye.width;
info.height = g.eye.height;
info.faceCount = 1;
info.arraySize = 2;
info.mipCount = 1;
if (!Ok(xrCreateSwapchain(g.session, &info, &g.swapchain), "xrCreateSwapchain")) {
return false;
}
xrEnumerateSwapchainImages(g.swapchain, 0, &count, nullptr);
g.swapchain_images.assign(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
xrEnumerateSwapchainImages(
g.swapchain, count, &count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(g.swapchain_images.data()));
vk::ImageCreateInfo staging {};
staging.imageType = vk::ImageType::e2D;
staging.format = format->second;
staging.extent = vk::Extent3D {g.eye.width * 2, g.eye.height, 1};
staging.mipLevels = 1;
staging.arrayLayers = 1;
staging.samples = vk::SampleCountFlagBits::e1;
staging.tiling = vk::ImageTiling::eOptimal;
staging.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
for (auto& slot: g.staging) {
if (!g.graphics->CreateImage(staging, slot.image)) {
return false;
}
}
Log::WriteToConsoleAndLog(fmt::format("OpenXR: eye swapchain {}x{} {}\n", g.eye.width,
g.eye.height, vk::to_string(format->first)));
return true;
}
bool CreateInput() {
XrActionSetCreateInfo set {XR_TYPE_ACTION_SET_CREATE_INFO};
std::strcpy(set.actionSetName, "gameplay");
std::strcpy(set.localizedActionSetName, "Gameplay");
if (!Ok(xrCreateActionSet(g.instance, &set, &g.action_set), "xrCreateActionSet")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
xrStringToPath(g.instance, fmt::format("/user/hand/{}", HANDS[hand]).c_str(),
&g.hand_path[hand]);
}
std::vector<XrActionSuggestedBinding> bindings;
for (int i = 0; i < ACTION_COUNT; i++) {
XrActionCreateInfo info {XR_TYPE_ACTION_CREATE_INFO};
info.actionType = ACTIONS[i].type;
info.countSubactionPaths = 2;
info.subactionPaths = g.hand_path;
std::strcpy(info.actionName, ACTIONS[i].name);
std::strcpy(info.localizedActionName, ACTIONS[i].name);
if (!Ok(xrCreateAction(g.action_set, &info, &g.actions[i]), "xrCreateAction")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
if (ACTIONS[i].path[hand] != nullptr) {
XrPath path = XR_NULL_PATH;
xrStringToPath(
g.instance,
fmt::format("/user/hand/{}/input/{}", HANDS[hand], ACTIONS[i].path[hand])
.c_str(),
&path);
bindings.push_back({g.actions[i], path});
}
}
}
XrInteractionProfileSuggestedBinding suggested {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
xrStringToPath(g.instance, "/interaction_profiles/oculus/touch_controller",
&suggested.interactionProfile);
suggested.countSuggestedBindings = static_cast<uint32_t>(bindings.size());
suggested.suggestedBindings = bindings.data();
XrSessionActionSetsAttachInfo attach {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
attach.countActionSets = 1;
attach.actionSets = &g.action_set;
if (!Ok(xrSuggestInteractionProfileBindings(g.instance, &suggested), "suggest bindings") ||
!Ok(xrAttachSessionActionSets(g.session, &attach), "attach action sets")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
XrActionSpaceCreateInfo space {XR_TYPE_ACTION_SPACE_CREATE_INFO};
space.action = g.actions[GRIP];
space.subactionPath = g.hand_path[hand];
space.poseInActionSpace = {{0, 0, 0, 1}, {0, 0, 0}};
if (!Ok(xrCreateActionSpace(g.session, &space, &g.grip_space[hand]), "grip space")) {
return false;
}
}
return true;
}
// The head is tracked while the frame loop runs; the controllers only while the session has focus.
void PollInput(XrTime time) {
const auto head = Locate(g.view_space, time);
VrControllerInput input[2];
XrActiveActionSet active {g.action_set, XR_NULL_PATH};
XrActionsSyncInfo sync {XR_TYPE_ACTIONS_SYNC_INFO};
sync.countActiveActionSets = 1;
sync.activeActionSets = &active;
if (g.action_set != XR_NULL_HANDLE && g.session_state == XR_SESSION_STATE_FOCUSED &&
XR_SUCCEEDED(xrSyncActions(g.session, &sync))) {
for (int hand = 0; hand < 2; hand++) {
const auto get = [&](Action action, auto read, auto state) {
XrActionStateGetInfo info {XR_TYPE_ACTION_STATE_GET_INFO};
info.action = g.actions[action];
info.subactionPath = g.hand_path[hand];
read(g.session, &info, &state);
return state;
};
const auto value = [&](Action action) {
return get(action, xrGetActionStateFloat,
XrActionStateFloat {XR_TYPE_ACTION_STATE_FLOAT})
.currentState;
};
const auto flag = [&](Action action) {
return get(action, xrGetActionStateBoolean,
XrActionStateBoolean {XR_TYPE_ACTION_STATE_BOOLEAN})
.currentState == XR_TRUE;
};
const auto stick = get(STICK, xrGetActionStateVector2f,
XrActionStateVector2f {XR_TYPE_ACTION_STATE_VECTOR2F});
auto& out = input[hand];
out.trigger = value(TRIGGER);
out.squeeze = value(SQUEEZE);
out.stick_x = stick.currentState.x;
out.stick_y = stick.currentState.y;
out.stick_click = flag(STICK_CLICK);
out.primary = flag(PRIMARY);
out.secondary = flag(SECONDARY);
out.menu = flag(MENU);
out.pose = Locate(g.grip_space[hand], time);
out.connected =
get(GRIP, xrGetActionStatePose, XrActionStatePose {XR_TYPE_ACTION_STATE_POSE})
.isActive == XR_TRUE;
}
}
std::lock_guard lock(g.input_mutex);
g.head = head;
g.input[0] = input[0];
g.input[1] = input[1];
}
void HandleEvents() {
for (XrEventDataBuffer event {XR_TYPE_EVENT_DATA_BUFFER};
xrPollEvent(g.instance, &event) == XR_SUCCESS; event = {XR_TYPE_EVENT_DATA_BUFFER}) {
if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) {
g.running = false;
continue;
}
if (event.type != XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
continue;
}
g.session_state = reinterpret_cast<const XrEventDataSessionStateChanged&>(event).state;
LOGF("OpenXR: session state %d\n", static_cast<int>(g.session_state));
switch (g.session_state) {
case XR_SESSION_STATE_READY: {
XrSessionBeginInfo begin {XR_TYPE_SESSION_BEGIN_INFO};
begin.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
g.session_running = Ok(xrBeginSession(g.session, &begin), "xrBeginSession");
break;
}
case XR_SESSION_STATE_STOPPING: {
xrEndSession(g.session);
g.session_running = false;
std::lock_guard lock(g.input_mutex);
g.head = {};
g.input[0] = {};
g.input[1] = {};
break;
}
case XR_SESSION_STATE_EXITING:
case XR_SESSION_STATE_LOSS_PENDING: g.running = false; break;
default: break;
}
}
}
bool CopyToSwapchain(const VulkanImage& staging) {
uint32_t image = 0;
XrSwapchainImageAcquireInfo acquire {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
if (!Ok(xrAcquireSwapchainImage(g.swapchain, &acquire, &image), "acquire")) {
return false;
}
}
XrSwapchainImageWaitInfo wait {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait.timeout = XR_INFINITE_DURATION;
bool copied = Ok(xrWaitSwapchainImage(g.swapchain, &wait), "wait swapchain image");
if (copied) {
const auto target = g.swapchain_images[image].image;
(void)g.command.reset();
vk::CommandBufferBeginInfo begin {};
begin.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
(void)g.command.begin(&begin);
Barrier(g.command, target, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, 2, NO_ACCESS, TRANSFER_WRITE);
vk::ImageCopy copies[2] {};
for (uint32_t eye = 0; eye < 2; eye++) {
copies[eye].srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
copies[eye].srcOffset = vk::Offset3D {static_cast<int32_t>(eye * g.eye.width), 0, 0};
copies[eye].dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, eye, 1};
copies[eye].extent = vk::Extent3D {g.eye.width, g.eye.height, 1};
}
g.command.copyImage(staging.image, vk::ImageLayout::eTransferSrcOptimal, target,
vk::ImageLayout::eTransferDstOptimal, 2, copies);
Barrier(g.command, target, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eColorAttachmentOptimal, 2, TRANSFER_WRITE, COLOR_ATTACHMENT);
(void)g.command.end();
vk::SubmitInfo submit {};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &g.command;
(void)g.graphics->device.resetFences(1, &g.fence);
vk::Result result {};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
result = g.graphics->queue.submit(1, &submit, g.fence);
}
copied = result == vk::Result::eSuccess &&
g.graphics->device.waitForFences(1, &g.fence, VK_TRUE, UINT64_MAX) ==
vk::Result::eSuccess;
}
XrSwapchainImageReleaseInfo release {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
Common::LockGuard queue_lock(g.graphics->queue_mutex);
return Ok(xrReleaseSwapchainImage(g.swapchain, &release), "release") && copied;
}
// Holds the newest recorded eye image until the GPU has finished it, without waiting, then copies
// it into the swapchain. The swapchain keeps the last copy, so frames in between show it again.
bool UpdateSwapchain() {
int index = -1;
MasterSemaphore* semaphore = nullptr;
{
std::lock_guard lock(g.mutex);
if (g.held < 0 && g.latest >= 0 && g.staging[g.latest].tick != g.shown_tick) {
g.held = g.latest;
}
index = g.held;
semaphore = g.semaphore;
}
if (index >= 0) {
const auto& slot = g.staging[index];
semaphore->Refresh();
if (semaphore->IsFree(slot.tick)) {
if (CopyToSwapchain(slot.image)) {
g.shown_tick = slot.tick;
g.shown_head = slot.head;
}
std::lock_guard lock(g.mutex);
g.held = -1;
}
}
return g.shown_tick != 0;
}
// The game drew the picture for the head pose it read, so it is placed in the world at that pose
// and the runtime reprojects it as the head moves. Eye offsets come from the runtime.
void PlaceViews(XrTime time, XrCompositionLayerProjection& layer,
XrCompositionLayerProjectionView (&views)[2]) {
XrViewLocateInfo locate {XR_TYPE_VIEW_LOCATE_INFO};
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
locate.displayTime = time;
locate.space = g.view_space;
XrViewState state {XR_TYPE_VIEW_STATE};
XrView located[2] {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
uint32_t count = 0;
const bool valid =
XR_SUCCEEDED(xrLocateViews(g.session, &locate, &state, 2, &count, located)) &&
(state.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
const auto& head = g.shown_head;
const XrQuaternionf head_q {head.orientation[0], head.orientation[1], head.orientation[2],
head.orientation[3]};
const XrVector3f head_p {head.position[0], head.position[1], head.position[2]};
// The outer side of each eye is its temporal side: left for the left eye.
const float out = std::atan(Hmd2::FOV_TAN_OUT);
const float in = std::atan(Hmd2::FOV_TAN_IN);
const float top = std::atan(Hmd2::FOV_TAN_TOP);
const float bottom = std::atan(Hmd2::FOV_TAN_BOTTOM);
for (int eye = 0; eye < 2; eye++) {
const float side = eye == 0 ? -Hmd2::EYE_OFFSET_M : Hmd2::EYE_OFFSET_M;
auto& view = views[eye];
view.pose = valid ? located[eye].pose : XrPosef {{0, 0, 0, 1}, {side, 0, 0}};
if (head.valid) {
const auto offset = Rotate(head_q, view.pose.position);
view.pose.position = {head_p.x + offset.x, head_p.y + offset.y, head_p.z + offset.z};
view.pose.orientation = Multiply(head_q, view.pose.orientation);
}
view.fov = {eye == 0 ? -out : -in, eye == 0 ? in : out, top, -bottom};
view.subImage.swapchain = g.swapchain;
view.subImage.imageRect = {
{0, 0}, {static_cast<int32_t>(g.eye.width), static_cast<int32_t>(g.eye.height)}};
view.subImage.imageArrayIndex = static_cast<uint32_t>(eye);
}
layer.space = head.valid ? g.local_space : g.view_space;
layer.viewCount = 2;
layer.views = views;
}
void ThreadMain() {
while (g.running) {
HandleEvents();
XrFrameWaitInfo wait {XR_TYPE_FRAME_WAIT_INFO};
XrFrameState frame {XR_TYPE_FRAME_STATE};
if (!g.session_running || !Ok(xrWaitFrame(g.session, &wait, &frame), "xrWaitFrame")) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
PollInput(frame.predictedDisplayTime);
XrFrameBeginInfo begin {XR_TYPE_FRAME_BEGIN_INFO};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
if (!Ok(xrBeginFrame(g.session, &begin), "xrBeginFrame")) {
continue;
}
}
XrCompositionLayerProjection layer {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
XrCompositionLayerProjectionView views[2] {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
const auto* layers = reinterpret_cast<const XrCompositionLayerBaseHeader*>(&layer);
XrFrameEndInfo end {XR_TYPE_FRAME_END_INFO};
end.displayTime = frame.predictedDisplayTime;
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
if (frame.shouldRender == XR_TRUE && UpdateSwapchain()) {
PlaceViews(frame.predictedDisplayTime, layer, views);
end.layerCount = 1;
end.layers = &layers;
}
Common::LockGuard queue_lock(g.graphics->queue_mutex);
(void)Ok(xrEndFrame(g.session, &end), "xrEndFrame");
}
}
} // namespace
void OpenXrInit() {
if (!Config::VrEnabled()) {
return;
}
if (const char* env = std::getenv("KYTY_OPENXR");
env != nullptr && std::strcmp(env, "0") == 0) {
return;
}
const char* extensions[] = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
XrInstanceCreateInfo info {XR_TYPE_INSTANCE_CREATE_INFO};
std::strcpy(info.applicationInfo.applicationName, "KytyPS5");
std::strcpy(info.applicationInfo.engineName, "Kyty");
info.applicationInfo.apiVersion = XR_API_VERSION_1_0;
info.enabledExtensionCount = 1;
info.enabledExtensionNames = extensions;
if (XR_FAILED(xrCreateInstance(&info, &g.instance))) {
DestroyInstance("no runtime available");
return;
}
XrSystemGetInfo system {XR_TYPE_SYSTEM_GET_INFO};
system.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
if (XR_FAILED(xrGetSystem(g.instance, &system, &g.system))) {
DestroyInstance("no headset available");
return;
}
PFN_xrGetVulkanGraphicsRequirementsKHR get_requirements = nullptr;
PFN_xrGetVulkanInstanceExtensionsKHR get_extensions[2] {};
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsRequirementsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_requirements));
xrGetInstanceProcAddr(g.instance, "xrGetVulkanInstanceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[0]));
// Both queries share one signature.
xrGetInstanceProcAddr(g.instance, "xrGetVulkanDeviceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[1]));
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsDeviceKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&g.get_graphics_device));
XrGraphicsRequirementsVulkanKHR requirements {XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
if (get_requirements == nullptr || get_extensions[0] == nullptr ||
get_extensions[1] == nullptr || g.get_graphics_device == nullptr ||
XR_FAILED(get_requirements(g.instance, g.system, &requirements))) {
DestroyInstance("the runtime does not support XR_KHR_vulkan_enable");
return;
}
for (int i = 0; i < 2; i++) {
uint32_t size = 0;
get_extensions[i](g.instance, g.system, 0, &size, nullptr);
std::string names(size, '\0');
get_extensions[i](g.instance, g.system, size, &size, names.data());
g.extensions[i] = Common::Split(names.c_str(), ' ');
}
XrViewConfigurationView views[2] {{XR_TYPE_VIEW_CONFIGURATION_VIEW},
{XR_TYPE_VIEW_CONFIGURATION_VIEW}};
uint32_t count = 0;
xrEnumerateViewConfigurationViews(g.instance, g.system,
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 2, &count, views);
g.eye = {views[0].recommendedImageRectWidth, views[0].recommendedImageRectHeight};
XrSystemProperties properties {XR_TYPE_SYSTEM_PROPERTIES};
xrGetSystemProperties(g.instance, g.system, &properties);
Log::WriteToConsoleAndLog(fmt::format("OpenXR: headset '{}' ready\n", properties.systemName));
}
void OpenXrAppendExtensions(std::vector<const char*>& out,
const std::vector<vk::ExtensionProperties>& available, bool device) {
if (g.instance == XR_NULL_HANDLE) {
return;
}
for (const auto& name: g.extensions[device ? 1 : 0]) {
const auto same = [&](const char* other) { return name == other; };
if (std::any_of(available.begin(), available.end(),
[&](const auto& extension) { return same(extension.extensionName); }) &&
std::none_of(out.begin(), out.end(), same)) {
out.push_back(name.c_str());
}
}
}
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out) {
VkPhysicalDevice device = VK_NULL_HANDLE;
if (g.instance == XR_NULL_HANDLE ||
XR_FAILED(g.get_graphics_device(g.instance, g.system, static_cast<VkInstance>(instance),
&device))) {
return false;
}
out = vk::PhysicalDevice(device);
return true;
}
void OpenXrStartSession(GraphicContext& graphics) {
if (g.instance == XR_NULL_HANDLE) {
return;
}
// The runtime must be asked for its GPU before a session is created.
vk::PhysicalDevice required;
if (!OpenXrPhysicalDevice(graphics.instance, required) ||
required != graphics.physical_device) {
DestroyInstance("the headset runs on another GPU; select it with --gpu-index");
return;
}
g.graphics = &graphics;
XrGraphicsBindingVulkanKHR binding {XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
binding.instance = static_cast<VkInstance>(graphics.instance);
binding.physicalDevice = static_cast<VkPhysicalDevice>(graphics.physical_device);
binding.device = static_cast<VkDevice>(graphics.device);
binding.queueFamilyIndex = graphics.queue_family;
binding.queueIndex = 0;
XrSessionCreateInfo info {XR_TYPE_SESSION_CREATE_INFO};
info.next = &binding;
info.systemId = g.system;
XrReferenceSpaceCreateInfo space {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
space.poseInReferenceSpace = {{0, 0, 0, 1}, {0, 0, 0}};
const auto reference = [&](XrReferenceSpaceType type, XrSpace& out) {
space.referenceSpaceType = type;
return Ok(xrCreateReferenceSpace(g.session, &space, &out), "xrCreateReferenceSpace");
};
if (!Ok(xrCreateSession(g.instance, &info, &g.session), "xrCreateSession") ||
!reference(XR_REFERENCE_SPACE_TYPE_VIEW, g.view_space) ||
!reference(XR_REFERENCE_SPACE_TYPE_LOCAL, g.local_space) || !CreateSwapchain()) {
DestroyInstance("the headset session could not be created");
return;
}
if (!CreateInput()) {
g.action_set = XR_NULL_HANDLE;
}
vk::CommandPoolCreateInfo pool {};
pool.queueFamilyIndex = graphics.queue_family;
pool.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
(void)graphics.device.createCommandPool(&pool, nullptr, &g.pool);
vk::CommandBufferAllocateInfo allocate {};
allocate.commandPool = g.pool;
allocate.level = vk::CommandBufferLevel::ePrimary;
allocate.commandBufferCount = 1;
(void)graphics.device.allocateCommandBuffers(&allocate, &g.command);
vk::FenceCreateInfo fence {};
(void)graphics.device.createFence(&fence, nullptr, &g.fence);
g.running = true;
g.thread = std::thread(ThreadMain);
}
void OpenXrShutdown() {
g.running = false;
if (g.thread.joinable()) {
g.thread.join();
}
DestroyInstance(nullptr);
if (g.graphics == nullptr) {
return;
}
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
(void)g.graphics->device.waitIdle();
}
for (auto& slot: g.staging) {
if (slot.image.image != nullptr) {
g.graphics->DeleteImage(slot.image);
}
}
g.graphics->device.destroyFence(g.fence, nullptr);
g.graphics->device.destroyCommandPool(g.pool, nullptr);
g.graphics = nullptr;
}
VrPose OpenXrGetHeadPose() {
if (!g.running) {
return {};
}
std::lock_guard lock(g.input_mutex);
if (g.head.valid) {
g.last_head_read = g.head;
}
return g.head;
}
VrControllerInput OpenXrGetController(int hand) {
if (!g.running) {
return {};
}
std::lock_guard lock(g.input_mutex);
return g.input[hand];
}
void OpenXrQueueEyeSource(CommandBuffer& command, const VulkanImage& source) {
if (!g.running) {
return;
}
std::lock_guard lock(g.mutex);
int index = 0;
while (index == g.held || index == g.latest) {
index++;
}
auto& slot = g.staging[index];
const auto vk_command = command.Handle();
Barrier(vk_command, slot.image.image, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, 1, TRANSFER, TRANSFER_WRITE);
vk::ImageBlit blit {};
blit.srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
blit.srcOffsets[1] = vk::Offset3D {static_cast<int32_t>(source.extent.width),
static_cast<int32_t>(source.extent.height), 1};
blit.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
blit.dstOffsets[1] = vk::Offset3D {static_cast<int32_t>(slot.image.extent.width),
static_cast<int32_t>(slot.image.extent.height), 1};
vk_command.blitImage(source.image, vk::ImageLayout::eTransferSrcOptimal, slot.image.image,
vk::ImageLayout::eTransferDstOptimal, 1, &blit, vk::Filter::eLinear);
Barrier(vk_command, slot.image.image, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal, 1, TRANSFER_WRITE, TRANSFER_READ);
auto& scheduler = command.GetContext().GetCommandScheduler();
slot.tick = scheduler.CurrentTick();
g.semaphore = &scheduler.GetMasterSemaphore();
g.latest = index;
std::lock_guard input_lock(g.input_mutex);
slot.head = g.last_head_read;
}
} // namespace Libs::Vr
+41
View File
@@ -0,0 +1,41 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
#include "graphics/host_gpu/vulkanCommon.h"
#include <vector>
namespace Libs::Graphics {
struct GraphicContext;
struct VulkanImage;
class CommandBuffer;
} // namespace Libs::Graphics
// Output to a real headset through OpenXR. Everything here is inert unless the virtual headset is
// enabled (--vr) and an OpenXR runtime with a headset is available; KYTY_OPENXR=0 turns it off.
namespace Libs::Vr {
// Creates the OpenXR instance. Call before the Vulkan instance exists, because the runtime names
// the Vulkan extensions and the GPU it needs.
void OpenXrInit();
// Appends the Vulkan instance or device extensions the runtime needs, when available.
void OpenXrAppendExtensions(std::vector<const char*>& out,
const std::vector<vk::ExtensionProperties>& available, bool device);
// The physical device the runtime renders with. False when OpenXR is not in use.
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out);
// Creates the session and starts the headset thread. Call once the device and queue exist.
void OpenXrStartSession(Graphics::GraphicContext& graphics);
// Stops the headset thread and destroys the session. Call before the device is destroyed.
void OpenXrShutdown();
// Render thread, headset flips only: scales the side-by-side eye image into a staging image for the
// headset thread. The source is in transfer-source layout.
void OpenXrQueueEyeSource(Graphics::CommandBuffer& command, const Graphics::VulkanImage& source);
} // namespace Libs::Vr
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
@@ -310,6 +310,10 @@ void GpuActivityRows(const Snapshot& shown) {
Row("CPU waiting on GPU",
Fmt("%.0f ms/s (%.0f waits/s)", Rate(shown, Counter::GpuWaitNs) / 1e6,
Rate(shown, Counter::GpuWaits)));
Row("DCC/CMASK readbacks",
Fmt("%.0f ms/s (%.0f/s)", Rate(shown, Counter::ColorMetadataReadbackNs) / 1e6,
Rate(shown, Counter::ColorMetadataReadbacks)));
Row("DCC/CMASK GPU checks", Fmt("%.0f/s", Rate(shown, Counter::ColorMetadataChecks)));
}
void CompilationRows(const Snapshot& shown) {
@@ -529,16 +533,16 @@ void DrawMemoryAndIo(const Snapshot& shown) {
Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)");
Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total));
Row("Extended", Bytes(guest.automatic_allocated));
// Past the pool size the guest sees no page-table entries available.
const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries);
// Past its total a side sees no page-table entries available.
const auto percent = std::max(guest.cpu_page_entries * 100 / guest.cpu_page_entries_total,
guest.gpu_page_entries * 100 / guest.gpu_page_entries_total);
Row("Page table (2 MB)",
Fmt("CPU %llu GPU %llu / %llu",
Fmt("CPU %llu / %llu GPU %llu / %llu",
static_cast<unsigned long long>(guest.cpu_page_entries),
static_cast<unsigned long long>(guest.cpu_page_entries_total),
static_cast<unsigned long long>(guest.gpu_page_entries),
static_cast<unsigned long long>(guest.page_entries_total)),
entries > guest.page_entries_total ? BAD
: entries * 10 > guest.page_entries_total * 9 ? MEDIUM
: GOOD);
static_cast<unsigned long long>(guest.gpu_page_entries_total)),
LevelColor(static_cast<double>(percent), 90, 100));
ImGui::EndTable();
}
if (BeginSection("Emulator process")) {
+1 -1
View File
@@ -27,7 +27,7 @@ public:
KYTY_CLASS_NO_COPY(Presenter);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
bool hdr = false);
bool hdr = false, bool headset = false);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] bool PresentLastFrame();
+94 -24
View File
@@ -21,6 +21,7 @@
#include "graphics/presentation/renderDoc.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/systemContent.h"
@@ -51,6 +52,7 @@ constexpr int VIDEO_OUT_FALSE = 0;
constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0;
constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1;
constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2;
constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 32;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC = 1;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC_MULTI = 4;
constexpr int VIDEO_OUT_FLIP_MODE_SLAVE = 8;
@@ -66,6 +68,7 @@ constexpr uint64_t VIDEO_OUT_REFRESH_RATE_59_94HZ = 3;
constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2 = 0x10;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED = 32;
@@ -218,7 +221,7 @@ public:
void WaitForSubmitSlot(VideoOutConfig& cfg);
bool Flip(uint32_t micros);
void GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out);
void Wait(VideoOutConfig& cfg, int index);
bool IsSafeForRendering(VideoOutConfig& cfg, int index);
private:
enum class RequestState { Reserved, Recording, Ready, Presenting };
@@ -237,6 +240,7 @@ private:
Graphics::Presenter::Frame* frame;
bool premultiplied_alpha;
};
void SignalDoneLocked();
Graphics::Presenter& m_presenter;
Common::Mutex m_mutex;
@@ -247,13 +251,15 @@ private:
std::list<Request> m_cpu_requests;
std::list<Request> m_cancelled_requests;
bool m_processing = false;
bool m_gpu_waiting = false;
uint64_t m_next_request_id = 1;
};
struct VideoOutDriver::Impl {
public:
static constexpr std::array VIDEO_OUT_BUSES {
VIDEO_OUT_BUS_TYPE_MAIN, VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB};
static constexpr std::array VIDEO_OUT_BUSES {VIDEO_OUT_BUS_TYPE_MAIN,
VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB,
VIDEO_OUT_BUS_TYPE_HMD2_MAIN};
static constexpr int VIDEO_OUT_NUM_MAX = VIDEO_OUT_BUSES.size() + 1;
Impl(uint32_t width, uint32_t height, Graphics::Presenter& presenter)
@@ -516,6 +522,10 @@ static bool IsValidFlipMode(int mode) {
mode == VIDEO_OUT_FLIP_MODE_SLAVE || mode == VIDEO_OUT_FLIP_MODE_MASTER;
}
// Set once the headset port shows eye images. The window then shows them instead of the main port,
// so the picture is visible without a headset.
static std::atomic<bool> g_headset_mirror {false};
static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int index, int flip_mode,
int64_t flip_arg, FlipRequestSource source, uint64_t& request_id) {
auto* video_out = driver.Get(handle);
@@ -534,8 +544,10 @@ static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int inde
(video_out->slave_count != 0) != (flip_mode == VIDEO_OUT_FLIP_MODE_MASTER)) {
return VIDEO_OUT_ERROR_INVALID_FLIP_MODE;
}
// The headset port can flip before its reprojection buffers are registered; those show blank.
if (video_out->closing ||
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied())) {
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied() &&
video_out->bus != VIDEO_OUT_BUS_TYPE_HMD2_MAIN)) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
if (!driver.GetFlipQueue().Reserve(*video_out, index, flip_arg, source, request_id)) {
@@ -740,6 +752,9 @@ bool VideoOutDriver::Impl::Close(int handle) {
master->slave_count--;
}
m_flip_queue.Cancel(config);
if (config.bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) {
g_headset_mirror = false;
}
m_presenter.ClearLayer(config.bus);
DeleteVideoOutEvents(flip_events, VideoOutEventKind::Flip);
DeleteVideoOutEvents(pre_vblank_events, VideoOutEventKind::PreVblankStart);
@@ -940,7 +955,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe
}
}
}
m_done_cond_var.SignalAll();
SignalDoneLocked();
}
if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRejected);
@@ -1020,7 +1035,7 @@ void FlipQueue::Cancel(VideoOutConfig& cfg) {
it = queue->erase(it);
}
}
m_done_cond_var.SignalAll();
SignalDoneLocked();
m_submit_slot_cond_var.SignalAll();
m_submit_cond_var.SignalAll();
m_mutex.Unlock();
@@ -1066,16 +1081,19 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
index = request->index;
}
const bool special = IsSpecialBufferIndex(index);
bool special = IsSpecialBufferIndex(index);
Graphics::ImageInfo source_info;
uint32_t width = 0;
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
bool hdr = false;
bool headset = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
headset = cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
special = special || (headset && !cfg->buffers[index].Occupied());
if (current && special) {
width = cfg->width;
height = cfg->height;
@@ -1120,7 +1138,10 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
&buffer);
} else {
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr, headset);
if (headset) {
g_headset_mirror = true;
}
}
Common::LockGuard lock(m_mutex);
@@ -1186,19 +1207,26 @@ void FlipQueue::WaitForSubmitSlot(VideoOutConfig& cfg) {
}
}
void FlipQueue::Wait(VideoOutConfig& cfg, int index) {
Common::LockGuard lock(m_mutex);
auto has_request = [this, &cfg, index] {
auto matches = [&cfg, index](const auto& r) { return r.cfg == &cfg && r.index == index; };
return std::any_of(m_requests.begin(), m_requests.end(), matches) ||
std::any_of(m_cpu_requests.begin(), m_cpu_requests.end(), matches);
};
while (has_request()) {
m_done_cond_var.Wait(&m_mutex);
void FlipQueue::SignalDoneLocked() {
m_done_cond_var.SignalAll();
// A stalled GPU cannot drain and shut down without re-checking under m_mutex, so it is alive here.
if (m_gpu_waiting) {
m_gpu_waiting = false;
m_presenter.Renderer().GetGpu().Wake();
}
}
// A display buffer is safe for rendering once it is neither displayed nor waiting to be.
bool FlipQueue::IsSafeForRendering(VideoOutConfig& cfg, int index) {
Common::LockGuard lock(m_mutex);
auto matches = [&cfg, index](const auto& r) { return r.cfg == &cfg && r.index == index; };
const bool safe = cfg.flip_status.currentBuffer != index &&
std::none_of(m_requests.begin(), m_requests.end(), matches) &&
std::none_of(m_cpu_requests.begin(), m_cpu_requests.end(), matches);
m_gpu_waiting = !safe;
return safe;
}
bool FlipQueue::Flip(uint32_t micros) {
KYTY_PROFILER_BLOCK("FlipQueue::Flip");
@@ -1239,6 +1267,8 @@ bool FlipQueue::Flip(uint32_t micros) {
[](const auto& a, const auto& b) { return a.cfg->bus < b.cfg->bus; });
bool due = true;
std::array<Graphics::Presenter::Layer, VideoOutDriver::Impl::VIDEO_OUT_BUSES.size()> layers {};
size_t layer_count = 0;
Graphics::Presenter::Frame* hidden = nullptr; // The main port behind the headset image.
for (size_t i = 0; i < count; i++) {
auto& r = requests[i];
r.cfg->mutex.Lock();
@@ -1246,10 +1276,22 @@ bool FlipQueue::Flip(uint32_t micros) {
if (r.id == group) {
due &= IsFlipDueLocked(*r.cfg, r.generation);
}
layers[i] = {r.frame, r.cfg->bus, r.premultiplied_alpha};
if (r.cfg->bus == VIDEO_OUT_BUS_TYPE_MAIN &&
g_headset_mirror.load(std::memory_order_relaxed)) {
hidden = r.frame;
} else {
const bool headset = r.cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
layers[layer_count++] = {r.frame, headset ? VIDEO_OUT_BUS_TYPE_MAIN : r.cfg->bus,
r.premultiplied_alpha};
}
}
if (due) {
m_presenter.Present(std::span(layers.data(), count));
if (hidden != nullptr) {
m_presenter.Discard(*hidden);
}
if (layer_count != 0) {
m_presenter.Present(std::span(layers.data(), layer_count));
}
if (requests[0].cfg->bus == 0) {
Graphics::PerformanceOverlayRecordFlip(requests[0].cfg->width, requests[0].cfg->height);
}
@@ -1278,7 +1320,7 @@ bool FlipQueue::Flip(uint32_t micros) {
it = m_requests.erase(it);
}
m_processing = false;
m_done_cond_var.SignalAll();
SignalDoneLocked();
m_submit_slot_cond_var.SignalAll();
m_mutex.Unlock();
for (size_t i = count; i != 0; i--) {
@@ -1461,6 +1503,33 @@ KYTY_SYSV_ABI int VideoOutRegisterBuffers2(int handle, int set_index, int buffer
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
}
// The headset port takes reprojection buffers with a zeroed attribute. Each names its eye
// texture, which is registered and flipped like a regular surface.
const bool headset = ctx->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
if (headset != (category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2)) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
}
VideoOutBufferAttribute2 headset_attribute {};
std::array<VideoOutBuffers, VIDEO_OUT_BUFFER_NUM_MAX> headset_buffers {};
if (headset) {
for (int i = 0; i < buffer_num; i++) {
Hmd2::ReprojectionSurface surface {};
if (buffers[i].metadata != nullptr ||
!Hmd2::GetReprojectionSurface(buffers[i].data, surface) ||
(i != 0 && (surface.width != headset_attribute.width ||
surface.height != headset_attribute.height ||
surface.pixel_format != headset_attribute.pixel_format))) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
headset_attribute.pixel_format = surface.pixel_format;
headset_attribute.width = surface.width;
headset_attribute.height = surface.height;
headset_buffers[i].data = reinterpret_cast<const void*>(surface.address);
}
attribute = &headset_attribute;
buffers = headset_buffers.data();
category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED;
}
if (category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED &&
category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
@@ -1634,12 +1703,12 @@ void VideoOutDriver::WaitForSubmitSlot(int handle) {
m_impl->GetFlipQueue().WaitForSubmitSlot(*cfg);
}
void VideoOutDriver::WaitFlipDone(int handle, int index) {
bool VideoOutDriver::IsSafeForRendering(int handle, int index) {
auto* ctx = m_impl->Get(handle);
EXIT_IF(ctx == nullptr);
EXIT_NOT_IMPLEMENTED(!IsValidBufferIndex(index));
m_impl->GetFlipQueue().Wait(*ctx, index);
return m_impl->GetFlipQueue().IsSafeForRendering(*ctx, index);
}
KYTY_SYSV_ABI int VideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) {
@@ -1871,7 +1940,8 @@ static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO
}
}
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) {
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ &&
mode != Hmd2::OUTPUT_MODE_89_91HZ && mode != Hmd2::OUTPUT_MODE_119_88HZ) {
return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE;
}
+1 -1
View File
@@ -38,7 +38,7 @@ public:
void CompleteFlip(uint64_t request_id);
void SubmitFlipPreparation(uint64_t request_id);
void WaitForSubmitSlot(int handle);
void WaitFlipDone(int handle, int index);
[[nodiscard]] bool IsSafeForRendering(int handle, int index);
[[nodiscard]] Impl& State() noexcept;
+36
View File
@@ -0,0 +1,36 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
namespace Libs::Vr {
// A pose in the tracking space: metres, right-handed, +y up, -z forward (the OpenXR convention).
struct VrPose {
bool valid = false;
float orientation[4] = {0.0f, 0.0f, 0.0f, 1.0f}; // x, y, z, w
float position[3] = {0.0f, 0.0f, 0.0f};
};
// One headset controller. Sticks are -1..1 with +y up, the trigger and squeeze 0..1.
struct VrControllerInput {
bool connected = false;
VrPose pose;
float trigger = 0.0f;
float squeeze = 0.0f;
float stick_x = 0.0f;
float stick_y = 0.0f;
bool primary = false; // X on the left controller, A on the right one.
bool secondary = false; // Y on the left controller, B on the right one.
bool stick_click = false;
bool menu = false;
};
// Latest head pose; not valid, at the origin, while no headset session tracks the head.
[[nodiscard]] VrPose OpenXrGetHeadPose();
// Latest state of the left (0) or right (1) controller. While it is not connected the caller keeps
// using the keyboard or gamepad.
[[nodiscard]] VrControllerInput OpenXrGetController(int hand);
} // namespace Libs::Vr
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
@@ -11,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/openxrHeadset.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
@@ -918,7 +919,8 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr,
bool headset) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto frame_format = info.pixel_format;
@@ -938,6 +940,12 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
frame->Configure(m_impl->window.graphic_ctx,
{image.backing.extent.width, image.backing.extent.height}, frame_format);
frame->CopyFrom(buffer, image);
if (headset) {
// The frame holds the raw display-encoded pixels the headset needs.
frame->Transit(buffer.Handle(), vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead);
Vr::OpenXrQueueEyeSource(buffer, frame->image);
}
return *frame;
}
@@ -1,6 +1,3 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
@@ -14,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/openxrHeadset.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/videoOut.h"
@@ -23,7 +21,10 @@
#include "libs/controller.h"
#include "loader/systemContent.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fmt/format.h>
@@ -50,6 +51,7 @@ struct VulkanExtensions {
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
vk::PhysicalDeviceVulkan11Features features {};
features.storageBuffer16BitAccess = VK_TRUE;
features.shaderDrawParameters = VK_TRUE;
return features;
}
@@ -65,6 +67,9 @@ vk::PhysicalDeviceVulkan12Features WindowContext::RequiredVulkan12Features() noe
features.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
features.shaderStorageImageArrayNonUniformIndexing = VK_TRUE;
features.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE;
features.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
features.descriptorBindingUpdateUnusedWhilePending = VK_TRUE;
features.descriptorBindingPartiallyBound = VK_TRUE;
return features;
}
@@ -170,6 +175,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
LOGF("Vulkan GPU index %d is unavailable; selecting automatically\n",
Config::GetGpuIndex());
}
} else if (vk::PhysicalDevice headset; Vr::OpenXrPhysicalDevice(instance, headset)) {
devices = {headset}; // The headset runtime renders on this GPU.
}
vk::PhysicalDevice best_device = nullptr;
@@ -272,6 +279,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
required_features12.samplerMirrorClampToEdge);
check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess",
WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess);
check_feature(features11.shaderDrawParameters, "shaderDrawParameters",
WindowContext::RequiredVulkan11Features().shaderDrawParameters);
check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess",
required_features12.storageBuffer8BitAccess);
check_feature(features12.timelineSemaphore, "timelineSemaphore",
@@ -293,6 +302,14 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
check_feature(features12.shaderStorageBufferArrayNonUniformIndexing,
"shaderStorageBufferArrayNonUniformIndexing",
required_features12.shaderStorageBufferArrayNonUniformIndexing);
check_feature(features12.descriptorBindingSampledImageUpdateAfterBind,
"descriptorBindingSampledImageUpdateAfterBind",
required_features12.descriptorBindingSampledImageUpdateAfterBind);
check_feature(features12.descriptorBindingUpdateUnusedWhilePending,
"descriptorBindingUpdateUnusedWhilePending",
required_features12.descriptorBindingUpdateUnusedWhilePending);
check_feature(features12.descriptorBindingPartiallyBound, "descriptorBindingPartiallyBound",
required_features12.descriptorBindingPartiallyBound);
check_feature(features13.robustImageAccess, "robustImageAccess");
check_feature(robustness2.nullDescriptor, "nullDescriptor");
check_feature(features13.dynamicRendering, "dynamicRendering",
@@ -323,9 +340,13 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
check_feature(device_features2.features.shaderImageGatherExtended,
"shaderImageGatherExtended");
check_feature(device_features2.features.shaderResourceMinLod, "shaderResourceMinLod");
check_feature(device_features2.features.independentBlend, "independentBlend");
check_feature(device_features2.features.tessellationShader, "tessellationShader");
check_feature(device_features2.features.drawIndirectFirstInstance,
"drawIndirectFirstInstance");
check_feature(device_features2.features.multiDrawIndirect, "multiDrawIndirect");
if (!skip_device) {
auto available_extensions = EnumerateVulkan<vk::ExtensionProperties>(
@@ -490,6 +511,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
maintenance8.pNext = supported_features2.pNext;
supported_features2.pNext = &maintenance8;
}
const bool shader_clock_extension =
HasExtension(device_extensions, VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
vk::PhysicalDeviceShaderClockFeaturesKHR shader_clock {};
if (shader_clock_extension) {
shader_clock.pNext = supported_features2.pNext;
supported_features2.pNext = &shader_clock;
}
const bool image_atomic_int64_extension =
HasExtension(device_extensions, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME);
vk::PhysicalDeviceShaderImageAtomicInt64FeaturesEXT image_atomic_int64 {};
@@ -497,6 +525,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.pNext = supported_features2.pNext;
supported_features2.pNext = &image_atomic_int64;
}
const bool conditional_rendering_extension =
HasExtension(device_extensions, VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME);
vk::PhysicalDeviceConditionalRenderingFeaturesEXT conditional_rendering {};
if (conditional_rendering_extension) {
conditional_rendering.pNext = supported_features2.pNext;
supported_features2.pNext = &conditional_rendering;
}
const bool barycentric_extension =
HasExtension(device_extensions, VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR supported_barycentric {};
@@ -570,6 +605,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
feedback_dynamic.attachmentFeedbackLoopDynamicState;
graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8;
// The device clock ticks at timestampPeriod nanoseconds.
if (shader_clock_extension && shader_clock.shaderDeviceClock) {
graphics.device_clock_period_ps = static_cast<uint32_t>(
std::lround(graphics.physical_device_properties.limits.timestampPeriod * 1000.0f));
}
graphics.conditional_rendering_enabled =
conditional_rendering_extension && conditional_rendering.conditionalRendering;
LOGF("Vulkan maintenance8 support: %s\n",
graphics.maintenance8_enabled ? "true" : "false");
LOGF("Vulkan depth feedback support: %s\n",
@@ -595,10 +637,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
device_features.depthClamp = VK_TRUE;
#endif
device_features.shaderStorageImageWriteWithoutFormat = VK_TRUE;
device_features.shaderStorageImageMultisample =
supported_features2.features.shaderStorageImageMultisample;
device_features.shaderImageGatherExtended = VK_TRUE;
device_features.shaderResourceMinLod = VK_TRUE;
device_features.independentBlend = VK_TRUE;
device_features.dualSrcBlend = VK_TRUE;
device_features.tessellationShader = VK_TRUE;
device_features.drawIndirectFirstInstance = VK_TRUE;
device_features.multiDrawIndirect = VK_TRUE;
device_features.sampleRateShading = VK_TRUE;
device_features.depthBiasClamp = VK_TRUE;
device_features.shaderClipDistance = VK_TRUE;
@@ -607,7 +654,22 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
device_features.multiViewport = VK_TRUE;
device_features.fillModeNonSolid = VK_TRUE;
device_features.vertexPipelineStoresAndAtomics = VK_TRUE;
// Storage image sample counts beyond 1x require the feature.
if (!device_features.shaderStorageImageMultisample) {
graphics.physical_device_properties.limits.storageImageSampleCounts =
vk::SampleCountFlagBits::e1;
}
graphics.sample_rate_shading_enabled = true;
// Occlusion queries reset their queries from the host and count samples exactly where they can.
graphics.occlusion_queries_enabled =
Config::OcclusionQueriesEnabled() && supported_features12.hostQueryReset;
graphics.occlusion_query_precise_enabled =
graphics.occlusion_queries_enabled && supported_features2.features.occlusionQueryPrecise;
features12.hostQueryReset = graphics.occlusion_queries_enabled;
device_features.occlusionQueryPrecise = graphics.occlusion_query_precise_enabled;
LOGF("Vulkan occlusion queries: %s, precise: %s\n",
graphics.occlusion_queries_enabled ? "true" : "false",
graphics.occlusion_query_precise_enabled ? "true" : "false");
device_features.shaderInt64 = VK_TRUE;
device_features.shaderFloat64 =
supported_features2.features.shaderFloat64 &&
@@ -668,6 +730,23 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
maintenance8.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &maintenance8;
}
if (graphics.device_clock_period_ps != 0u) {
shader_clock.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &shader_clock;
}
if (graphics.conditional_rendering_enabled) {
conditional_rendering.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &conditional_rendering;
}
// The renderer only uses the Vulkan 1.3 core dynamic states, but RADV (Mesa 26.3) creates its
// PS epilog cache only when this EXT feature is enabled and crashes in vkCmdDraw when a
// depth- or stencil-writing fragment shader needs an epilog.
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extended_dynamic_state {};
extended_dynamic_state.extendedDynamicState = VK_TRUE;
if (HasExtension(device_extensions, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME)) {
extended_dynamic_state.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &extended_dynamic_state;
}
create_info.pQueueCreateInfos = &queue_create_info;
create_info.queueCreateInfoCount = 1;
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
@@ -876,8 +955,11 @@ void WindowContext::CreateVulkan() {
}
VULKAN_HPP_DEFAULT_DISPATCHER.init(get_instance_proc_addr);
Vr::OpenXrInit();
VulkanExtensions r;
VulkanGetExtensions(r);
Vr::OpenXrAppendExtensions(r.required_extensions, r.available_extensions, false);
VulkanCheckInstanceVersion();
vk::ApplicationInfo app_info {};
@@ -1036,12 +1118,15 @@ void WindowContext::CreateVulkan() {
graphic_ctx.memory_budget_ext_enabled = true;
}
for (const auto* extension: {VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
VK_KHR_SHADER_CLOCK_EXTENSION_NAME,
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME}) {
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME,
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME,
VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME}) {
if (HasExtension(available_extensions, extension)) {
device_extensions.push_back(extension);
}
@@ -1051,6 +1136,7 @@ void WindowContext::CreateVulkan() {
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME);
}
Vr::OpenXrAppendExtensions(device_extensions, available_extensions, true);
}
graphic_ctx.device = VulkanCreateDevice(graphic_ctx, device_extensions);
@@ -1068,6 +1154,7 @@ void WindowContext::CreateVulkan() {
render_context = std::make_unique<RenderContext>(graphic_ctx);
LibKernel::Memory::InstallGpuResources(render_context.get());
presenter = std::make_unique<Presenter>(*this);
Vr::OpenXrStartSession(graphic_ctx);
}
void WindowContext::RefreshSurfaceCapabilities() {
@@ -1090,6 +1177,7 @@ void WindowContext::RecreateSurface() {
}
WindowContext::~WindowContext() {
Vr::OpenXrShutdown();
ShutdownSystemOverlayInput();
presenter.reset();
LibKernel::Memory::InstallGpuResources(nullptr);
@@ -32,8 +32,6 @@
#include <vector>
#include <vulkan/vk_platform.h>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_SIMD
#include "stb_image.h"
// IWYU pragma: no_include <intrin.h>
@@ -30,10 +30,6 @@ namespace Libs::Graphics::ShaderRecompiler {
namespace {
const char* GetDumpLabel(const CompileOptions& options) {
return options.dump_label != nullptr ? options.dump_label : "ShaderRecompiler";
}
std::string MakeIrDump(std::string_view cfg, const IR::Program& ir) {
std::string dump = "CFG:\n";
dump += cfg;
@@ -44,30 +40,17 @@ std::string MakeIrDump(std::string_view cfg, const IR::Program& ir) {
return dump;
}
const char* StageName(ShaderType stage) {
switch (stage) {
case ShaderType::Compute: return "CS";
case ShaderType::Vertex: return "VS";
case ShaderType::Local: return "LS";
case ShaderType::TessellationControl: return "HS";
case ShaderType::TessellationEvaluation: return "TES";
case ShaderType::Mesh: return "MS";
case ShaderType::Pixel: return "PS";
default: return "unknown";
}
}
void LogDispatcherFallback(const CompileOptions& options, const CFG::Graph& cfg, const char* phase) {
const auto* block = cfg.FindBlock(cfg.failure_block);
const auto start = block != nullptr ? block->start_pc : UINT32_MAX;
const auto end = block != nullptr ? block->end_pc : UINT32_MAX;
const auto predecessors = block != nullptr ? block->predecessors.size() : 0u;
const auto successors = block != nullptr ? block->successors.size() : 0u;
LOGF("%s CFG dispatcher fallback: stage=%s hash=0x%016" PRIx64
LOGF("ShaderRecompiler %s CFG dispatcher fallback: hash=0x%016" PRIx64
" phase=%s failure=%s block=%" PRIu32 " pc=0x%08" PRIx32 "..0x%08" PRIx32 " preds=%" PRIu64
" succs=%" PRIu64 " blocks=%" PRIu64 " loops=%" PRIu64 " back_edges=%" PRIu64
" reason=%s\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash, phase,
StageName(options.stage), options.shader_hash, phase,
CFG::FailureKindToString(cfg.failure_kind).c_str(), cfg.failure_block, start, end,
static_cast<uint64_t>(predecessors), static_cast<uint64_t>(successors),
static_cast<uint64_t>(cfg.blocks.size()), static_cast<uint64_t>(cfg.natural_loops.size()),
@@ -209,20 +192,20 @@ int BufferTableAttribFromOffset(uint32_t raw_offset, int dword) {
}
Frontend::EmbeddedFetchPlan DetectEmbeddedVertexFetch(
const Decoder::Program& decoded, const ShaderVertexInputInfo* input_info,
const Decoder::Program& decoded, const ShaderVertexInputInfo& input_info,
uint32_t user_data_base, uint32_t user_data_count, uint32_t wave_size) {
const uint32_t vertex_index_reg = input_info->logical_stage == ShaderType::Local ? 2u : 5u;
const uint32_t instance_index_reg = input_info->logical_stage == ShaderType::Local ? 5u : 8u;
const uint32_t vertex_index_reg = input_info.logical_stage == ShaderType::Local ? 2u : 5u;
const uint32_t instance_index_reg = input_info.logical_stage == ShaderType::Local ? 5u : 8u;
Frontend::EmbeddedFetchPlan data;
data.loads.reserve(input_info->resources_num);
data.loads.reserve(input_info.resources_num);
int32_t vertex_offset_candidate = -1;
int32_t instance_offset_candidate = -1;
bool vertex_offset_conflict = false;
bool instance_offset_conflict = false;
const int shift_regs = 8;
const int attrib_reg = input_info->fetch_attrib_reg + shift_regs;
const int buffer_reg = input_info->fetch_buffer_reg + shift_regs;
const int attrib_reg = input_info.fetch_attrib_reg + shift_regs;
const int buffer_reg = input_info.fetch_buffer_reg + shift_regs;
std::array<EmbeddedFetchSgprInfo, 108> sgprs {};
std::array<bool, 256> vgpr_is_index {};
@@ -481,18 +464,14 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
return result;
}
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
source.wave_size = options.wave_size;
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
source.call = ShaderSource::Call {.instruction = i};
source.calls.push_back({.instruction = i});
}
EXIT_IF(options.input_info.compute == nullptr);
const auto& call = source.decoded.instructions[source.call->instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
const auto graph = CFG::BuildGraph(source.decoded);
Program query;
query.stage = options.stage;
@@ -524,7 +503,12 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
return value;
};
Register = [&](uint32_t before, uint32_t code) -> Value {
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
std::vector<uint32_t> crossed_calls;
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code,
source.wave_size, crossed_calls);
for (const auto crossed: crossed_calls) {
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
}
if (index == UINT32_MAX) {
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
@@ -557,30 +541,58 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
EXIT_IF(!producer.ready || !producer.values.contains(code));
return producer.values.at(code).Resolve();
};
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(source.call->instruction, call.src0.reg + i);
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
source.call->target[i] = Value(&root);
for (auto& site: source.calls) {
const auto& call = source.decoded.instructions[site.instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(site.instruction, call.src0.reg + i);
if (!ValidateRuntimeValue(query, value, RuntimeValueType::Integer)) {
EXIT_IF(value.Instruction()->GetOpcode() != ValueOpcode::ReadConstBuffer);
source.unsupported_call_pc = call.pc;
source.calls.clear();
return;
}
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
site.target[i] = Value(&root);
}
}
RewriteToSsa(query.blocks);
ConstantPropagationPass(query.blocks, query.wave_size);
RemoveIdentities(query);
EliminateDeadCode(query.blocks);
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
for (auto& site: source.calls)
for (auto& value: site.target) value = value.Instruction()->Arg(0).Resolve();
query.value_storage.clear();
for (auto& inst: block) {
inst.SetParent(nullptr);
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 || inst.GetOpcode() == ValueOpcode::ReadConstBuffer)
inst.SetFlags(SrtReadFlags {.index = inst.Flags<MemoryFlags>().index});
inst.SetFlags(MemoryFlags {.index = inst.Flags<MemoryFlags>().index});
}
query.value_storage.splice(query.value_storage.end(), block.Instructions());
source.call_targets = std::move(static_cast<ResourcePlan&>(query));
std::vector<Value> targets;
for (const auto& site: source.calls) targets.insert(targets.end(), site.target.begin(), site.target.end());
CompileSrtWalker(source.call_targets, targets);
}
} // namespace
const char* StageName(ShaderType stage) {
switch (stage) {
case ShaderType::Compute: return "CS";
case ShaderType::Vertex: return "VS";
case ShaderType::Local: return "LS";
case ShaderType::TessellationControl: return "HS";
case ShaderType::TessellationEvaluation: return "TES";
case ShaderType::Mesh: return "MS";
case ShaderType::Pixel: return "PS";
default: return "unknown";
}
}
ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOptions& options) {
EXIT_IF(code.empty());
ShaderSource source;
@@ -595,7 +607,7 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
Decoder::DecodeProgram(code, source.decoded);
}
if (source.decoded.has_swap_pc) {
PrepareCallTarget(source, options);
PrepareCallTargets(source, options);
source.code.assign(code.begin(), code.end());
source.decoded.code = source.code;
}
@@ -603,88 +615,128 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
}
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
EXIT_IF(source.unsupported_call_pc != UINT32_MAX);
auto& reads = source.reads;
reads.clear();
IR::SrtReadCapture capture(runtime, reads);
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
IR::SrtWalker walker(source.call_targets, clean);
uint32_t low = 0, high = 0;
const auto& call = source.decoded.instructions[source.call->instruction];
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
std::vector<uint32_t> flat;
const bool walked = IR::WalkSrt(source.call_targets, runtime, flat, &reads);
const auto read_code = [&](uint64_t address, std::span<uint32_t> words) {
if (runtime.read_specialization_memory == nullptr ||
!runtime.read_specialization_memory(runtime.userdata, address, words)) return false;
reads.emplace_back(address, words.size_bytes());
return true;
};
for (auto& site: source.calls) {
uint32_t low = 0, high = 0;
if (!walked || !IR::WalkedValue(source.call_targets, site.target[0], low) ||
!IR::WalkedValue(source.call_targets, site.target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target",
source.decoded.instructions[site.instruction].pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
site.address = address;
}
const auto query_reads = reads.size();
if (source.linked) {
auto& linked = *source.linked;
const auto function = std::span(linked.code).subspan(source.code.size());
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
std::ranges::equal(function, linked.observed_function)) {
return linked.decoded;
bool unchanged = true;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto& function = linked.functions[i];
const auto expected = std::span(linked.code).subspan(function.offset, function.size);
const auto preceding = std::span(source.calls).first(i);
const auto previous = std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address);
if (previous != preceding.end()) {
const auto& observed = linked.functions[previous - preceding.begin()];
if (std::ranges::equal(expected, std::span(linked.code).subspan(observed.offset, observed.size))) continue;
unchanged = false;
break;
}
linked.observed_function.resize(function.size);
if (!read_code(source.calls[i].address, linked.observed_function) ||
!std::ranges::equal(expected, linked.observed_function)) {
unchanged = false;
break;
}
}
if (unchanged) return linked.decoded;
reads.resize(query_reads);
}
ShaderSource::Linked linked {.code = source.code,
.decoded = source.decoded};
ShaderSource::Linked linked {.code = source.code, .decoded = source.decoded};
const auto& last = source.decoded.instructions.back();
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
// PCs follow decoded instructions; its physical code is independently read and owned.
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
for (uint32_t index = 0;;) {
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
std::array<uint32_t, 2> words {};
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(1)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
const auto family = Decoder::GetInstructionFamily(words[0]);
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
family != Decoder::Family::SOPP);
Decoder::Instruction inst;
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count == 2u) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
std::span(words).last(1)))
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
// Keep the caller's data/footer at native offsets. Expand leaf CFGs per call site
// before SSA, so return edges neither require a dispatcher nor merge resource roots.
uint32_t function_pc = last.pc + last.word_count * sizeof(uint32_t);
std::map<uint64_t, std::vector<Decoder::Instruction>> functions;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto& site = source.calls[i];
const auto& call = source.decoded.instructions[site.instruction];
const auto address = source.calls[i].address;
auto [entry, inserted] = functions.try_emplace(address);
auto& function = entry->second;
if (inserted) {
for (uint32_t index = 0;;) {
std::array<uint32_t, Decoder::MaxInstructionRawWords> words {};
EXIT_IF(index > UINT32_MAX / sizeof(uint32_t) - words.size() ||
address > UINT64_MAX - (uint64_t {index} + words.size()) * sizeof(uint32_t));
uint32_t count = 1;
Decoder::Instruction inst;
for (;;) {
if (!read_code(address + uint64_t {index} * sizeof(uint32_t), std::span(words).first(count)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count <= count) break;
count = inst.word_count;
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
function.push_back(inst);
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) break;
}
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
if (returns) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
inst.branch_target += function_pc;
const uint32_t function_size = function.back().pc + function.back().word_count * sizeof(uint32_t);
EXIT_IF(function_pc > UINT32_MAX - function_size);
const auto function_offset = static_cast<uint32_t>(linked.code.size());
for (auto inst: function) {
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg, source.wave_size) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u, source.wave_size));
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
if (site.preserved_registers[code] &&
CFG::MayWriteScalarRegister(inst, code, source.wave_size))
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
}
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target >= function_size);
inst.branch_target += function_pc;
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
if (returns) break;
}
const auto function_end = linked.decoded.instructions.back().pc +
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
const auto& inst = linked.decoded.instructions[i];
if (Decoder::IsDirectBranch(inst.opcode))
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
linked.functions.push_back({function_offset, function_size / 4u});
linked.decoded.instructions[site.instruction].branch_target = function_pc;
function_pc += function_size;
}
reads.resize(query_reads);
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto preceding = std::span(source.calls).first(i);
if (std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address) == preceding.end())
reads.emplace_back(source.calls[i].address, linked.functions[i].size * sizeof(uint32_t));
}
linked.decoded.code = linked.code;
linked.observed_function.resize(linked.code.size() - source.code.size());
linked.observed_function.resize(std::ranges::max_element(linked.functions, {},
&ShaderSource::Linked::Function::size)->size);
source.linked = std::move(linked);
++source.revision;
return source.linked->decoded;
@@ -711,6 +763,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
static_cast<unsigned>(options.stage));
}
const auto* stage = StageName(options.stage);
const auto compile_begin = std::chrono::steady_clock::now();
const auto phase_ms = [&compile_begin]() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
@@ -720,27 +773,25 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
std::string decoded_dump;
if (options.dump_ir) {
decoded_dump = Decoder::ProgramToString(decoded);
if (options.early_dump) {
LOGF("%s decoded RDNA2 (early):\n%s", GetDumpLabel(options), decoded_dump.c_str());
}
LOGF("ShaderRecompiler %s decoded RDNA2:\n%s", stage, decoded_dump.c_str());
}
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph\n", GetDumpLabel(options),
StageName(options.stage), options.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " CFG BuildGraph\n", stage,
options.shader_hash);
auto native_cfg = CFG::BuildGraph(decoded);
CFG::Graph structured_cfg;
auto* selected_cfg = &native_cfg;
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
" loops=%" PRIu64 " back_edges=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
stage, options.shader_hash,
static_cast<uint64_t>(native_cfg.blocks.size()),
static_cast<uint64_t>(native_cfg.natural_loops.size()),
static_cast<uint64_t>(native_cfg.back_edges.size()), phase_ms());
if (native_cfg.irreducible) {
LogDispatcherFallback(options, native_cfg, "build");
} else {
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG Structurize\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " CFG Structurize\n", stage,
options.shader_hash);
structured_cfg = CFG::Structurize(native_cfg);
if (structured_cfg.unsupported) {
native_cfg.unsupported = true;
@@ -750,12 +801,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
LogDispatcherFallback(options, native_cfg, "structurize");
} else {
selected_cfg = &structured_cfg;
LOGF("%s structured CFG success: blocks=%" PRIu64 "\n", GetDumpLabel(options),
LOGF("ShaderRecompiler %s structured CFG success: blocks=%" PRIu64 "\n", stage,
static_cast<uint64_t>(selected_cfg->blocks.size()));
}
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
" loops=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
stage, options.shader_hash,
static_cast<uint64_t>(selected_cfg->blocks.size()),
static_cast<uint64_t>(selected_cfg->natural_loops.size()), phase_ms());
}
@@ -763,12 +814,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
const auto& cfg = *selected_cfg;
Frontend::EmbeddedFetchPlan embedded_fetch;
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
options.input_info.vertex != nullptr && options.input_info.vertex->fetch_embedded) {
options.input_info.vertex->fetch_embedded) {
embedded_fetch = DetectEmbeddedVertexFetch(
decoded, options.input_info.vertex, options.user_data_base,
decoded, *options.input_info.vertex, options.user_data_base,
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
if (!embedded_fetch.loads.empty()) {
LOGF("%s embedded vertex fetch plan: detected=%" PRIu64 "\n", GetDumpLabel(options),
LOGF("ShaderRecompiler %s embedded vertex fetch plan: detected=%" PRIu64 "\n", stage,
static_cast<uint64_t>(embedded_fetch.loads.size()));
}
}
@@ -781,12 +832,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
.input_info = options.input_info,
.embedded_fetch = embedded_fetch.loads.empty() ? nullptr : &embedded_fetch,
};
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " IR TranslateProgram\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " IR TranslateProgram\n", stage,
options.shader_hash);
auto ir = Frontend::TranslateProgram(decoded, cfg, translate_options);
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " IR TranslateProgram blocks=%" PRIu64
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " IR TranslateProgram blocks=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
stage, options.shader_hash,
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
IR::RewriteToSsa(ir.blocks);
const auto simplify = [&ir] {
@@ -797,8 +848,9 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
simplify();
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
if (read_lane_stats.rewritten_reads != 0 || read_lane_stats.rewritten_reductions != 0) {
LOGF("%s read-lane elimination: reads=%" PRIu32 " reductions=%" PRIu32 "\n",
GetDumpLabel(options), read_lane_stats.rewritten_reads, read_lane_stats.rewritten_reductions);
LOGF("ShaderRecompiler %s read-lane elimination: reads=%" PRIu32
" reductions=%" PRIu32 "\n",
stage, read_lane_stats.rewritten_reads, read_lane_stats.rewritten_reductions);
}
// Cleanup also exposes profitable folds by removing dead predicate consumers.
simplify();
@@ -812,10 +864,8 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
std::string cfg_dump;
if (options.dump_ir) {
cfg_dump = CFG::GraphToString(cfg);
if (options.early_dump) {
LOGF("%s native IR before resource tracking:\n%s", GetDumpLabel(options),
MakeIrDump(cfg_dump, ir).c_str());
}
LOGF("ShaderRecompiler %s native IR before resource tracking:\n%s", stage,
MakeIrDump(cfg_dump, ir).c_str());
}
IR::TrackResources(ir, decoded, native_cfg, options.input_info);
TranslateResult result;
@@ -832,6 +882,7 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
uint32_t push_data_start_dword) {
const auto emit_begin = std::chrono::steady_clock::now();
auto& ir = translated.program;
const auto* stage = StageName(ir.stage);
IR::ApplyResourceSpecialization(ir, specialization);
// The resource plan owns host descriptor evaluation now. Keep only dependencies consumed
// by GPU memory operations; bound descriptor dwords must not retain shader instructions.
@@ -878,19 +929,16 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
std::string ir_dump;
if (options.dump_ir) {
ir_dump = MakeIrDump(translated.cfg_dump, ir);
if (options.early_dump) {
LOGF("%s native IR (early):\n%s", GetDumpLabel(options), ir_dump.c_str());
}
LOGF("ShaderRecompiler %s native IR:\n%s", stage, ir_dump.c_str());
}
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword,
options.maintenance8_enabled, options.emit_debug_info);
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,
static_cast<uint64_t>(spirv.size()),
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " SPIR-V EmitProgram\n", stage,
ir.shader_hash);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword, *options.profile,
options.emit_debug_info);
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
" wave_size=%u elapsed_ms=%" PRIu64 "\n",
stage, ir.shader_hash, static_cast<uint64_t>(spirv.size()), ir.wave_size,
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - emit_begin)
.count()));
@@ -3,11 +3,13 @@
#include "common/common.h"
#include "common/stringUtils.h"
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/shader.h"
#include <array>
#include <bitset>
#include <optional>
#include <span>
#include <vector>
@@ -20,10 +22,8 @@ struct CompileOptions {
uint32_t user_data_base = 0;
uint64_t shader_hash = 0;
bool dump_ir = true;
bool early_dump = false;
bool emit_debug_info = false;
bool maintenance8_enabled = false;
const char* dump_label = nullptr;
const Spirv::Profile* profile = &Spirv::DefaultProfile;
std::span<const uint32_t> user_data;
std::span<const uint32_t> back_code;
ShaderStageInputInfo input_info;
@@ -47,21 +47,28 @@ struct ShaderSource {
struct Call {
uint32_t instruction;
std::array<IR::Value, 2> target;
std::bitset<128> preserved_registers;
uint64_t address = 0;
};
struct Linked {
struct Function { uint32_t offset; uint32_t size; };
std::vector<uint32_t> code;
Decoder::Program decoded;
std::vector<Function> functions;
std::vector<uint32_t> observed_function;
};
std::vector<uint32_t> code;
Decoder::Program decoded;
IR::ResourcePlan call_targets;
std::optional<Call> call;
std::vector<Call> calls;
uint32_t unsupported_call_pc = UINT32_MAX;
uint32_t wave_size = 64;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
};
[[nodiscard]] const char* StageName(ShaderType stage);
[[nodiscard]] ShaderSource PrepareShaderSource(std::span<const uint32_t> code,
const CompileOptions& options);
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime);
+177 -97
View File
@@ -21,15 +21,21 @@ struct TessellationAddress {
Kind kind = Kind::Unknown;
uint32_t coefficient = 0;
uint32_t constant = 0;
bool operator==(const TessellationAddress&) const = default;
};
uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
uint32_t control_points, uint32_t input_stride) {
// Reflects the LS output stride, or the HS output stride and patch constant range.
void ReflectTessellationLayout(const Decoder::Program& program, bool local,
ShaderTessellationInputInfo& info) {
using namespace Decoder;
using Address = TessellationAddress;
using Kind = Address::Kind;
std::array<Address, IR::NumVectorRegs> registers {};
registers[local ? 3u : 1u] =
using Address = TessellationAddress;
using Kind = Address::Kind;
using Registers = std::array<Address, IR::NumVectorRegs>;
const auto control_points = local ? info.input_control_points : info.output_control_points;
Registers entry {}, registers;
entry[local ? 3u : 1u] =
local ? Address {Kind::Affine, 1u, 0u} : Address {Kind::PackedControlPoint};
const auto constant = [](uint32_t value) { return Address {Kind::Affine, 0u, value}; };
const auto read = [&](const Operand& operand) -> Address {
@@ -79,78 +85,122 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
? value
: Address {};
};
uint32_t stride = 0;
for (const auto& inst: program.instructions) {
// Stage exits preserve the active path's definitions. An internal join
// would require merging register definitions.
EXIT_NOT_IMPLEMENTED(IsDirectBranch(inst.opcode) &&
inst.branch_target != program.instructions.back().pc);
const bool local_store =
inst.opcode == Opcode::DS_WRITE_B32 || inst.opcode == Opcode::DS_WRITE2_B32;
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
const bool control_store =
!local && buffer_store && inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
const bool control_read =
!local && (inst.opcode == Opcode::DS_READ_B32 || inst.opcode == Opcode::DS_READ2_B32);
if ((local && local_store) || control_store || control_read) {
const auto address = read(inst.src0);
if (address.kind != Kind::Affine) {
EXIT("%s tessellation address is not affine at pc 0x%08x\n", local ? "LS" : "HS",
inst.pc);
}
if (address.coefficient != 0u) {
const auto expected = control_read ? input_stride : stride;
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
(expected != 0u && expected != address.coefficient));
if (!control_read) stride = address.coefficient;
}
// A branch joins its target with the fallthrough path. Definitions that differ between
// the paths are unknown there. Loop headers repeat the walk until back edges are stable.
std::unordered_map<uint32_t, Registers> joins, loops;
const auto merge = [](Registers& into, const Registers& from) {
bool changed = false;
for (uint32_t index = 0; index < into.size(); index++) {
if (into[index] == from[index]) continue;
changed |= into[index] != Address {};
into[index] = {};
}
// Store vdata is a source, despite occupying the decoder's dst field.
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
const auto lhs = read(inst.src0);
const auto rhs = read(inst.src1);
const auto third = read(inst.src2);
Address value;
switch (inst.opcode) {
case Opcode::V_BFE_U32:
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
rhs.coefficient == 0u && third.kind == Kind::Affine &&
third.coefficient == 0u) {
if (rhs.constant == 8u && third.constant == 5u) value = {Kind::Affine, 1u, 0u};
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
return changed;
};
uint32_t stride = 0, patch_begin = UINT32_MAX, patch_end = 0;
for (bool repeat = true; repeat;) {
repeat = false;
registers = entry;
for (const auto& inst: program.instructions) {
if (const auto join = joins.find(inst.pc); join != joins.end()) {
merge(registers, join->second);
joins.erase(join);
}
if (const auto loop = loops.find(inst.pc); loop != loops.end())
merge(registers, loop->second);
if (IsDirectBranch(inst.opcode)) {
const bool back = inst.branch_target <= inst.pc;
const auto [join, inserted] =
(back ? loops : joins).try_emplace(inst.branch_target, registers);
const bool changed = !inserted && merge(join->second, registers);
repeat |= back && (inserted || changed);
}
const bool local_store = IsDsWriteOpcode(inst.opcode);
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
const bool control_store = !local && buffer_store &&
inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
if ((local && local_store) || control_store) {
const auto address = read(inst.src0);
if (address.kind != Kind::Affine) {
EXIT("%s tessellation address is not affine at pc 0x%08x\n",
local ? "LS" : "HS", inst.pc);
}
break;
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
case Opcode::V_MAD_U32_U24: value = add(multiply(low24(lhs), low24(rhs)), third); break;
case Opcode::V_LSHL_ADD_U32:
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
break;
case Opcode::V_LSHLREV_B32:
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
value = multiply(rhs, constant(1u << lhs.constant));
break;
case Opcode::V_SUB_NC_U32:
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
value = affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
break;
default: break;
if (address.coefficient != 0u) {
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
(stride != 0u && stride != address.coefficient));
stride = address.coefficient;
} else if (control_store) {
// Control-point-independent HS outputs are patch constants.
const auto begin = address.constant + inst.offset;
patch_begin = std::min(patch_begin, begin);
patch_end = std::max(patch_end, begin + 4u * inst.data_dwords);
}
}
// Store vdata is a source, despite occupying the decoder's dst field.
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
const auto lhs = read(inst.src0);
const auto rhs = read(inst.src1);
const auto third = read(inst.src2);
Address value;
switch (inst.opcode) {
case Opcode::V_BFE_U32:
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
rhs.coefficient == 0u && third.kind == Kind::Affine &&
third.coefficient == 0u) {
if (rhs.constant == 8u && third.constant == 5u)
value = {Kind::Affine, 1u, 0u};
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
}
break;
case Opcode::V_AND_B32:
if ((lhs.kind == Kind::PackedControlPoint && rhs == constant(0xffu)) ||
(rhs.kind == Kind::PackedControlPoint && lhs == constant(0xffu)))
value = constant(0u);
break;
case Opcode::V_ADD_NC_U32: value = add(lhs, rhs); break;
case Opcode::V_MUL_LO_U32: value = multiply(lhs, rhs); break;
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
case Opcode::V_MAD_U32_U24:
value = add(multiply(low24(lhs), low24(rhs)), third);
break;
case Opcode::V_LSHL_ADD_U32:
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
break;
case Opcode::V_LSHLREV_B32:
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
value = multiply(rhs, constant(1u << lhs.constant));
break;
case Opcode::V_SUB_NC_U32:
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
value =
affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
break;
default: break;
}
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
inst.dst.dpp)
value = {};
for (uint32_t index = 0;
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size();
index++) {
registers[inst.dst.reg + index] = {};
}
registers.at(inst.dst.reg) = value;
}
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
inst.dst.dpp)
value = {};
for (uint32_t index = 0;
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size(); index++) {
registers[inst.dst.reg + index] = {};
}
registers.at(inst.dst.reg) = value;
}
EXIT_NOT_IMPLEMENTED(stride == 0u);
return stride;
EXIT_NOT_IMPLEMENTED(!joins.empty() || stride == 0u);
(local ? info.ls_stride : info.hs_stride) = stride;
if (patch_end != 0u) {
EXIT_NOT_IMPLEMENTED(patch_begin < stride * control_points);
info.patch_base = patch_begin;
info.patch_size = patch_end - patch_begin;
}
}
const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
@@ -161,6 +211,15 @@ const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
: nullptr;
}
// Whether an access predicate, possibly a conjunction of EXEC masks, implies a condition.
bool Implies(IR::Value predicate, IR::Value condition) {
predicate = predicate.Resolve();
if (predicate == condition.Resolve()) return true;
const auto* inst = predicate.TryInstruction();
return inst != nullptr && inst->GetOpcode() == IR::ValueOpcode::LogicalAnd &&
(Implies(inst->Arg(0), condition) || Implies(inst->Arg(1), condition));
}
IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value value,
IR::Value predicate, std::unordered_map<IR::Inst*, IR::Value>& resolved) {
using namespace IR;
@@ -169,18 +228,28 @@ IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value
if (source == nullptr) return value;
if (const auto found = resolved.find(source); found != resolved.end()) return found->second;
if (source->GetOpcode() == ValueOpcode::SelectU32) {
return source->Arg(0).Resolve() == predicate.Resolve()
return Implies(predicate, source->Arg(0))
? ActiveAddress(block, before, source->Arg(1), predicate, resolved)
: value;
}
if (source->GetOpcode() != ValueOpcode::IAdd32 && source->GetOpcode() != ValueOpcode::ISub32)
// Pure address arithmetic that stays behind EXEC selects; mul24 lowers to IMul32 of BFEs.
const auto opcode = source->GetOpcode();
if (opcode != ValueOpcode::IAdd32 && opcode != ValueOpcode::ISub32 &&
opcode != ValueOpcode::IMul32 && opcode != ValueOpcode::BitFieldUExtract)
return value;
const auto lhs = ActiveAddress(block, before, source->Arg(0), predicate, resolved);
const auto rhs = ActiveAddress(block, before, source->Arg(1), predicate, resolved);
if (lhs != source->Arg(0).Resolve() || rhs != source->Arg(1).Resolve()) {
auto copy = block.PrependNewInst(before, source->GetOpcode(), {lhs, rhs},
source->Flags<uint64_t>());
value = Value(&*copy);
std::array<Value, 3> args;
bool changed = false;
for (size_t index = 0; index < source->NumArgs(); index++) {
args[index] = ActiveAddress(block, before, source->Arg(index), predicate, resolved);
changed |= args[index] != source->Arg(index).Resolve();
}
if (changed) {
const auto flags = source->Flags<uint64_t>();
const auto copy =
source->NumArgs() == 2u
? block.PrependNewInst(before, opcode, {args[0], args[1]}, flags)
: block.PrependNewInst(before, opcode, {args[0], args[1], args[2]}, flags);
value = Value(&*copy);
}
resolved.emplace(source, value);
return value;
@@ -193,11 +262,12 @@ void AnalyzeTessellationPrograms(std::span<const uint32_t> local, std::span<cons
const auto local_program = Decoder::DecodeFrontProgram(local);
Decoder::Program control_program;
Decoder::DecodeProgram(control, control_program);
info.ls_stride = ReflectTessellationStride(local_program, true, info.input_control_points, 0u);
info.hs_stride = ReflectTessellationStride(control_program, false, info.output_control_points,
info.ls_stride);
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u\n",
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride);
ReflectTessellationLayout(local_program, true, info);
ReflectTessellationLayout(control_program, false, info);
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u "
"patch_base=0x%x patch_size=%u\n",
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride,
info.patch_base, info.patch_size);
}
void LowerTessellationMemory(IR::Program& program, const CompileOptions& options) {
@@ -218,7 +288,7 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
}
}
ConstantPropagationPass(program.blocks, program.wave_size);
uint32_t reads = 0, writes = 0, factors = 0;
uint32_t reads = 0, writes = 0, factors = 0, patches = 0;
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
auto& inst = *it;
@@ -261,15 +331,22 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
kind = TessellationAttribute::Factor;
factors += components;
} else {
kind = write ? TessellationAttribute::ControlOutput
: TessellationAttribute::EvaluationInput;
EXIT_NOT_IMPLEMENTED(write
? options.stage != ShaderType::TessellationControl
: options.stage != ShaderType::TessellationEvaluation);
// After its barrier, the HS may read back outputs of other control points.
const bool output = options.stage == ShaderType::TessellationControl;
EXIT_NOT_IMPLEMENTED(
!output && (write || options.stage != ShaderType::TessellationEvaluation));
kind = output ? TessellationAttribute::ControlOutput
: TessellationAttribute::EvaluationInput;
if (address.IsImmediate() &&
address.U32() >= tess.hs_stride * tess.output_control_points) {
EXIT_NOT_IMPLEMENTED(!write);
kind = TessellationAttribute::PatchOutput;
const auto begin = address.U32() + memory.offset;
EXIT_NOT_IMPLEMENTED(begin < tess.patch_base ||
begin + 4u * components >
tess.patch_base + tess.patch_size);
kind = output ? TessellationAttribute::PatchOutput
: TessellationAttribute::PatchInput;
// HS read-backs must not hide misplaced patch stores from the check below.
if (write || !output) patches += components;
}
}
} else {
@@ -320,11 +397,14 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
ConstantPropagationPass(program.blocks, program.wave_size);
RemoveIdentities(program);
EliminateDeadCode(program.blocks);
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u\n",
// Patch constant stores whose EXEC selects were not stripped would land in control points.
EXIT_NOT_IMPLEMENTED(options.stage == ShaderType::TessellationControl &&
tess.patch_size != 0u && patches == 0u);
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u patches=%u\n",
options.stage == ShaderType::Local ? "LS"
: options.stage == ShaderType::TessellationControl ? "HS"
: "TES",
reads, writes, factors);
reads, writes, factors, patches);
}
} // namespace Libs::Graphics::ShaderRecompiler
@@ -169,6 +169,9 @@ uint32_t Builder::AddString(const char* text) {
}
void Builder::AddName(uint32_t target, const char* name) {
if (!m_emit_names) {
return;
}
std::vector<uint32_t> operands = {target};
AppendString(operands, name);
AppendInstruction(m_debug, spv::OpName, operands);
@@ -67,6 +67,7 @@ public:
}
uint32_t AddString(const char* text);
void EnableNames() { m_emit_names = true; }
void AddName(uint32_t target, const char* name);
template <typename... Args>
void AddAnnotation(spv::Op opcode, const Args&... operands) {
@@ -159,6 +160,7 @@ private:
std::map<std::string, uint32_t> m_import_ids;
std::map<std::vector<uint32_t>, uint32_t> m_declaration_ids;
size_t m_unpatched_phi_incomings = 0;
bool m_emit_names = false;
};
} // namespace Libs::Graphics::ShaderRecompiler::Spirv
@@ -161,6 +161,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
if (!kind.has_value()) {
Fail(program, "native shader plan has an invalid image class");
}
if (image.heap) continue;
if (image.mip_count == 0u ||
(image.mip_mode != IR::ImageMipMode::Dynamic && image.mip_count != 1u)) {
Fail(program, "native shader plan has an invalid image mip descriptor count");
@@ -204,7 +205,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
} // namespace
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
uint32_t push_data_start_dword, bool maintenance8_enabled,
uint32_t push_data_start_dword, const Profile& profile,
bool emit_debug_info) {
using namespace Emitter;
@@ -220,12 +221,13 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
}
IR::ValidateProgram(program, true);
program.bindings = {.push_data_start_dword = push_data_start_dword};
EmitterState state(program, input_info, maintenance8_enabled);
EmitterState state(program, input_info, profile);
if (emit_debug_info) {
char name[64];
std::snprintf(name, sizeof(name), "rdna2-%016" PRIx64 "-block-byte-pc-plus-one",
program.shader_hash);
state.debug_source = state.builder.AddString(name);
state.builder.EnableNames();
}
const auto* workgroup = ShaderWorkgroupInput(program.stage, input_info);
state.lane_count =
@@ -233,8 +235,8 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
? 2u
: 1u;
DefineModule(state);
ValidateNativeProgram(program, program.stage == ShaderType::Compute &&
input_info.compute != nullptr && input_info.compute->lds_storage);
ValidateNativeProgram(program,
program.stage == ShaderType::Compute && input_info.compute->lds_storage);
EmitProgram(state);
state.builder.AddEntryPoint(ExecutionModelForStage(state.program.stage), state.main_func,
"main", state.interface_variables);
@@ -9,11 +9,21 @@
namespace Libs::Graphics::ShaderRecompiler::Spirv {
// Host device capabilities that change the emitted SPIR-V. Built once per device.
struct Profile {
bool maintenance8 = false; // Dynamic texel offsets on image samples.
bool native_float_min_max = false; // NMin/NMax give the guest's NaN and signed-zero results.
// OpReadClockKHR Device tick in picoseconds; 0 without shaderDeviceClock.
uint32_t device_clock_period_ps = 0;
};
inline constexpr Profile DefaultProfile {};
// Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently.
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
uint32_t push_data_start_dword = 0,
bool maintenance8_enabled = false,
bool emit_debug_info = false);
uint32_t push_data_start_dword = 0,
const Profile& profile = DefaultProfile,
bool emit_debug_info = false);
} // namespace Libs::Graphics::ShaderRecompiler::Spirv
@@ -5,18 +5,6 @@
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
struct Pair {
uint32_t low = 0;
uint32_t high = 0;
};
Pair ExtractPair(EmitterState& state, uint32_t value) {
Pair result {state.builder.AllocateId(), state.builder.AllocateId()};
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), result.low, value, 0);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), result.high, value, 1);
return result;
}
uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto bits_type = TypeU32Vector(state, 2);
const auto lhs_bits = Unary(state, spv::OpBitcast, bits_type, lhs);
@@ -32,11 +20,10 @@ uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max
result = Select(state, TypeF64(state), equal,
Unary(state, spv::OpBitcast, TypeF64(state), equal_bits), result);
// Non-IEEE mode selects the other operand for NaN, including rhs when both are NaN.
result = Select(state, TypeF64(state), Unary(state, spv::OpIsNan, TypeBool(state), rhs),
lhs, result);
return Select(state, TypeF64(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs),
rhs, result);
// Non-IEEE mode selects the other operand for NaN, and lhs when both are NaN. The ordered
// compares already pick rhs for a NaN lhs.
return Select(state, TypeF64(state), Unary(state, spv::OpIsNan, TypeBool(state), rhs),
lhs, result);
}
uint32_t EmitMulHigh(EmitterState& state, uint32_t lhs, uint32_t rhs, bool signed_value) {
@@ -67,9 +54,30 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode,
return result;
}
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
// Clamping first keeps the truncating conversion defined; NaN and values past the largest
// float below the integer limit are selected afterwards.
const auto clamped =
EmitExt(state, TypeF32(state), GLSLstd450NClamp,
{src, ConstantF32(state, signed_value ? 0xcf000000u : 0u),
ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)});
const auto converted = signed_value
? Unary(state, spv::OpBitcast, TypeU32(state),
Unary(state, spv::OpConvertFToS, TypeI32(state), clamped))
: Unary(state, spv::OpConvertFToU, TypeU32(state), clamped);
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u));
const auto high =
Select(state, TypeU32(state), above,
ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted);
return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src),
ConstantU32(state, 0), high);
}
} // namespace
uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits) {
// Native half conversion causes regressions; use compact integer RTZ conversion.
const auto bits = EmitBitcastF32ToU32(state, value);
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
const auto sign = EmitAndConstant(
state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)),
@@ -105,40 +113,6 @@ uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
return EmitOrU32(state, sign, magnitude);
}
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
const auto trunc = EmitTruncF32Value(state, src);
const auto converted_raw = state.builder.AllocateId();
if (signed_value) {
const auto converted_i = state.builder.AllocateId();
state.builder.AddFunction(spv::OpConvertFToS, TypeI32(state), converted_i, trunc);
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), converted_raw, converted_i);
} else {
state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc);
}
const auto nan = EmitClassifyF32(state, src).nan;
if (signed_value) {
const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src,
ConstantF32(state, 0xcf000000u));
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
ConstantF32(state, 0x4f000000u));
const auto high =
Select(state, TypeU32(state), above, ConstantU32(state, 0x7fffffffu), converted_raw);
const auto low =
Select(state, TypeU32(state), below, ConstantU32(state, 0x80000000u), high);
return Select(state, TypeU32(state), nan, ConstantU32(state, 0), low);
}
const auto below =
Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src, ConstantF32(state, 0));
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
ConstantF32(state, 0x4f800000u));
const auto zero = Binary(state, spv::OpLogicalOr, TypeBool(state), nan, below);
const auto high =
Select(state, TypeU32(state), above, ConstantU32(state, 0xffffffffu), converted_raw);
return Select(state, TypeU32(state), zero, ConstantU32(state, 0), high);
}
} // namespace
uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto result = EmitGlsl<GLSLstd450Fma, IR::Type::F32>(state, a, b, c);
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
@@ -160,31 +134,6 @@ uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
return sum;
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
const auto max_ab = EmitMinMaxF32Value(state, a, b, true);
const auto high_min = EmitMinMaxF32Value(state, max_ab, c, false);
const auto median = EmitMinMaxF32Value(state, min_ab, high_min, true);
const auto nan_ab = Binary(state, spv::OpLogicalOr, TypeBool(state),
EmitClassifyF32(state, a).nan, EmitClassifyF32(state, b).nan);
const auto any_nan =
Binary(state, spv::OpLogicalOr, TypeBool(state), nan_ab, EmitClassifyF32(state, c).nan);
return Select(state, TypeF32(state), any_nan, min3, median);
}
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
// GLSL FindUMsb is restricted to 32-bit components.
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
const auto pair =
ExtractPair(state, EmitExt(state, TypeU32Vector(state, 2), GLSLstd450FindUMsb, {unpacked}));
const auto high_found =
Binary(state, spv::OpINotEqual, TypeBool(state), pair.high, ConstantU32(state, UINT32_MAX));
return Select(state, TypeU32(state), high_found,
Binary(state, spv::OpIAdd, TypeU32(state), pair.high, ConstantU32(state, 32)),
pair.low);
}
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0) {
return EmitNative<spv::OpBitwiseAnd, IR::Type::U16>(state, arg0, ConstantU32(state, 0xffffu));
}
@@ -248,18 +197,16 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
}
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
const auto low = EmitF32ToF16RtzBits(state, arg0);
const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16));
return EmitOrU32(state, low, high);
return EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg0)),
EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg1)),
ConstantU32(state, 16), ConstantU32(state, 16));
}
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
clamped);
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0, as NClamp's NMax step does.
return EmitExt(state, TypeF32(state), GLSLstd450NClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
}
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
@@ -270,14 +217,6 @@ uint32_t EmitUMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return EmitMulHigh(state, arg0, arg1, false);
}
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0) {
// Vulkan bit-count operands remain 32-bit, including when counting a U64.
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), arg0);
const auto pair =
ExtractPair(state, Unary(state, spv::OpBitCount, TypeU32Vector(state, 2), unpacked));
return Binary(state, spv::OpIAdd, TypeU32(state), pair.low, pair.high);
}
uint32_t EmitSMinTri32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32_t arg2) {
return EmitSMin32(state, arg0, EmitSMin32(state, arg1, arg2));
}
@@ -354,8 +293,8 @@ uint32_t EmitFPSqrt(EmitterState& state, uint32_t arg0) {
}
uint32_t EmitFPExp2(EmitterState& state, uint32_t arg0) {
return EmitExt(state, TypeF32(state), GLSLstd450Exp2,
{EmitFlushF32DenormToSignedZero(state, arg0)});
// A denormal input already rounds to 1.0, the result for its flushed zero.
return EmitExt(state, TypeF32(state), GLSLstd450Exp2, {arg0});
}
uint32_t EmitFPLog2(EmitterState& state, uint32_t arg0) {
@@ -17,16 +17,9 @@ uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t va
}
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits) {
F32Class cls;
cls.bits = bits;
const auto abs_bits = EmitAndConstant(state, cls.bits, 0x7fffffffu);
cls.nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u);
cls.zero = EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0);
return cls;
}
F32Class EmitClassifyF32(EmitterState& state, uint32_t value) {
return EmitClassifyF32Bits(state, EmitBitcastF32ToU32(state, value));
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
return {EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u),
EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0)};
}
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
@@ -100,75 +93,88 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
return EmitClassMaskBits(state, bits, mask, true);
}
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, uint32_t lhs_bits,
uint32_t rhs_bits, bool max_value) {
// PS5 V_MIN/V_MAX_F32 return the other source for a NaN source (S0 when both are NaN)
// and order -0 below +0. An ordered compare takes S1 for a NaN S0 and, unlike NMin/NMax,
// always returns the exact bits of one source.
const auto either = EmitOrU32(state, lhs_bits, rhs_bits);
const auto both_zero = EmitCompareU32Constant(state, spv::OpIEqual,
EmitAndConstant(state, either, 0x7fffffffu), 0);
const auto zero =
EmitBitcastU32ToF32(state, max_value ? EmitAndU32(state, lhs_bits, rhs_bits) : either);
const auto keep_lhs =
Binary(state, max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
TypeBool(state), lhs, rhs);
const auto ordered = Select(state, TypeF32(state), keep_lhs, lhs, rhs);
const auto numeric = Select(state, TypeF32(state), both_zero, zero, ordered);
return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), rhs), lhs,
numeric);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
if (state.profile.native_float_min_max) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpExtInst, TypeF32(state), result, GlslStd450(state),
max_value ? GLSLstd450NMax : GLSLstd450NMin, lhs, rhs);
return result;
}
return EmitMinMaxF32(state, lhs, rhs, EmitBitcastF32ToU32(state, lhs),
EmitBitcastF32ToU32(state, rhs), max_value);
}
const auto numeric_cond = state.builder.AllocateId();
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
TypeBool(state), numeric_cond, lhs, rhs);
const auto ordered_bits =
EmitSelectValueU32(state, numeric_cond, lhs_class.bits, rhs_class.bits);
const auto both_zero = EmitLogicalAndBool(state, lhs_class.zero, rhs_class.zero);
const auto zero_bits = max_value ? EmitAndU32(state, lhs_class.bits, rhs_class.bits)
: EmitOrU32(state, lhs_class.bits, rhs_class.bits);
const auto numeric_bits = EmitSelectValueU32(state, both_zero, zero_bits, ordered_bits);
const auto rhs_nan_bits =
EmitSelectValueU32(state, rhs_class.nan, lhs_class.bits, numeric_bits);
const auto result_bits = EmitSelectValueU32(state, lhs_class.nan, rhs_class.bits, rhs_nan_bits);
return EmitBitcastU32ToF32(state, result_bits);
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto a_bits = EmitBitcastF32ToU32(state, a);
const auto b_bits = EmitBitcastF32ToU32(state, b);
const auto c_bits = EmitBitcastF32ToU32(state, c);
// The NaN path is RDNA2 MIN3, which keeps the later source when two are NaN.
const auto min_ba = EmitMinMaxF32(state, b, a, b_bits, a_bits, false);
const auto min3 =
EmitMinMaxF32(state, c, min_ba, c_bits, EmitBitcastF32ToU32(state, min_ba), false);
const auto max_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, true);
const auto max3 =
EmitMinMaxF32(state, max_ab, c, EmitBitcastF32ToU32(state, max_ab), c_bits, true);
// RDNA2 compares numerically against MAX3, including ties between signed zeros, then takes
// V_MAX_F32 of the other two sources in their original order.
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
const auto x = Select(state, TypeF32(state), a_is_max, b, a);
const auto y =
Select(state, TypeF32(state), EmitLogicalOrBool(state, a_is_max, b_is_max), c, b);
const auto median = EmitMinMaxF32Value(state, x, y, true);
const auto is_nan = [&](uint32_t value) {
return Unary(state, spv::OpIsNan, TypeBool(state), value);
};
const auto any_nan =
EmitLogicalOrBool(state, EmitLogicalOrBool(state, is_nan(a), is_nan(b)), is_nan(c));
return Select(state, TypeF32(state), any_nan, min3, median);
}
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
const auto bits = state.builder.AllocateId();
const auto abs_bits = state.builder.AllocateId();
const auto sign_bits = state.builder.AllocateId();
const auto subnormal = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto ret = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), abs_bits, bits,
ConstantU32(state, 0x7fffffffu));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), sign_bits, bits,
ConstantU32(state, 0x80000000u));
state.builder.AddFunction(spv::OpULessThan, TypeBool(state), subnormal, abs_bits,
ConstantU32(state, 0x00800000u));
state.builder.AddFunction(spv::OpSelect, TypeU32(state), selected, subnormal, sign_bits, bits);
state.builder.AddFunction(spv::OpBitcast, TypeF32(state), ret, selected);
return ret;
// x * 0 keeps the sign of a denormal; NaN and infinity fail the ordered compare.
const auto magnitude = state.builder.AllocateId();
state.builder.AddFunction(spv::OpExtInst, TypeF32(state), magnitude, GlslStd450(state),
GLSLstd450FAbs, value);
const auto denormal = Binary(state, spv::OpFOrdLessThan, TypeBool(state), magnitude,
ConstantF32(state, 0x00800000u));
return Select(state, TypeF32(state), denormal,
Binary(state, spv::OpFMul, TypeF32(state), value, ConstantF32(state, 0)), value);
}
uint32_t EmitTrigCycleF32(EmitterState& state, uint32_t src, bool preserve_signed_zero) {
const auto fract = state.builder.AllocateId();
const auto bits = state.builder.AllocateId();
const auto abs_bits = state.builder.AllocateId();
const auto large = state.builder.AllocateId();
const auto finite = state.builder.AllocateId();
const auto large_finite = state.builder.AllocateId();
const auto reduced = state.builder.AllocateId();
// Finite values of magnitude 2^23 and above are integers, so fract already gives them +0.
const auto fract = state.builder.AllocateId();
state.builder.AddFunction(spv::OpExtInst, TypeF32(state), fract, GlslStd450(state),
GLSLstd450Fract, src);
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, src);
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), abs_bits, bits,
ConstantU32(state, 0x7fffffffu));
state.builder.AddFunction(spv::OpUGreaterThanEqual, TypeBool(state), large, abs_bits,
ConstantU32(state, 0x4b000000u));
state.builder.AddFunction(spv::OpULessThan, TypeBool(state), finite, abs_bits,
ConstantU32(state, 0x7f800000u));
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), large_finite, large, finite);
state.builder.AddFunction(spv::OpSelect, TypeF32(state), reduced, large_finite,
ConstantF32(state, 0), fract);
if (!preserve_signed_zero) {
return reduced;
return fract;
}
const auto zero = state.builder.AllocateId();
const auto ret = state.builder.AllocateId();
state.builder.AddFunction(spv::OpIEqual, TypeBool(state), zero, abs_bits,
ConstantU32(state, 0));
state.builder.AddFunction(spv::OpSelect, TypeF32(state), ret, zero, src, reduced);
return ret;
const auto abs_bits = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
Unary(state, spv::OpBitcast, TypeU32(state), src),
ConstantU32(state, 0x7fffffffu));
return Select(state, TypeF32(state),
Binary(state, spv::OpIEqual, TypeBool(state), abs_bits, ConstantU32(state, 0)),
src, fract);
}
uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits) {
@@ -30,6 +30,13 @@ bool PixelParameterIsCustom(const EmitterState& state, uint32_t attr) {
ShaderPixelParameterIsCustom(*state.input_info.pixel, attr);
}
uint32_t MrtOutputType(EmitterState& state, uint32_t index) {
const auto& pixel = *state.input_info.pixel;
if ((pixel.target_sint_mask & (1u << index)) != 0u) return TypeI32Vector(state, 4);
if ((pixel.target_uint_mask & (1u << index)) != 0u) return TypeU32Vector(state, 4);
return TypeF32Vector(state, 4);
}
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp) {
if (exp.kind == IR::ExportTargetKind::Position && exp.index == 0) {
return state.per_vertex_variable;
@@ -120,7 +127,14 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image) {
} else {
EXIT("invalid image resource class");
}
if (image.dimension == ImageDimension::Dim1D || image.dimension == ImageDimension::Dim1DArray) {
state.builder.RequireCapability(sampled == 1 ? spv::CapabilitySampled1D
: spv::CapabilityImage1D);
}
const auto& info = ImageDimensionInfoFor(image.dimension);
if (sampled == 2 && info.multisampled != 0u) {
state.builder.RequireCapability(spv::CapabilityStorageImageMultisample);
}
const auto scalar_type =
image.atomic64 ? TypeU64(state) : ImageScalarType(state, image.numeric_class);
return state.builder.Type(spv::OpTypeImage, scalar_type,
@@ -144,14 +158,20 @@ void DecorateNonUniformDescriptor(EmitterState& state, uint32_t id, spv::Capabil
state.builder.RequireCapability(capability);
state.builder.AddAnnotation(spv::OpDecorate, id, spv::DecorationNonUniform);
}
ImageDefinition& ImageDescriptorDefinition(EmitterState& state, const IR::ImageResource& image,
IR::DescriptorBindingKind kind) {
return (image.heap ? state.heap_images : state.images)[IR::ImageBindingIndex(kind)];
}
} // namespace
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip,
uint32_t array_index) {
const auto pointer = ImageDescriptorPointer(state, resource, mip, array_index);
const auto image = state.builder.AllocateId();
const auto kind = *IR::DescriptorBindingForImage(state.program.info.images[resource]);
state.builder.AddFunction(spv::OpLoad, state.images[IR::ImageBindingIndex(kind)].type,
const auto& resource_info = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(resource_info);
state.builder.AddFunction(spv::OpLoad, ImageDescriptorDefinition(state, resource_info, kind).type,
image, pointer);
if (array_index != 0u) {
DecorateNonUniformDescriptor(state, image,
@@ -184,13 +204,13 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl
EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled);
const auto image = LoadImageDescriptor(state, resource, mip, array_index);
const auto sampled_image = state.builder.AllocateId();
const auto kind = *IR::DescriptorBindingForImage(image_resource);
auto& sampled_type = state.images[IR::ImageBindingIndex(kind)].sampled_type;
if (sampled_type == 0) {
sampled_type = state.builder.Type(
spv::OpTypeSampledImage, state.images[IR::ImageBindingIndex(kind)].type);
auto& definition = ImageDescriptorDefinition(state, image_resource,
*IR::DescriptorBindingForImage(image_resource));
if (definition.sampled_type == 0) {
definition.sampled_type = state.builder.Type(spv::OpTypeSampledImage, definition.type);
}
state.builder.AddFunction(spv::OpSampledImage, sampled_type, sampled_image, image, sampler_id);
state.builder.AddFunction(spv::OpSampledImage, definition.sampled_type, sampled_image, image,
sampler_id);
if (array_index != 0u || sampler_dynamic) {
DecorateNonUniformDescriptor(state, sampled_image, spv::CapabilitySampledImageArrayNonUniformIndexing);
}
@@ -202,26 +222,31 @@ uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t
const auto& image = state.program.info.images.at(resource);
EXIT_IF(mip >= image.mip_count);
const auto kind = IR::DescriptorBindingForImage(image);
EXIT_IF(!kind.has_value());
EXIT_IF(!kind.has_value() || (image.heap && array_index == 0u));
if (array_index == 0u) {
array_index = ConstantU32(state, image.descriptor_index + mip);
}
const auto& definition = state.images[IR::ImageBindingIndex(*kind)];
const auto& definition = ImageDescriptorDefinition(state, image, *kind);
return DescriptorElementPointer(state, definition.pointer_type, definition.variable,
array_index, *kind, resource,
"image descriptor array was not emitted");
}
void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord,
uint32_t texel, uint32_t array_index) {
uint32_t texel, uint32_t array_index, uint32_t sample) {
const auto& image = state.program.info.images.at(resource);
EXIT_IF(image.resource_class != IR::ImageResourceClass::Storage);
if (!image.atomic) {
state.builder.RequireCapability(spv::CapabilityStorageImageWriteWithoutFormat);
}
const auto EmitWrite = [&](uint32_t mip) {
state.builder.AddFunction(spv::OpImageWrite, LoadImageDescriptor(state, resource, mip, array_index),
coord, texel);
const auto descriptor = LoadImageDescriptor(state, resource, mip, array_index);
if (sample == 0u) {
state.builder.AddFunction(spv::OpImageWrite, descriptor, coord, texel);
} else {
state.builder.AddFunction(spv::OpImageWrite, descriptor, coord, texel,
spv::ImageOperandsSampleMask, sample);
}
};
if (image.mip_mode != IR::ImageMipMode::Dynamic) {
EmitWrite(0);
@@ -4,6 +4,8 @@
#include <algorithm>
#include <atomic>
#include <bit>
#include <optional>
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
@@ -53,8 +55,7 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeBool(state), value, variable);
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects uint masks or float +/-1.
const bool all_bits =
state.input_info.pixel != nullptr && state.input_info.pixel->ps_front_face_all_bits;
const bool all_bits = state.input_info.pixel->ps_front_face_all_bits;
const auto front = ConstantU32(state, all_bits ? 0xffffffffu : 0x3f800000u);
const auto back = ConstantU32(state, all_bits ? 0u : 0xbf800000u);
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value, front, back);
@@ -62,7 +63,8 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
}
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
kind == IR::StageInputKind::InvocationId || kind == IR::StageInputKind::PrimitiveId ||
kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId) {
kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId ||
kind == IR::StageInputKind::BaseVertex || kind == IR::StageInputKind::BaseInstance) {
const auto value = state.builder.AllocateId();
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable);
@@ -113,53 +115,127 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
return EmitInputComponentU32(state, kind, component);
}
// Whether EXEC enables a source lane within the caller's 32-lane half.
uint32_t EmitSourceLaneActive(ValueEmitContext& ctx, uint32_t exec, uint32_t lane) {
auto& state = ctx.state;
const auto scope = ConstantU32(state, spv::ScopeSubgroup);
const auto ballot = EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(state, scope, exec);
if (ctx.half != 0) {
lane = EmitAndConstant(state, lane, 31);
}
return EmitNative<spv::OpGroupNonUniformBallotBitExtract, IR::Type::U1>(state, scope, ballot,
lane);
}
uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& flags,
uint32_t exec) {
auto& state = ctx.state;
const auto lane = EmitSubgroupLocalInvocationId(state);
const auto bank_shift = state.builder.AllocateId();
const auto row_shift = state.builder.AllocateId();
const auto bank = state.builder.AllocateId();
const auto row = state.builder.AllocateId();
const auto bank_bit = state.builder.AllocateId();
const auto row_bit = state.builder.AllocateId();
const auto bank_hit = state.builder.AllocateId();
const auto row_hit = state.builder.AllocateId();
const auto bank_ok = state.builder.AllocateId();
const auto row_ok = state.builder.AllocateId();
const auto masks_ok = state.builder.AllocateId();
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), bank_shift, lane,
ConstantU32(state, 2));
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), row_shift, lane,
ConstantU32(state, 4));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), bank, bank_shift,
ConstantU32(state, 3));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, row_shift,
ConstantU32(state, 3));
state.builder.AddFunction(spv::OpShiftLeftLogical, TypeU32(state), bank_bit,
ConstantU32(state, 1), bank);
state.builder.AddFunction(spv::OpShiftLeftLogical, TypeU32(state), row_bit,
ConstantU32(state, 1), row);
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), bank_hit,
ConstantU32(state, flags.bank_mask), bank_bit);
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row_hit,
ConstantU32(state, flags.row_mask), row_bit);
state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), bank_ok, bank_hit,
ConstantU32(state, 0));
state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), row_ok, row_hit,
ConstantU32(state, 0));
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), masks_ok, bank_ok, row_ok);
uint32_t writable = masks_ok;
if (!flags.bound_control) {
const auto target = EmitDppTargetLane(state, flags);
const auto bounded = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), bounded, writable,
target.valid);
writable = bounded;
auto& state = ctx.state;
uint32_t writable = exec;
uint32_t lane = 0;
// Each mask bit enables writes to one bank (4 lanes) or row (16 lanes); a full mask enables all.
const auto enable = [&](uint32_t mask, uint32_t group_shift) {
if (mask == 0xfu) return;
lane = lane != 0 ? lane : EmitSubgroupLocalInvocationId(state);
const auto group = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, lane, ConstantU32(state, group_shift), ConstantU32(state, 2));
const auto bit = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, ConstantU32(state, mask), group, ConstantU32(state, 1));
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable,
Binary(state, spv::OpINotEqual, TypeBool(state), bit, ConstantU32(state, 0)));
};
enable(flags.bank_mask, 2u);
enable(flags.row_mask, 4u);
// Only row shifts (0x101-0x10f, 0x111-0x11f) can vacate a lane; other controls always read one.
const bool row_shift = flags.control > 0x100u && flags.control < 0x120u &&
flags.control != 0x110u;
if (flags.bound_control || (flags.fetch_inactive && !row_shift)) {
return writable;
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), result, exec, writable);
return result;
// Without BOUND_CTRL, a vacated source or, without FI, an EXEC-disabled one disables the write.
const auto target = EmitDppTargetLane(state, flags);
if (row_shift) {
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable, target.valid);
}
if (!flags.fetch_inactive) {
// Vacated lanes target outside their row; wrapping keeps the ballot index in range.
auto source = target.lane;
if (row_shift) {
lane = lane != 0 ? lane : EmitSubgroupLocalInvocationId(state);
source = EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, lane, source, ConstantU32(state, 0), ConstantU32(state, 4));
}
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable,
EmitSourceLaneActive(ctx, exec, source));
}
return writable;
}
uint32_t LoadVertexParameter(EmitterState& state, const InputBinding& input, uint32_t vertex,
uint32_t chan) {
const auto pointer = state.builder.AllocateId();
const auto value = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer,
input.variable_id, ConstantU32(state, vertex),
ConstantU32(state, chan & 3u));
state.builder.AddFunction(spv::OpLoad, TypeF32(state), value, pointer);
return value;
}
// Interpolates a pixel input channel to f32. A half (0 low, 1 high) selects one f16 of a packed
// FP16_INTERP_MODE dword before blending; smooth inputs read by halves are always per-vertex.
uint32_t EmitPixelAttribute(EmitterState& state, const InputBinding& input, uint32_t attr,
uint32_t chan, std::optional<uint32_t> half) {
const auto select = [&](uint32_t value) {
if (!half) {
return value;
}
const auto bits = state.builder.AllocateId();
const auto unpacked = state.builder.AllocateId();
const auto word = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 2), unpacked,
GlslStd450(state), GLSLstd450UnpackHalf2x16, bits);
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), word, unpacked, *half);
return word;
};
if (!input.per_vertex) {
const auto vector = state.builder.AllocateId();
const auto component = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeF32Vector(state, 4), vector, input.variable_id);
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), component, vector,
chan & 3u);
return select(component);
}
if (PixelParameterIsFlat(state, attr)) {
const auto vertex =
state.input_info.pixel->parameter_mode == ShaderPixelParameterMode::LastVertex ? 2u : 0u;
return select(LoadVertexParameter(state, input, vertex, chan));
}
const auto barycentric_kind = state.input_info.pixel->ps_no_perspective
? IR::StageInputKind::BaryCoordNoPerspective
: IR::StageInputKind::BaryCoordSmooth;
const auto barycentric = InputVariableForKind(state, barycentric_kind);
uint32_t sum = 0;
for (uint32_t vertex = 0; vertex < 3u; vertex++) {
const auto pointer = state.builder.AllocateId();
const auto weight = state.builder.AllocateId();
const auto product = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassInput, TypeF32(state)),
pointer, barycentric, ConstantU32(state, vertex));
state.builder.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer);
state.builder.AddFunction(spv::OpFMul, TypeF32(state), product,
select(LoadVertexParameter(state, input, vertex, chan)), weight);
if (vertex == 0u) {
sum = product;
} else {
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFAdd, TypeF32(state), next, sum, product);
sum = next;
}
}
return sum;
}
uint32_t EmitAttribute(EmitterState& state, uint32_t attr, uint32_t chan) {
@@ -170,57 +246,9 @@ uint32_t EmitAttribute(EmitterState& state, uint32_t attr, uint32_t chan) {
if (state.program.stage == ShaderType::Vertex || state.program.stage == ShaderType::Local) {
return EmitVertexParameterComponentU32(state, *input, chan & 3u);
}
const auto load_per_vertex = [&](uint32_t vertex) {
const auto pointer = state.builder.AllocateId();
const auto value = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer,
input->variable_id, ConstantU32(state, vertex), ConstantU32(state, chan & 3u));
state.builder.AddFunction(spv::OpLoad, TypeF32(state), value, pointer);
return value;
};
if (input->per_vertex) {
if (PixelParameterIsFlat(state, attr)) {
const auto value = load_per_vertex(
state.input_info.pixel->parameter_mode == ShaderPixelParameterMode::LastVertex ? 2u : 0u);
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
const auto barycentric_kind = state.input_info.pixel->ps_no_perspective
? IR::StageInputKind::BaryCoordNoPerspective
: IR::StageInputKind::BaryCoordSmooth;
const auto barycentric = InputVariableForKind(state, barycentric_kind);
uint32_t sum = 0;
for (uint32_t vertex = 0; vertex < 3u; vertex++) {
const auto pointer = state.builder.AllocateId();
const auto weight = state.builder.AllocateId();
const auto product = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassInput, TypeF32(state)),
pointer, barycentric, ConstantU32(state, vertex));
state.builder.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer);
state.builder.AddFunction(spv::OpFMul, TypeF32(state), product, load_per_vertex(vertex),
weight);
if (vertex == 0u) {
sum = product;
} else {
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFAdd, TypeF32(state), next, sum, product);
sum = next;
}
}
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, sum);
return bits;
}
const auto vector = state.builder.AllocateId();
const auto component = state.builder.AllocateId();
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeF32Vector(state, 4), vector, input->variable_id);
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), component, vector,
chan & 3u);
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, component);
const auto value = EmitPixelAttribute(state, *input, attr, chan, std::nullopt);
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
@@ -231,25 +259,16 @@ uint32_t EmitInterpolationParameter(ValueEmitContext& ctx, uint32_t attr, uint32
if (!input->per_vertex) {
return EmitAttribute(ctx.state, attr, chan);
}
const auto load_vertex = [&](uint32_t vertex) {
const auto pointer = state.builder.AllocateId();
const auto value = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer,
input->variable_id, ConstantU32(state, vertex), ConstantU32(state, chan & 3u));
state.builder.AddFunction(spv::OpLoad, TypeF32(state), value, pointer);
return value;
};
uint32_t selected_vertex = (mode + 1u) % 3u;
if (mode == 2u && PixelParameterIsFlat(state, attr)) {
selected_vertex =
state.input_info.pixel->parameter_mode == ShaderPixelParameterMode::LastVertex ? 2u : 0u;
}
uint32_t value = load_vertex(selected_vertex);
uint32_t value = LoadVertexParameter(state, *input, selected_vertex, chan);
if (!PixelParameterIsCustom(state, attr) && mode < 2u) {
const auto delta = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFSub, TypeF32(state), delta, value, load_vertex(0));
state.builder.AddFunction(spv::OpFSub, TypeF32(state), delta, value,
LoadVertexParameter(state, *input, 0, chan));
value = delta;
}
const auto bits = state.builder.AllocateId();
@@ -273,9 +292,10 @@ uint32_t ExportRawComponent(ValueEmitContext& ctx, uint32_t vector, uint32_t com
}
uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp,
bool uint_output) {
auto& state = ctx.state;
if (exp.compr && !uint_output) {
uint32_t vector_type) {
auto& state = ctx.state;
const bool integer_output = vector_type != TypeF32Vector(state, 4);
if (exp.compr && !integer_output) {
auto unpack = GLSLstd450UnpackHalf2x16;
switch (MrtOutputMode(state, exp)) {
case 5: unpack = GLSLstd450UnpackUnorm2x16; break;
@@ -312,9 +332,11 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
ConstantU32(state, 0),
ConstantU32(state, 0),
ConstantU32(state, 0),
ConstantU32(state, uint_output ? 1u : 0x3f800000u),
ConstantU32(state, integer_output ? 1u : 0x3f800000u),
};
if (exp.compr) {
const auto extract = vector_type == TypeU32Vector(state, 4) ? spv::OpBitFieldUExtract
: spv::OpBitFieldSExtract;
for (uint32_t pair = 0; pair < 2u; pair++) {
if ((exp.en & (3u << (pair * 2u))) == 0u) {
continue;
@@ -326,8 +348,8 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
continue;
}
raw[component] = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), raw[component],
packed, ConstantU32(state, lane * 16u),
state.builder.AddFunction(extract, TypeU32(state), raw[component], packed,
ConstantU32(state, lane * 16u),
ConstantU32(state, 16));
}
}
@@ -342,12 +364,42 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), data,
raw[0], raw[1], raw[2], raw[3]);
}
if (uint_output) {
if (vector_type == TypeU32Vector(state, 4)) {
return data;
}
const auto vector = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeF32Vector(state, 4), vector, data);
return vector;
return Unary(state, spv::OpBitcast, vector_type, data);
}
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
if (state.program.stage == ShaderType::Mesh) {
return MeshOutputPointer(state, kind, index);
}
const auto variable = kind == IR::StageOutputKind::ClipDistance ? state.clip_distance_variable
: state.cull_distance_variable;
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer,
variable, ConstantU32(state, index));
return pointer;
}
// The clipper keeps the half space where dot(plane, position) >= 0 for each UCP_ENA plane.
void EmitUserClipDistances(EmitterState& state, uint32_t position) {
uint32_t index = 0;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
const auto dword = IR::PushData::UserClipPlaneDword + 4u * std::countr_zero(mask);
uint32_t components[4] {};
for (uint32_t i = 0; i < 4; i++) {
components[i] = Unary(state, spv::OpBitcast, TypeF32(state),
EmitPushDataLoad(state, ConstantU32(state, dword + i)));
}
const auto plane = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), plane,
components[0], components[1], components[2], components[3]);
state.builder.AddFunction(
spv::OpStore, DistancePointer(state, IR::StageOutputKind::ClipDistance, index++),
Binary(state, spv::OpDot, TypeF32(state), plane, position));
}
}
void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp) {
@@ -390,23 +442,13 @@ void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::Expor
state.builder.AddFunction(spv::OpStore, state.point_size_variable, f32);
continue;
}
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t variable, uint32_t index) {
if (index == UINT32_MAX) {
return;
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t index) {
if (index != UINT32_MAX) {
state.builder.AddFunction(spv::OpStore, DistancePointer(state, kind, index), f32);
}
uint32_t pointer;
if (state.program.stage == ShaderType::Mesh) {
pointer = MeshOutputPointer(state, kind, index);
} else {
pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
pointer, variable, ConstantU32(state, index));
}
state.builder.AddFunction(spv::OpStore, pointer, f32);
};
StoreDistance(IR::StageOutputKind::ClipDistance, state.clip_distance_variable, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, state.cull_distance_variable, output.cull_distance);
StoreDistance(IR::StageOutputKind::ClipDistance, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, output.cull_distance);
}
}
@@ -523,11 +565,11 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
if (state.program.stage != ShaderType::Mesh && variable == 0) {
return;
}
const bool uint_output = MrtOutputMode(state, exp) == 7u;
const auto vector_type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
auto value = ExportVector(ctx, data, exp, uint_output);
const auto vector_type = MrtOutputMode(state, exp) != 0u ? MrtOutputType(state, exp.index)
: TypeF32Vector(state, 4);
auto value = ExportVector(ctx, data, exp, vector_type);
if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt &&
exp.index == 0 && !uint_output &&
exp.index == 0 && vector_type == TypeF32Vector(state, 4) &&
state.input_info.pixel->alpha_blend_source != ShaderAlphaBlendSource::None) {
// Broadcast logical alpha before swizzling the primary output.
const auto blend_output =
@@ -597,6 +639,9 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.input_info.vertex->clip_space.enabled) {
value = ConvertPositionToClipSpace(state, value);
}
if (exp.kind == IR::ExportTargetKind::Position && state.user_clip_planes != 0) {
EmitUserClipDistances(state, value);
}
if (state.program.stage == ShaderType::Mesh) {
const auto kind = exp.kind == IR::ExportTargetKind::Position
? IR::StageOutputKind::Position
@@ -609,7 +654,6 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto equal = state.builder.AllocateId();
const auto invalid = state.builder.AllocateId();
const auto distance = state.builder.AllocateId();
const auto distance_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFOrdEqual, TypeBoolVector(state, 4), equal,
value, zero);
state.builder.AddFunction(spv::OpAll, TypeBool(state), invalid, equal);
@@ -618,11 +662,10 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.builder.AddFunction(spv::OpSelect, TypeF32(state), distance, invalid,
ConstantF32Value(state, -1.0f),
ConstantF32Value(state, 0.0f));
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
distance_pointer, state.clip_distance_variable,
ConstantU32(state, state.invalid_position_clip_distance));
state.builder.AddFunction(spv::OpStore, distance_pointer, distance);
state.builder.AddFunction(spv::OpStore,
DistancePointer(state, IR::StageOutputKind::ClipDistance,
state.invalid_position_clip_distance),
distance);
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
@@ -655,6 +698,14 @@ void EmitStoreCompletion(EmitterState& state) {
spv::MemorySemanticsImageMemoryMask));
}
void EmitDataCacheInvalidate(EmitterState& state) {
// RDNA2 GL0/GL1 invalidation makes vector memory visible without a workgroup rendezvous.
state.builder.AddFunction(
spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeDevice),
ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsUniformMemoryMask |
spv::MemorySemanticsImageMemoryMask));
}
void EmitBarrier(EmitterState& state) {
const auto tessellation = state.program.stage == ShaderType::TessellationControl;
if (!tessellation && ShaderWorkgroupInput(state.program.stage, state.input_info) == nullptr) {
@@ -662,9 +713,16 @@ void EmitBarrier(EmitterState& state) {
return;
}
const auto memory_scope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup;
const auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
? spv::MemorySemanticsUniformMemoryMask
: spv::MemorySemanticsWorkgroupMemoryMask;
auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
? spv::MemorySemanticsUniformMemoryMask
: spv::MemorySemanticsWorkgroupMemoryMask;
// Shaders read other waves' buffer and image stores with plain loads right after S_BARRIER.
const auto written = [](const auto& resource) { return resource.written; };
if (std::ranges::any_of(state.program.info.buffers, written) ||
std::ranges::any_of(state.program.info.images, written)) {
memory = memory | spv::MemorySemanticsUniformMemoryMask |
spv::MemorySemanticsImageMemoryMask;
}
const auto semantics = tessellation ? spv::MemorySemanticsMaskNone
: spv::MemorySemanticsAcquireReleaseMask | memory;
state.builder.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup),
@@ -762,6 +820,13 @@ uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
return ctx.Ballot(predicate);
}
uint32_t EmitLiveMask(EmitterState& state) {
const auto helper =
EmitNative<spv::OpLoad, IR::Type::U1>(state, state.helper_invocation_variable);
return EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(
state, ConstantU32(state, spv::ScopeSubgroup), EmitLogicalNotBool(state, helper));
}
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto ballot = ctx.Ballot(inst.Arg(1));
const auto lane = ctx.FirstLane(ballot);
@@ -772,18 +837,28 @@ uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst) {
return ctx.Shuffle(inst, 0, ctx.Arg(inst, 1));
}
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
template <spv::Op Reduce, auto Combine>
static uint32_t EmitSubgroupReduceU32(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
auto value = ctx.Arg(inst, 0);
if (ctx.other_half != nullptr) {
value = EmitUMin32(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
value = Combine(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpGroupNonUniformUMin, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce, value);
state.builder.AddFunction(Reduce, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce,
value);
return result;
}
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMin, EmitUMin32>(ctx, inst);
}
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMax, EmitUMax32>(ctx, inst);
}
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto hit = state.builder.AllocateId();
@@ -794,27 +869,23 @@ uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
}
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto flags = inst.Flags<IR::PermlaneFlags>();
const auto subid = EmitSubgroupLocalInvocationId(state);
const auto row = state.builder.AllocateId();
const auto row_value = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto lane8 = state.builder.AllocateId();
const auto shift = state.builder.AllocateId();
const auto upper = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto shifted = state.builder.AllocateId();
const auto index = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
auto& state = ctx.state;
const auto flags = inst.Flags<IR::PermlaneFlags>();
const auto subid = EmitSubgroupLocalInvocationId(state);
const auto row = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto lane8 = state.builder.AllocateId();
const auto shift = state.builder.AllocateId();
const auto upper = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto shifted = state.builder.AllocateId();
const auto index = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
ConstantU32(state, 0xfffffff0u));
if (flags.x16) {
state.builder.AddFunction(spv::OpBitwiseXor, TypeU32(state), row_value, row,
ConstantU32(state, 16));
} else {
state.builder.AddFunction(spv::OpCopyObject, TypeU32(state), row_value, row);
}
const auto row_value =
flags.x16 ? Binary(state, spv::OpBitwiseXor, TypeU32(state), row, ConstantU32(state, 16))
: row;
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane, subid,
ConstantU32(state, 15));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane8, lane,
@@ -830,20 +901,23 @@ uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst) {
ConstantU32(state, 15));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row_value, index);
const auto shuffled = ctx.Shuffle(inst, 0, target);
uint32_t result = shuffled;
if (!flags.fetch_inactive) {
const auto source_exec = ctx.Shuffle(inst, 3, target);
result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), result, source_exec, shuffled,
ConstantU32(state, 0));
if (flags.fetch_inactive) {
return shuffled;
}
return result;
return Select(state, TypeU32(state), EmitSourceLaneActive(ctx, ctx.Arg(inst, 3), target),
shuffled, ctx.Arg(inst, 4));
}
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitAttribute(ctx.state, inst.Arg(0).U32(), inst.Arg(1).U32());
}
uint32_t EmitGetAttributeF16(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto attr = inst.Arg(0).U32();
return EmitPixelAttribute(ctx.state, *InputBindingForParameter(ctx.state, attr), attr,
inst.Arg(1).U32(), inst.Arg(2).U32());
}
uint32_t EmitGetInterpolationParameter(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitInterpolationParameter(ctx, inst.Arg(0).U32(), inst.Arg(1).U32(), inst.Arg(2).U32());
}
@@ -854,6 +928,22 @@ uint32_t EmitGetShaderBase(ValueEmitContext& ctx) {
return ctx.Def(IR::Value(uint64_t {0}));
}
uint32_t EmitReadRealtimeClock(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto period = state.profile.device_clock_period_ps;
if (period == 0u) {
ctx.Fail(inst, "requires shaderDeviceClock");
}
state.builder.RequireExtension("SPV_KHR_shader_clock");
state.builder.RequireCapability(spv::CapabilityShaderClockKHR);
const auto clock =
Unary(state, spv::OpReadClockKHR, TypeU64(state), ConstantU32(state, spv::ScopeDevice));
// S_MEMREALTIME counts at 100 MHz. Shift the host clock by the most bits that keep its tick at
// most 15000 ps (2/3 of 100 MHz): 1 GHz shifts by 3 to 125 MHz; 100 MHz and slower stay.
const uint32_t shift = std::bit_width(7500u / period);
return EmitShiftRightLogical64(ctx, clock, IR::Value(shift));
}
void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Fail(inst, "must be lowered before SPIR-V emission");
}
@@ -4,25 +4,16 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
DppTargetLane EmitDppPermTargetLane(EmitterState& state, uint32_t subid, uint32_t control,
uint32_t lane_bits) {
const auto lane_mask = (1u << lane_bits) - 1u;
const auto group_base = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto shift = state.builder.AllocateId();
const auto selected0 = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), group_base, subid,
ConstantU32(state, ~lane_mask));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane, subid,
ConstantU32(state, lane_mask));
state.builder.AddFunction(spv::OpIMul, TypeU32(state), shift, lane,
ConstantU32(state, lane_bits));
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), selected0,
ConstantU32(state, control), shift);
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), selected, selected0,
ConstantU32(state, lane_mask));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, group_base, selected);
return {target, ConstantBool(state, true)};
// Each lane of a group takes its lane_bits-wide source index from control.
const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), subid,
ConstantU32(state, (1u << lane_bits) - 1u));
const auto selected = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, ConstantU32(state, control),
Binary(state, spv::OpIMul, TypeU32(state), lane, ConstantU32(state, lane_bits)),
ConstantU32(state, lane_bits));
return {EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, subid, selected, ConstantU32(state, 0), ConstantU32(state, lane_bits)),
ConstantBool(state, true)};
}
DppTargetLane EmitDppRowShiftTargetLane(EmitterState& state, uint32_t subid, uint32_t amount,
@@ -53,42 +44,19 @@ DppTargetLane EmitDppRowShiftTargetLane(EmitterState& state, uint32_t subid, uin
DppTargetLane EmitDppRowRotateRightTargetLane(EmitterState& state, uint32_t subid,
uint32_t amount) {
const auto row = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto in_high = state.builder.AllocateId();
const auto minus = state.builder.AllocateId();
const auto plus = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
ConstantU32(state, 0xfffffff0u));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane, subid,
ConstantU32(state, 15));
state.builder.AddFunction(spv::OpUGreaterThanEqual, TypeBool(state), in_high, lane,
ConstantU32(state, amount));
state.builder.AddFunction(spv::OpISub, TypeU32(state), minus, lane, ConstantU32(state, amount));
state.builder.AddFunction(spv::OpIAdd, TypeU32(state), plus, lane,
ConstantU32(state, 16u - amount));
state.builder.AddFunction(spv::OpSelect, TypeU32(state), selected, in_high, minus, plus);
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row, selected);
return {target, ConstantBool(state, true)};
// Rotation wraps within the row, so only the low four lane bits change.
const auto rotated = Binary(state, spv::OpISub, TypeU32(state), subid, ConstantU32(state, amount));
return {EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(state, subid, rotated,
ConstantU32(state, 0),
ConstantU32(state, 4)),
ConstantBool(state, true)};
}
DppTargetLane EmitDppMirrorTargetLane(EmitterState& state, uint32_t subid, bool half_row) {
const auto base_mask = half_row ? 0xfffffff8u : 0xfffffff0u;
const auto lane_mask = half_row ? 7u : 15u;
const auto base = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto mirrored = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), base, subid,
ConstantU32(state, base_mask));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane, subid,
ConstantU32(state, lane_mask));
state.builder.AddFunction(spv::OpISub, TypeU32(state), mirrored, ConstantU32(state, lane_mask),
lane);
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, base, mirrored);
return {target, ConstantBool(state, true)};
// Mirroring lane i of a row or half row reads lane (size - 1 - i), which flips its low bits.
return {Binary(state, spv::OpBitwiseXor, TypeU32(state), subid,
ConstantU32(state, half_row ? 7u : 15u)),
ConstantBool(state, true)};
}
DppTargetLane EmitDppTargetLane(EmitterState& state, const IR::DppMoveFlags& flags) {
@@ -117,13 +85,10 @@ DppTargetLane EmitDppTargetLane(EmitterState& state, const IR::DppMoveFlags& fla
}
// row_share:N reads lane N of the caller's row of 16.
if (control >= 0x150u && control <= 0x15fu) {
const auto row = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
ConstantU32(state, 0xfffffff0u));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row,
ConstantU32(state, control & 0xfu));
return {target, ConstantBool(state, true)};
return {EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, subid, ConstantU32(state, control & 0xfu), ConstantU32(state, 0),
ConstantU32(state, 4)),
ConstantBool(state, true)};
}
if (control >= 0x160u && control <= 0x16fu) {
const auto target = state.builder.AllocateId();
@@ -38,7 +38,8 @@ bool HasFlag(const IR::MemoryInfo& mem, uint32_t flag) {
uint32_t AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
uint32_t component) {
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component);
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
mem.image_dimension, component);
const auto packed = layout.bit_offset / 32u;
if (packed >= address.NumArgs()) return ConstantU32(ctx.state, 0);
auto value = ctx.Def(address.Arg(packed));
@@ -55,10 +56,11 @@ uint32_t AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::
uint32_t AddressF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
uint32_t component) {
const auto value = AddressU32(ctx, mem, address, component);
return Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component).bit_width == 16u
? EmitF16BitsToF32(ctx.state, value)
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
const auto value = AddressU32(ctx, mem, address, component);
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
mem.image_dimension, component);
return layout.bit_width == 16u ? EmitF16BitsToF32(ctx.state, value)
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
}
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
@@ -77,6 +79,7 @@ ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
layout.coord = cursor;
cursor += info.coordinate_components;
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) layout.lod = cursor++;
if (HasFlag(mem, Decoder::ImageSampleFlagLodClamp)) layout.clamp = cursor++;
return layout;
}
@@ -97,10 +100,14 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
ConstantF32Value(state, 1.0f / 256.0f));
};
const auto sampler_control = ctx.Arg(inst, 6);
const auto mip_filter = Extract(sampler_control, 26, 2);
const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state),
Extract(sampler_control, 28, 1), ConstantU32(state, 0));
const auto half = ConstantF32Value(state, 0.5f);
const auto zero = ZeroF32(state);
// Point selects the nearest mip; linear gathers return the lower, primary mip only.
const auto rounding = Select(state, TypeF32(state),
Binary(state, spv::OpIEqual, TypeBool(state), mip_filter, ConstantU32(state, 1)),
ConstantF32Value(state, 0.5f), zero);
const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state),
Extract(ctx.Arg(inst, 4), 12, 4));
const auto minimum = Binary(state, spv::OpFSub, TypeF32(state),
@@ -108,19 +115,19 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
const auto last = ConstantF32Value(state, static_cast<float>(mip_count - 1u));
auto lod = Binary(state, spv::OpFAdd, TypeF32(state),
AddressF32(ctx, mem, address, layout.lod),
Select(state, TypeF32(state), preclamp, half, zero));
Select(state, TypeF32(state), preclamp, rounding, zero));
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12));
// T# minimum LOD is in physical mip space and applies after the S# view-relative clamp.
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, minimum);
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(state, lod, zero, last);
lod = Binary(state, spv::OpFAdd, TypeF32(state), lod,
Select(state, TypeF32(state), preclamp, zero, half));
Select(state, TypeF32(state), preclamp, zero, rounding));
// The clamped value is nonnegative, so conversion performs the integer mip rounding.
const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod),
ConstantU32(state, mip_count - 1u));
const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state),
Extract(sampler_control, 26, 2), ConstantU32(state, 0));
mip_filter, ConstantU32(state, 0));
return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip);
}
@@ -284,10 +291,10 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
}
uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
const IR::Inst& address) {
const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension;
const IR::Inst& address, uint32_t resource, uint32_t array_index) {
const auto dimension = ctx.state.program.info.images.at(resource).dimension;
const auto& info = ImageDimensionInfoFor(dimension);
const auto image = LoadImageDescriptor(ctx.state, mem.resource);
const auto image = LoadImageDescriptor(ctx.state, resource, 0u, array_index);
const auto size = ctx.state.builder.AllocateId();
if (info.multisampled != 0u) {
ctx.state.builder.AddFunction(spv::OpImageQuerySize,
@@ -385,6 +392,7 @@ Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
const auto format = image.conversion_format;
if (format == Prospero::BufferFormat::kInvalid) return {};
const auto info = Format::GetFormatInfo(format);
if (IR::ReadsDepthBits(image)) return info;
const bool raw_signed = IR::RawSignedStorageFormat(image, format) != Prospero::BufferFormat::kInvalid;
EXIT_IF((Prospero::RemapTextureFormat(format) == format && !raw_signed) || info.component_count == 0u ||
info.component_count > 4u);
@@ -420,6 +428,7 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
const auto numeric_class = ctx.state.program.info.images[mem.resource].numeric_class;
const auto scalar_type = ImageScalarType(ctx.state, numeric_class);
const bool integer = info.packed_bitfield || info.type == Format::ComponentType::Uint;
uint32_t packed = 0;
if (info.packed_bitfield) {
packed = ctx.state.builder.AllocateId();
@@ -437,8 +446,16 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type,
components[component], texel, component);
// UNorm backing preserves the guest's filtering; scaling precedes swizzle constants.
const auto unorm_max = static_cast<float>((1u << info.component_bits[component]) - 1u);
components[component] = Binary(ctx.state, spv::OpFMul, scalar_type, components[component],
ConstantF32Value(ctx.state, 255.0f));
ConstantF32Value(ctx.state, unorm_max));
if (integer) {
// Depth bits: round back to the stored UNorm code.
components[component] =
Unary(ctx.state, spv::OpConvertFToU, TypeU32(ctx.state),
Binary(ctx.state, spv::OpFAdd, scalar_type, components[component],
ConstantF32Value(ctx.state, 0.5f)));
}
}
}
for (uint32_t component = info.component_count; component < 4u; component++) {
@@ -450,20 +467,19 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
for (uint32_t component = 0; component < 4u; component++) {
const auto selector = (swizzle >> (component * 3u)) & 7u;
if (selector == 1u) {
selected[component] = info.packed_bitfield
selected[component] = integer
? ConstantU32(ctx.state, Format::FormattedConstantBits(info, Format::FormattedSourceKind::One))
: ConstantF32Value(ctx.state, 1.0f);
} else if (selector >= 4u) {
selected[component] = components[selector - 4u];
} else {
selected[component] = info.packed_bitfield ? ConstantU32(ctx.state, 0u)
: ZeroF32(ctx.state);
selected[component] = integer ? ConstantU32(ctx.state, 0u) : ZeroF32(ctx.state);
}
}
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeConstruct,
ImageVectorType(ctx.state, numeric_class, 4), result, selected[0],
selected[1], selected[2], selected[3]);
integer ? TypeU32Vector(ctx.state, 4) : TypeF32Vector(ctx.state, 4),
result, selected[0], selected[1], selected[2], selected[3]);
return info.type == Format::ComponentType::Unorm
? Unary(ctx.state, spv::OpBitcast, TypeF32Vector(ctx.state, 4), result) : result;
}
@@ -687,70 +703,75 @@ auto EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t resul
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
}
if (image.heap) {
const auto entry = EmitImageHeapEntry(state, key, image.indirect_mapping_offset);
const auto kinds = static_cast<uint32_t>(image.indirect_resources.size()) - 1u;
// One kind leaves every slot word a bare element.
if (kinds == 1u) return emit(image.indirect_resources[1], entry);
const auto element = Binary(state, spv::OpBitwiseAnd, TypeU32(state), entry,
ConstantU32(state, (1u << IR::ImageHeapKindShift) - 1u));
const auto EmitKind = [&](uint32_t kind) {
return emit(image.indirect_resources[kind + 1u], element);
};
return EmitIndexSwitch(state,
Binary(state, spv::OpShiftRightLogical, TypeU32(state), entry,
ConstantU32(state, IR::ImageHeapKindShift)),
kinds, result_type, EmitKind);
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct ImageRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
const auto SameType = [&](uint32_t lhs, uint32_t rhs) {
const auto& first = state.program.info.images[lhs];
const auto& second = state.program.info.images[rhs];
return first.dimension == second.dimension && first.cube == second.cube &&
IR::DescriptorBindingForImage(first) == IR::DescriptorBindingForImage(second) &&
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
first.shader_swizzle == second.shader_swizzle) &&
first.mip_count == 1u && second.mip_count == 1u;
};
std::vector<ImageRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
candidate.shader_swizzle == first.shader_swizzle) &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
// Descriptor order is unrelated to image type. Share each typed access and
// select its descriptor slot independently, including gaps between native roots.
struct ImageGroup { uint32_t resource; uint32_t count; };
std::vector<ImageGroup> groups;
std::vector<uint32_t> ordinal_groups;
for (const auto resource: image.indirect_resources) {
const auto found = std::ranges::find_if(groups, [&](const ImageGroup& group) {
return SameType(resource, group.resource);
});
ordinal_groups.push_back(static_cast<uint32_t>(found - groups.begin()));
if (found == groups.end()) groups.push_back({resource, 1u});
else ++found->count;
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return emit(run.resource, 0u);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return emit(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const bool dynamic_slot = std::ranges::any_of(groups, [](const ImageGroup& group) {
return group.count > 1u;
});
auto group_index = ConstantU32(state, 0u);
auto slot_bias = ConstantU32(state, image.descriptor_index);
auto previous_bias = image.descriptor_index;
for (uint32_t ordinal = 1; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto& candidate = state.program.info.images[image.indirect_resources[ordinal]];
const auto bias = candidate.descriptor_index - ordinal;
const bool select_group = ordinal_groups[ordinal] != ordinal_groups[ordinal - 1u];
const bool select_slot = dynamic_slot && bias != previous_bias;
previous_bias = bias;
if (!select_group && !select_slot) continue;
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
selected, ConstantU32(state, ordinal));
if (select_group) group_index = Select(state, TypeU32(state), at_or_after,
ConstantU32(state, ordinal_groups[ordinal]), group_index);
if (select_slot) slot_bias = Select(state, TypeU32(state), at_or_after,
ConstantU32(state, bias), slot_bias);
}
return runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
const auto slot = dynamic_slot
? Binary(state, spv::OpIAdd, TypeU32(state), selected, slot_bias) : 0u;
const auto EmitGroup = [&](uint32_t index) {
const auto& group = groups[index];
return emit(group.resource, group.count == 1u ? 0u : slot);
};
return groups.size() == 1u
? EmitGroup(0u)
: EmitIndexSwitch(state, group_index, static_cast<uint32_t>(groups.size()),
result_type, EmitGroup);
}
template <typename Emit>
@@ -792,6 +813,25 @@ uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit
return mixed ? value : result_bits(mem.resource, value);
}
// Ordinals outside the table select candidates[0]. Past ordinal 0, a consecutive table is an
// offset of ordinal - 1, which wraps at 0 so that one unsigned compare checks both ends.
uint32_t SamplerArrayIndex(EmitterState& state, uint32_t ordinal,
const std::vector<uint32_t>& candidates) {
const auto count = static_cast<uint32_t>(candidates.size());
const auto gap = std::adjacent_find(candidates.begin() + 1, candidates.end(),
[](uint32_t lhs, uint32_t rhs) { return rhs != lhs + 1u; });
if (count == 1u || gap != candidates.end()) {
return EmitIndexSwitch(state, ordinal, count, TypeU32(state),
[&](uint32_t item) { return ConstantU32(state, candidates[item]); });
}
const auto offset = Binary(state, spv::OpISub, TypeU32(state), ordinal, ConstantU32(state, 1u));
const auto in_table = Binary(state, spv::OpULessThan, TypeBool(state), offset,
ConstantU32(state, count - 1u));
const auto index = Binary(state, spv::OpIAdd, TypeU32(state), offset,
ConstantU32(state, candidates[1]));
return Select(state, TypeU32(state), in_table, index, ConstantU32(state, candidates[0]));
}
struct ImageSampler {
uint32_t id = 0;
bool dynamic = false;
@@ -827,9 +867,7 @@ std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::I
selected = EmitIndirectResourceIndex(state, selected,
samplers[index].indirect_mapping_offset,
samplers[index].indirect_search_iterations, 0u);
array_index = EmitIndexSwitch(
state, selected, static_cast<uint32_t>(candidates.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
array_index = SamplerArrayIndex(state, selected, candidates);
}
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
};
@@ -852,7 +890,10 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto* address = ctx.ImageAddress(inst.Arg(image_info.needs_sampler ? 2 : 1));
if (op == IR::ValueOpcode::ImageQueryDimensions) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
ctx.Define(inst, EmitImageAccess(ctx, inst, TypeU32Vector(state, 4),
[&](uint32_t resource, uint32_t array_index) {
return QueryDimensions(ctx, mem, *address, resource, array_index);
}));
return;
}
if (image_info.needs_sampler &&
@@ -873,14 +914,19 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
}
if (op == IR::ValueOpcode::ImageQueryLod) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto dimension = image.dimension;
const auto sampled = MakeSampledImage(state, mem.resource,
LoadSamplerDescriptor(state, mem.sampler));
const auto lod = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpImageQueryLod, TypeF32Vector(state, 2), lod, sampled,
CoordF32(ctx, mem, *address, 0, ImageDimensionInfoFor(dimension).spatial_components,
image.cube));
const auto sampler = LoadSamplerDescriptor(state, mem.sampler);
const auto lod = EmitImageAccess(ctx, inst, TypeF32Vector(state, 2),
[&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
const auto query = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpImageQueryLod, TypeF32Vector(state, 2), query,
MakeSampledImage(state, resource, sampler, 0u, array_index),
CoordF32(ctx, mem, *address, 0,
ImageDimensionInfoFor(candidate.dimension).spatial_components,
candidate.cube));
return query;
});
uint32_t values[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0),
ConstantU32(state, 0)};
for (uint32_t index = 0; index < 2u; index++) {
@@ -935,9 +981,12 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto mip_lod = candidate.mip_mode == IR::ImageMipMode::Dynamic
? LodU32(ctx, mem, *address, mem.image_dimension) : 0u;
const auto coord = CoordU32(ctx, mem, *address, candidate.dimension);
const auto& dimension_info = ImageDimensionInfoFor(candidate.dimension);
const auto sample = dimension_info.multisampled != 0u
? AddressU32(ctx, mem, *address, dimension_info.coordinate_components) : 0u;
const auto texel = StoreTexel(ctx, selected, ctx.Arg(inst, 2),
candidate.numeric_class == Prospero::TextureNumericClass::Uint);
EmitStorageImageWrite(state, resource, mip_lod, coord, texel, array_index);
EmitStorageImageWrite(state, resource, mip_lod, coord, texel, array_index, sample);
});
});
return;
@@ -1040,7 +1089,12 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
}
std::array<uint32_t, 4> operands {};
// _cl clamps the computed LOD from below.
uint32_t min_lod = 0;
if (layout.clamp != NoImageComponent) {
min_lod = AddressF32(ctx, mem, *address, layout.clamp);
}
std::array<uint32_t, 5> operands {};
size_t operand_count = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
operands[0] = spv::ImageOperandsGradMask;
@@ -1052,6 +1106,11 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
if (HasFlag(mem, Decoder::ImageSampleFlagLod) && layout.lod != NoImageComponent) {
lod = AddressF32(ctx, mem, *address, layout.lod);
}
// SPIR-V takes MinLod only with implicit LOD or Grad.
if (min_lod != 0) {
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, min_lod);
min_lod = 0;
}
operands[0] = spv::ImageOperandsLodMask;
operands[1] = lod;
operand_count = 2;
@@ -1060,8 +1119,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operands[1] = AddressF32(ctx, mem, *address, layout.bias);
operand_count = 2;
}
if (layout.offset != NoImageComponent &&
!offset_value.IsImmediate() && !state.maintenance8_enabled) {
if (layout.offset != NoImageComponent && !offset_value.IsImmediate() &&
!state.profile.maintenance8) {
ctx.Fail(inst, "requires maintenance8 for a dynamic sample texel offset");
}
const auto samplers = LoadImageSamplers(ctx, inst);
@@ -1075,6 +1134,13 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
sample_operands[sample_operand_count++] = PackedOffset(
ctx, offset_value, candidate.dimension, sample_operands[0]);
}
// MinLod follows the offset in operand order.
if (min_lod != 0) {
state.builder.RequireCapability(spv::CapabilityMinLod);
sample_operand_count = std::max(size_t{1}, sample_operand_count);
sample_operands[0] |= spv::ImageOperandsMinLodMask;
sample_operands[sample_operand_count++] = min_lod;
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
@@ -56,14 +56,13 @@ EMIT_NATIVE(BitFieldSExtract, OpBitFieldSExtract, U32, uint32_t, uint32_t, uint3
EMIT_NATIVE(SelectU1, OpSelect, U1, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(SelectU32, OpSelect, U32, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(SelectF32, OpSelect, F32, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(SelectF64, OpSelect, F64, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(IAdd32, OpIAdd, U32, uint32_t, uint32_t)
EMIT_NATIVE(ISub32, OpISub, U32, uint32_t, uint32_t)
EMIT_NATIVE(IMul32, OpIMul, U32, uint32_t, uint32_t)
EMIT_NATIVE(UDiv32, OpUDiv, U32, uint32_t, uint32_t)
EMIT_NATIVE(IAdd64, OpIAdd, U64, uint32_t, uint32_t)
EMIT_NATIVE(ISub64, OpISub, U64, uint32_t, uint32_t)
EMIT_NATIVE(IMul64, OpIMul, U64, uint32_t, uint32_t)
EMIT_NATIVE(IAddCarry32, OpIAddCarry, U32x2, uint32_t, uint32_t)
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitUMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1);
inline constexpr auto EmitIAbs32 = EmitGlsl<GLSLstd450SAbs, IR::Type::U32, uint32_t>;
@@ -92,10 +91,8 @@ EMIT_NATIVE(BitwiseNot32, OpNot, U32, uint32_t)
EMIT_NATIVE(BitwiseAnd64, OpBitwiseAnd, U64, uint32_t, uint32_t)
EMIT_NATIVE(BitReverse32, OpBitReverse, U32, uint32_t)
EMIT_NATIVE(BitCount32, OpBitCount, U32, uint32_t)
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitFindILsb32 = EmitGlsl<GLSLstd450FindILsb, IR::Type::U32, uint32_t>;
inline constexpr auto EmitFindUMsb32 = EmitGlsl<GLSLstd450FindUMsb, IR::Type::U32, uint32_t>;
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitSMin32 = EmitGlsl<GLSLstd450SMin, IR::Type::U32, uint32_t, uint32_t>;
inline constexpr auto EmitSMax32 = EmitGlsl<GLSLstd450SMax, IR::Type::U32, uint32_t, uint32_t>;
inline constexpr auto EmitUMin32 = EmitGlsl<GLSLstd450UMin, IR::Type::U32, uint32_t, uint32_t>;
@@ -218,6 +215,7 @@ inline constexpr auto EmitTtraceData = EmitVoid;
inline constexpr auto EmitInstPrefetch = EmitVoid;
void EmitBarrier(EmitterState& state);
void EmitStoreCompletion(EmitterState& state);
void EmitDataCacheInvalidate(EmitterState& state);
void EmitMeshAllocate(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitMeshDrawParameter(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetTessellationAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -226,25 +224,26 @@ uint32_t EmitGetUserData(EmitterState& state, IR::ScalarReg reg);
uint32_t EmitGetDispatchThreadExtent(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetBuiltin(ValueEmitContext& ctx, IR::Value kind, IR::Value index);
uint32_t EmitUndefU1(EmitterState& state, const IR::Inst& inst);
inline constexpr auto EmitUndefU8 = EmitUndefU1;
inline constexpr auto EmitUndefU16 = EmitUndefU1;
inline constexpr auto EmitUndefU32 = EmitUndefU1;
inline constexpr auto EmitUndefU64 = EmitUndefU1;
uint32_t EmitDppMoveU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitWqmU64(EmitterState& state, uint32_t value);
uint32_t EmitLaneId(EmitterState& state);
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate);
uint32_t EmitLiveMask(EmitterState& state);
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetAttributeF16(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetInterpolationParameter(ValueEmitContext& ctx, const IR::Inst& inst);
void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetShaderBase(ValueEmitContext& ctx);
uint32_t EmitReadRealtimeClock(ValueEmitContext& ctx, const IR::Inst& inst);
inline constexpr auto EmitGetSrtResource = EmitVoid;
inline constexpr auto EmitGetBufferResource = EmitGetSrtResource;
uint32_t EmitGetBufferResourceIndex(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -292,6 +291,7 @@ inline constexpr auto EmitBufferAtomicIAdd32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicISub32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMin64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
@@ -5,6 +5,7 @@
#include "common/stringUtils.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/backend/spirv/SpirvBuilder.h"
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
@@ -73,14 +74,14 @@ struct ImageDefinition {
};
struct EmitterState {
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, bool maintenance8_enabled_)
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, const Profile& profile_)
: builder(program_.stage == ShaderType::Mesh ? 0x00010400u : 0x00010300u),
program(program_), input_info(input_info_), maintenance8_enabled(maintenance8_enabled_) {
program(program_), input_info(input_info_), profile(profile_) {
if (ShaderWorkgroupInput(program.stage, input_info) != nullptr) {
lds_storage_class = spv::StorageClassWorkgroup;
}
if (program.info.uses_lds && program.stage == ShaderType::Compute &&
input_info.compute != nullptr && input_info.compute->lds_storage) {
input_info.compute->lds_storage) {
lds_storage_class = spv::StorageClassStorageBuffer;
}
}
@@ -88,7 +89,7 @@ struct EmitterState {
Builder builder;
IR::Program& program;
ShaderStageInputInfo input_info;
bool maintenance8_enabled;
const Profile& profile;
uint32_t debug_source = 0;
uint32_t void_type = 0;
uint32_t bool_type = 0;
@@ -104,14 +105,14 @@ struct EmitterState {
std::array<uint32_t, 3> u32_vector_types {};
std::array<uint32_t, 3> i32_vector_types {};
std::array<uint32_t, 3> f32_vector_types {};
std::array<uint32_t, 6> tess_variables {};
std::array<uint32_t, IR::NumTessellationAttributes> tess_variables {};
uint32_t tess_inner_variable = 0;
uint32_t tess_patch_base = 0;
uint32_t lane_count = 1;
uint32_t lane_half = 0;
std::array<BufferDefinition, 4> storage_buffers {};
std::vector<uint32_t> memory_byte_offsets;
std::vector<uint32_t> memory_byte_sizes;
uint32_t bda_pagetable_variable = 0;
uint32_t fault_buffer_variable = 0;
uint32_t bda_pointer_function = 0;
@@ -130,6 +131,7 @@ struct EmitterState {
uint32_t lds_u64_variable = 0;
std::array<uint32_t, 2> scratch_variable {};
std::array<ImageDefinition, IR::ImageBindingCount> images {};
std::array<ImageDefinition, IR::ImageBindingCount> heap_images {};
uint32_t sampler_variable = 0;
uint32_t sampler_type = 0;
uint32_t sampler_pointer_type = 0;
@@ -144,10 +146,12 @@ struct EmitterState {
const IR::Block* current_block = nullptr;
uint32_t pixel_valid_mask_variable = 0;
uint32_t subgroup_local_invocation_id_variable = 0;
uint32_t helper_invocation_variable = 0;
uint32_t per_vertex_variable = 0;
uint32_t point_size_variable = 0;
uint32_t clip_distance_variable = 0;
uint32_t invalid_position_clip_distance = UINT32_MAX;
uint32_t user_clip_planes = 0;
uint32_t cull_distance_variable = 0;
uint32_t layer_variable = 0;
uint32_t viewport_index_variable = 0;
@@ -280,12 +284,12 @@ struct ImageSampleLayout {
uint32_t bias = NoImageComponent;
uint32_t coord = 0;
uint32_t lod = NoImageComponent;
uint32_t clamp = NoImageComponent;
uint32_t grad_x = NoImageComponent;
uint32_t grad_y = NoImageComponent;
};
struct F32Class {
uint32_t bits = 0;
uint32_t nan = 0;
uint32_t zero = 0;
};
@@ -304,6 +308,7 @@ uint32_t VertexParameterScalarType(EmitterState& state, VertexInputScalarKind ki
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp);
uint32_t MrtOutputType(EmitterState& state, uint32_t index);
uint32_t ConstantU32(EmitterState& state, uint32_t value);
@@ -339,8 +344,9 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl
uint32_t mip = 0, uint32_t array_index = 0,
bool sampler_dynamic = false);
// sample is 0 (no id) for single-sampled images.
void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord,
uint32_t texel, uint32_t array_index);
uint32_t texel, uint32_t array_index, uint32_t sample);
spv::ExecutionModel ExecutionModelForStage(ShaderType stage);
@@ -412,7 +418,7 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resource);
void EmitMemoryOffsets(EmitterState& state);
void EmitMemoryBindings(EmitterState& state);
uint32_t LdsDwordCount(const EmitterState& state);
void EnsureLdsStorage(EmitterState& state);
@@ -430,6 +436,8 @@ struct MemoryResourceAccess {
uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t mapping_offset,
uint32_t search_iterations, uint32_t default_resource);
// Slot word of an image heap byte offset; offsets past the heap select the null element.
uint32_t EmitImageHeapEntry(EmitterState& state, uint32_t key, uint32_t mapping_offset);
MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::MemoryInfo& mem,
uint32_t array_index = 0u);
@@ -510,8 +518,6 @@ inline constexpr auto EmitLogicalNotBool = EmitNative<spv::OpLogicalNot, IR::Typ
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits);
F32Class EmitClassifyF32(EmitterState& state, uint32_t value);
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
uint32_t class_match);
@@ -520,8 +526,6 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask);
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value);
inline constexpr auto EmitTruncF32Value = EmitGlsl<GLSLstd450Trunc, IR::Type::F32, uint32_t>;
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value);
uint32_t EmitTrigCycleF32(EmitterState& state, uint32_t src, bool preserve_signed_zero);
@@ -531,6 +535,7 @@ inline constexpr auto EmitFNegateValue = EmitNative<spv::OpFNegate, IR::Type::F3
inline constexpr auto EmitFAbsValue = EmitGlsl<GLSLstd450FAbs, IR::Type::F32, uint32_t>;
uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits);
uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits);
void EmitProgram(EmitterState& state);
@@ -582,6 +587,10 @@ auto EmitIndexSwitch(EmitterState& state, uint32_t index, uint32_t count,
template <typename Fn>
void EmitIfCondition(EmitterState& state, uint32_t condition, Fn&& fn) {
if (condition == ConstantBool(state, true)) {
fn();
return;
}
const auto then_label = state.builder.AllocateId();
const auto merge_label = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelectionMerge, merge_label, spv::SelectionControlMaskNone);
@@ -595,6 +604,7 @@ void EmitIfCondition(EmitterState& state, uint32_t condition, Fn&& fn) {
template <typename Fn>
uint32_t EmitValueOrDefaultIfCondition(EmitterState& state, uint32_t condition, uint32_t type,
uint32_t default_value, Fn&& fn) {
if (condition == ConstantBool(state, true)) return fn();
const auto then_label = state.builder.AllocateId();
const auto header = state.current_label;
const auto merge_label = state.builder.AllocateId();
@@ -1541,16 +1541,16 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2));
const auto access = PrepareMemoryResourceAccess(state, mem);
const auto exec = ctx.Arg(inst, 1);
const auto ballot = ctx.Ballot(inst.Arg(1));
const auto low = state.builder.AllocateId();
const auto high = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1);
// Elect and count from the guest EXEC words, which V_MBCNT also ranks lanes by.
const auto low = ctx.Arg(inst, 1);
const auto high = ctx.Arg(inst, 2);
const auto count = Binary(state, spv::OpIAdd, TypeU32(state),
Unary(state, spv::OpBitCount, TypeU32(state), low),
Unary(state, spv::OpBitCount, TypeU32(state), high));
const auto first = ctx.FirstLane(ballot);
const auto mask = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), mask, low, high,
ConstantU32(state, 0), ConstantU32(state, 0));
const auto first = ctx.FirstLane(mask);
const auto source_lane =
state.lane_count == 2
? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31))
@@ -1564,9 +1564,7 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto condition = AndCondition(
state, is_first,
AndCondition(state,
state.lane_count == 2 ? Binary(state, spv::OpINotEqual, TypeBool(state), count,
ConstantU32(state, 0))
: exec,
Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0)),
bounds));
const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() {
const auto value = state.builder.AllocateId();
@@ -1605,9 +1603,12 @@ void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
}
auto& state = ctx.state;
mem.kind = IR::ResourceKind::ScalarBuffer;
auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, 2));
if (mem.offset >= 4u) {
const auto offset = inst.Arg(1).Resolve();
auto index = offset.IsImmediate()
? ConstantU32(state, (offset.U32() >> 2u) + (mem.offset >> 2u))
: Binary(state, spv::OpShiftRightLogical, TypeU32(state),
ctx.Arg(inst, 1), ConstantU32(state, 2));
if (!offset.IsImmediate() && mem.offset >= 4u) {
index = Binary(state, spv::OpIAdd, TypeU32(state), index, ConstantU32(state, mem.offset >> 2u));
}
const auto access = PrepareMemoryResourceAccess(state, mem);
@@ -38,19 +38,39 @@ uint32_t EmitBinaryU32(EmitterState& state, spv::Op opcode, uint32_t lhs, uint32
return ret;
}
static uint32_t LoadFlattenedSrt(EmitterState& state, uint32_t index) {
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer,
state.flattened_srt_variable, ConstantU32(state, 0), index);
const auto value = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return value;
}
uint32_t EmitImageHeapEntry(EmitterState& state, uint32_t key, uint32_t mapping_offset) {
const auto Mapping = [&](uint32_t word) {
return LoadFlattenedSrt(state, ConstantU32(state, mapping_offset + word));
};
// Keys past the table select its trailing null slot.
const auto index = EmitUMin32(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), key,
ConstantU32(state, 2u)),
Mapping(0u));
const auto offset = Unary(state, spv::OpUConvert, TypeU64(state),
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), index,
ConstantU32(state, 2u)));
const auto pointer = Unary(state, spv::OpConvertUToPtr, TypePhysicalU32Pointer(state),
Binary(state, spv::OpIAdd, TypeU64(state),
PackU64(state, Mapping(1u), Mapping(2u)), offset));
const auto entry = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeU32(state), entry, pointer,
spv::MemoryAccessAlignedMask, 4u);
return entry;
}
uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t mapping_offset,
uint32_t search_iterations, uint32_t default_resource) {
const auto LoadMapping = [&](uint32_t index) {
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state),
pointer, state.flattened_srt_variable, ConstantU32(state, 0),
index);
const auto value = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return value;
};
const auto mapping = ConstantU32(state, mapping_offset);
const auto count = LoadMapping(mapping);
const auto count = LoadFlattenedSrt(state, mapping);
if (search_iterations == 0u) {
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), key,
ConstantU32(state, 2u));
@@ -58,7 +78,7 @@ uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t m
const auto safe = Select(state, TypeU32(state), valid, index, ConstantU32(state, 0u));
const auto entry = Binary(state, spv::OpIAdd, TypeU32(state), safe,
ConstantU32(state, mapping_offset + 1u));
return Select(state, TypeU32(state), valid, LoadMapping(entry),
return Select(state, TypeU32(state), valid, LoadFlattenedSrt(state, entry),
ConstantU32(state, default_resource));
}
const auto entry_at = [&](uint32_t index) {
@@ -76,7 +96,7 @@ uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t m
ConstantU32(state, 1u));
const auto probe = Select(state, TypeU32(state), active, mid, ConstantU32(state, 0u));
const auto less = Binary(state, spv::OpULessThan, TypeBool(state),
LoadMapping(entry_at(probe)), key);
LoadFlattenedSrt(state, entry_at(probe)), key);
low = Select(state, TypeU32(state),
Binary(state, spv::OpLogicalAnd, TypeBool(state), active, less),
Binary(state, spv::OpIAdd, TypeU32(state), mid, ConstantU32(state, 1u)), low);
@@ -87,9 +107,10 @@ uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t m
const auto in_range = Binary(state, spv::OpULessThan, TypeBool(state), low, count);
const auto entry = entry_at(Select(state, TypeU32(state), in_range, low, ConstantU32(state, 0u)));
const auto match = Binary(state, spv::OpLogicalAnd, TypeBool(state), in_range,
Binary(state, spv::OpIEqual, TypeBool(state), LoadMapping(entry), key));
const auto resource = LoadMapping(Binary(state, spv::OpIAdd, TypeU32(state), entry,
ConstantU32(state, 1u)));
Binary(state, spv::OpIEqual, TypeBool(state),
LoadFlattenedSrt(state, entry), key));
const auto resource = LoadFlattenedSrt(
state, Binary(state, spv::OpIAdd, TypeU32(state), entry, ConstantU32(state, 1u)));
return Select(state, TypeU32(state), match, resource, ConstantU32(state, default_resource));
}
@@ -133,9 +154,10 @@ uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resour
return representative;
}
void EmitMemoryOffsets(EmitterState& state) {
void EmitMemoryBindings(EmitterState& state) {
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
state.memory_byte_offsets.resize(count);
state.memory_byte_sizes.resize(count);
std::vector<bool> dynamic_offsets(count);
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
if (SharedIndirectBufferResource(state, resource) == UINT32_MAX) continue;
@@ -153,13 +175,31 @@ void EmitMemoryOffsets(EmitterState& state) {
if (dynamic_offsets[i + lane]) continue;
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
state.memory_byte_sizes[i + lane] =
EmitShaderDataDwordLoad(state, state.program.bindings.MemorySizeDword() + i + lane);
}
}
}
// Stages without a workgroup emulate LDS with one private array per invocation. A GPU keeps
// hundreds of thousands of invocations in flight, so this size multiplies into driver local memory:
// a fixed 8192 dwords (32 KB) asked for gigabytes and killed the device (a write fault in the
// first heavy pixel shader that touched LDS). A pixel shader can only address the LDS that
// EXTRA_LDS_SIZE reserves, a vertex shader the LDS_SIZE of its GS stage; hardware reads zero and
// drops writes beyond it, as the bounds checks do.
constexpr uint32_t FUNCTION_LDS_DWORDS = 1024u;
uint32_t LdsDwordCount(const EmitterState& state) {
const auto* workgroup = ShaderWorkgroupInput(state.program.stage, state.input_info);
return workgroup != nullptr ? workgroup->lds_size_dwords : 8192u;
if (const auto* workgroup = ShaderWorkgroupInput(state.program.stage, state.input_info)) {
return workgroup->lds_size_dwords;
}
if (state.program.stage == ShaderType::Pixel) {
return std::max(state.input_info.pixel->lds_size_dwords, 1u);
}
if (state.program.stage == ShaderType::Vertex) {
return std::max(state.input_info.vertex->lds_size_dwords, 1u);
}
return FUNCTION_LDS_DWORDS;
}
void EnsureLdsStorage(EmitterState& state) {
@@ -220,22 +260,28 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
const auto offsets_start = state.program.bindings.memory_offset_dword +
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
const auto& bindings = state.program.bindings;
const auto data_start = bindings.UsesPushData() ? bindings.push_data_start_dword : 0u;
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, offsets_start),
state, ConstantU32(state, bindings.memory_offset_dword + data_start),
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
ConstantU32(state, 3));
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, shift, ConstantU32(state, 8));
access.length = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, bindings.MemorySizeDword() + data_start), array_index));
} else {
access.byte_offset = state.memory_byte_offsets[static_index];
access.length = state.memory_byte_sizes[static_index];
}
// storage_buffers[i] views the binding as 1 << i byte elements.
const auto shift = static_cast<uint32_t>(&buffer - state.storage_buffers.data());
if (shift != 0u) {
access.length = Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.length,
ConstantU32(state, shift));
}
access.length = state.builder.AllocateId();
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
access.object_pointer, 0);
return access;
}
@@ -435,6 +481,17 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer
UnormRounding rounding) {
EXIT_IF(rounding != UnormRounding::RoundEven && info.type != Format::ComponentType::Unorm);
const auto bits = info.component_bits[component];
if (info.type == Format::ComponentType::Float && bits < 16u) {
// Truncate to f16, whose bias the unsigned 11/10-bit floats share, and drop mantissa
// bits; negatives and NaN, whose f16 bits lie above +inf, store zero.
const auto half = data_bits == 16u ? EmitAndConstant(state, data, 0xffffu)
: EmitF32BitsToF16RtzBits(state, data);
return Select(state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpUGreaterThan, half, 0x7c00u),
ConstantU32(state, 0),
EmitBinaryU32(state, spv::OpShiftRightLogical, half,
ConstantU32(state, 15u - bits)));
}
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
@@ -528,6 +585,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicUMin32:
case IR::ValueOpcode::ImageAtomicUMin64:
case IR::ValueOpcode::BufferAtomicUMin32:
case IR::ValueOpcode::BufferAtomicUMin64:
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
case IR::ValueOpcode::ImageAtomicSMax32:
case IR::ValueOpcode::BufferAtomicSMax32:
@@ -187,7 +187,7 @@ void EmitMeshEntryPoint(EmitterState& state) {
vertex[component] = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpBitFieldUExtract, TypeU32(state), vertex[component], packed,
ConstantU32(state, component * 10u), ConstantU32(state, 10));
ConstantU32(state, component * 10u), ConstantU32(state, 9));
}
const auto triangle = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3), triangle,
@@ -206,6 +206,17 @@ uint32_t F32ArrayType(EmitterState& state, uint32_t count) {
return state.builder.Type(spv::OpTypeArray, TypeF32(state), ConstantU32(state, count));
}
void DefineImageArray(EmitterState& state, ImageDefinition& definition, const IR::ImageResource& image,
uint32_t count, uint32_t set, uint32_t binding) {
definition.type = ImageType(state, image);
const auto array = state.builder.Type(spv::OpTypeArray, definition.type, ConstantU32(state, count));
definition.variable = state.builder.DefineGlobalVariable(
TypePointer(state, spv::StorageClassUniformConstant, array), spv::StorageClassUniformConstant);
state.builder.AddAnnotation(spv::OpDecorate, definition.variable, spv::DecorationDescriptorSet, set);
state.builder.AddAnnotation(spv::OpDecorate, definition.variable, spv::DecorationBinding, binding);
definition.pointer_type = TypePointer(state, spv::StorageClassUniformConstant, definition.type);
}
void DefineDescriptors(EmitterState& state) {
using Kind = IR::DescriptorBindingKind;
auto& info = state.program.info;
@@ -222,6 +233,14 @@ void DefineDescriptors(EmitterState& state) {
(image.mip_mode != IR::ImageMipMode::Dynamic && image.mip_count != 1u)) {
EXIT("shader image has an invalid descriptor type or mip count");
}
if (image.heap) {
// Heap kinds alias one array; the root itself is never accessed.
auto& definition = state.heap_images[IR::ImageBindingIndex(*kind)];
if (image.indirect_resources.empty() && definition.variable == 0) {
DefineImageArray(state, definition, image, IR::ImageHeapCapacity, IR::ImageHeapSet, 0u);
}
continue;
}
auto& count = counts[static_cast<size_t>(*kind)];
image.descriptor_index = count;
if (count == 0) image_types[IR::ImageBindingIndex(*kind)] = &image;
@@ -244,7 +263,7 @@ void DefineDescriptors(EmitterState& state) {
counts[static_cast<size_t>(Kind::ShaderData)] =
bindings.ShaderDataDwords() != 0 && !bindings.UsesPushData();
if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh ||
state.object_primitive_id_variable != 0) {
state.object_primitive_id_variable != 0 || state.user_clip_planes != 0) {
const auto type = PushConstantBlockType(state);
state.push_constant_variable = state.builder.DefineGlobalVariable(
TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant);
@@ -331,21 +350,11 @@ void DefineDescriptors(EmitterState& state) {
default: {
EXIT_IF(IR::ImageBindingResourceClass(kind) ==
IR::ImageResourceClass::None);
const auto& image = *image_types[IR::ImageBindingIndex(kind)];
const auto name = "image_" + std::to_string(static_cast<uint32_t>(kind));
auto& definition = state.images[IR::ImageBindingIndex(kind)];
definition.type = ImageType(state, image);
definition.variable = Define(ArrayType(definition.type), name.c_str(),
spv::StorageClassUniformConstant);
definition.pointer_type = TypePointer(
state, spv::StorageClassUniformConstant, definition.type);
if (image.dimension == ImageDimension::Dim1D ||
image.dimension == ImageDimension::Dim1DArray) {
state.builder.RequireCapability(image.resource_class ==
IR::ImageResourceClass::Sampled
? spv::CapabilitySampled1D
: spv::CapabilityImage1D);
}
DefineImageArray(state, definition, *image_types[IR::ImageBindingIndex(kind)],
counts[index], 0u, IR::NativeBinding(state.program.stage, kind));
state.builder.AddName(definition.variable,
("image_" + std::to_string(static_cast<uint32_t>(kind))).c_str());
break;
}
}
@@ -467,6 +476,8 @@ uint32_t BuiltInForInput(IR::StageInputKind kind) {
case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId;
case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex;
case IR::StageInputKind::GlobalInvocationId: return spv::BuiltInGlobalInvocationId;
case IR::StageInputKind::BaseVertex: return spv::BuiltInBaseVertex;
case IR::StageInputKind::BaseInstance: return spv::BuiltInBaseInstance;
default: return UINT32_MAX;
}
}
@@ -517,7 +528,9 @@ void DefineInputs(EmitterState& state) {
case IR::StageInputKind::PrimitiveId:
case IR::StageInputKind::InstanceIndex:
case IR::StageInputKind::Layer:
case IR::StageInputKind::SampleId: type = TypeI32(state); break;
case IR::StageInputKind::SampleId:
case IR::StageInputKind::BaseVertex:
case IR::StageInputKind::BaseInstance: type = TypeI32(state); break;
case IR::StageInputKind::WorkgroupId:
case IR::StageInputKind::NumWorkgroups:
case IR::StageInputKind::LocalInvocationId:
@@ -586,6 +599,13 @@ void DefineInputs(EmitterState& state) {
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat);
}
}
if (state.program.info.helper_invocation) {
const auto variable = DefineInterfaceVariable(state, TypeBool(state), spv::StorageClassInput,
"gl_HelperInvocation");
state.helper_invocation_variable = variable;
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn,
spv::BuiltInHelperInvocation);
}
}
void DefineOutputs(EmitterState& state) {
@@ -600,14 +620,24 @@ void DefineOutputs(EmitterState& state) {
cull_distance_count = std::max(cull_distance_count, output.index + 1);
}
}
const bool position = std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
});
if (position && state.input_info.vertex->user_clip_planes != 0) {
// PipelineCache rejects UCP_ENA with shader clip or cull distances.
EXIT_IF(clip_distance_count + cull_distance_count != 0);
state.user_clip_planes = state.input_info.vertex->user_clip_planes;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
state.outputs.push_back(
{{IR::StageOutputKind::ClipDistance, clip_distance_count++, 0, "gl_ClipDistance"}});
}
}
if (state.program.stage == ShaderType::Mesh) {
DefineMeshOutputs(state, clip_distance_count, cull_distance_count);
return;
}
if (state.program.stage == ShaderType::Vertex && clip_distance_count + cull_distance_count < 8u &&
std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
})) {
if (state.program.stage == ShaderType::Vertex &&
clip_distance_count + cull_distance_count < 8u && position) {
// Reserve one plane for the enabled PA_CL_CLIP_CNTL clipping-error cull.
state.invalid_position_clip_distance = clip_distance_count++;
state.outputs.push_back({{IR::StageOutputKind::ClipDistance,
@@ -672,13 +702,15 @@ void DefineOutputs(EmitterState& state) {
break;
case IR::StageOutputKind::Parameter:
case IR::StageOutputKind::Mrt: {
const bool uint_output =
binding.kind == IR::StageOutputKind::Mrt &&
state.program.stage == ShaderType::Pixel &&
binding.index < std::size(state.input_info.pixel->target_output_mode) &&
(state.input_info.pixel->target_output_mode[binding.index] == 7u ||
(state.input_info.pixel->target_packed_unorm_mask & (1u << binding.index)) != 0u);
const auto type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
auto type = TypeF32Vector(state, 4);
if (binding.kind == IR::StageOutputKind::Mrt &&
state.program.stage == ShaderType::Pixel && binding.index < 8u) {
// Packed UNorm stores its encoded bits.
const auto bit = 1u << binding.index;
type = (state.input_info.pixel->target_packed_unorm_mask & bit) != 0u
? TypeU32Vector(state, 4)
: MrtOutputType(state, binding.index);
}
binding.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassOutput,
binding.debug_name.c_str());
const bool dual_source = binding.kind == IR::StageOutputKind::Mrt &&
@@ -742,7 +774,7 @@ void DefineModule(EmitterState& state) {
if (state.program.info.buffer_u16) {
state.builder.RequireCapability(spv::CapabilityStorageBuffer16BitAccess);
}
if (state.program.info.uses_dma) {
if (state.program.info.uses_dma || state.program.info.uses_image_heap) {
state.builder.RequireCapability(spv::CapabilityInt64);
state.builder.RequireCapability(spv::CapabilityPhysicalStorageBufferAddresses);
state.builder.RequireExtension("SPV_KHR_physical_storage_buffer");
@@ -774,6 +806,10 @@ void DefineModule(EmitterState& state) {
if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) {
state.builder.RequireCapability(spv::CapabilitySampleRateShading);
}
if (InputVariableForKind(state, IR::StageInputKind::BaseVertex) != 0 ||
InputVariableForKind(state, IR::StageInputKind::BaseInstance) != 0) {
state.builder.RequireCapability(spv::CapabilityDrawParameters);
}
if (state.program.info.image_gather_extended) {
state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
}
@@ -810,7 +846,7 @@ void DefineModule(EmitterState& state) {
state.builder.RequireExtension("SPV_KHR_fragment_shader_barycentric");
}
state.builder.RequireExtension("SPV_KHR_float_controls");
state.builder.AddMemoryModel(state.program.info.uses_dma
state.builder.AddMemoryModel(state.program.info.uses_dma || state.program.info.uses_image_heap
? spv::AddressingModelPhysicalStorageBuffer64
: spv::AddressingModelLogical,
spv::MemoryModelGLSL450);
@@ -16,10 +16,8 @@ namespace {
void EmitKillIfBoolFalse(EmitterState& state, uint32_t active) {
const auto kill_label = state.builder.AllocateId();
const auto merge_label = state.builder.AllocateId();
const auto inactive = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLogicalNot, TypeBool(state), inactive, active);
state.builder.AddFunction(spv::OpSelectionMerge, merge_label, spv::SelectionControlMaskNone);
state.builder.AddFunction(spv::OpBranchConditional, inactive, kill_label, merge_label);
state.builder.AddFunction(spv::OpBranchConditional, active, merge_label, kill_label);
EmitLabel(state, kill_label);
state.builder.AddFunction(spv::OpKill);
EmitLabel(state, merge_label);
@@ -233,14 +231,17 @@ void Invoke(Return (*emit)(Context&, Args...), ValueEmitContext& ctx, const IR::
}
void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.half != 0 && (inst.GetOpcode() == IR::ValueOpcode::Ballot ||
inst.GetOpcode() == IR::ValueOpcode::ReadFirstLane ||
inst.GetOpcode() == IR::ValueOpcode::SubgroupUMinU32)) {
// These operations combine both emulated halves into one whole-wave result.
using Op = IR::ValueOpcode;
const auto op = inst.GetOpcode();
if (ctx.half != 0 &&
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
op == Op::SubgroupUMaxU32 || op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
// Whole-wave results, clocks and SMEM loads (SGPR operands, no EXEC) match in both halves.
ctx.Define(inst, ctx.other_half->Result(inst));
return;
}
switch (inst.GetOpcode()) {
switch (op) {
#define VALUE_OPCODE(name, ...) \
case IR::ValueOpcode::name: return Invoke(Emit##name, ctx, inst);
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc"
@@ -307,6 +308,7 @@ void EmitBlock(ValueEmitContext& ctx, const IR::Block* block, EmitInstruction&&
ctx.state.lane_half = half;
if (half == 0 || (inst.GetOpcode() != IR::ValueOpcode::Barrier &&
inst.GetOpcode() != IR::ValueOpcode::StoreCompletion &&
inst.GetOpcode() != IR::ValueOpcode::DataCacheInvalidate &&
inst.GetOpcode() != IR::ValueOpcode::MeshAllocate)) {
emit_instruction(lane, inst);
}
@@ -491,29 +493,24 @@ uint32_t ValueEmitContext::Ballot(IR::Value predicate) {
}
uint32_t ValueEmitContext::FirstLane(uint32_t ballot) {
if (other_half == nullptr) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), ballot);
return result;
// An empty ballot (EXEC zero) gives lane 0, which V_READFIRSTLANE reads on hardware. FindILsb
// returns -1 for zero, unlike BallotFindLSB, whose result is then undefined.
const auto first = [&](uint32_t word) {
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), bits, ballot, word);
return std::pair {bits, EmitGlsl<GLSLstd450FindILsb, IR::Type::U32>(state, bits)};
};
const auto [low, low_first] = first(0);
if (state.program.wave_size == 32u) {
return EmitGlsl<GLSLstd450SMax, IR::Type::U32>(state, low_first, ConstantU32(state, 0));
}
const auto low = state.builder.AllocateId();
const auto high = state.builder.AllocateId();
const auto low_first = state.builder.AllocateId();
const auto high_first = state.builder.AllocateId();
const auto low_active = state.builder.AllocateId();
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1);
state.builder.AddFunction(spv::OpExtInst, TypeU32(state), low_first, GlslStd450(state),
GLSLstd450FindILsb, low);
state.builder.AddFunction(spv::OpExtInst, TypeU32(state), high_first, GlslStd450(state),
GLSLstd450FindILsb, high);
state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), low_active, low,
ConstantU32(state, 0));
state.builder.AddFunction(spv::OpSelect, TypeU32(state), result, low_active, low_first,
EmitAddU32(state, high_first, ConstantU32(state, 32)));
return result;
const auto high_first = first(1).second;
const auto high_lane = EmitGlsl<GLSLstd450SMax, IR::Type::U32>(
state, EmitBinaryU32(state, spv::OpBitwiseOr, high_first, ConstantU32(state, 32)),
ConstantU32(state, 0));
return Select(state, TypeU32(state),
Binary(state, spv::OpINotEqual, TypeBool(state), low, ConstantU32(state, 0)),
low_first, high_lane);
}
uint32_t ValueEmitContext::Shuffle(const IR::Inst& inst, size_t index, uint32_t lane) {
@@ -622,6 +619,12 @@ void EmitProgram(EmitterState& state) {
}
if (state.program.dispatcher_fallback) {
auto& dispatch = dispatcher.emplace();
// Allocate both halves in program order so the SPIR-V is identical across runs.
const auto add_spill = [&](const IR::Inst* inst) {
for (uint32_t half = 0; half < state.lane_count; half++) {
dispatch.spills[half].emplace(inst, state.builder.AllocateId());
}
};
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
if (inst.GetOpcode() != IR::ValueOpcode::Phi) {
@@ -631,7 +634,7 @@ void EmitProgram(EmitterState& state) {
ctx.Fail(inst, "cannot be stored by the dispatcher");
break;
}
dispatch.spills[0].emplace(&inst, state.builder.AllocateId());
add_spill(&inst);
}
}
const auto mark_cross_block = [&](IR::Value value, const IR::Block* consumer) {
@@ -646,7 +649,7 @@ void EmitProgram(EmitterState& state) {
return;
}
if (!dispatch.spills[0].contains(definition)) {
dispatch.spills[0].emplace(definition, state.builder.AllocateId());
add_spill(definition);
}
};
for (const auto* block: program.blocks) {
@@ -668,12 +671,7 @@ void EmitProgram(EmitterState& state) {
dispatch.continue_label = state.builder.AllocateId();
dispatch.merge_label = state.builder.AllocateId();
ctx.dispatcher_spills = &dispatch.spills[0];
if (state.lane_count == 2) {
for (const auto& [inst, id]: dispatch.spills[0]) {
dispatch.spills[1].emplace(inst, state.builder.AllocateId());
}
high.dispatcher_spills = &dispatch.spills[1];
}
high.dispatcher_spills = &dispatch.spills[1];
}
DefineGetBdaPointer(state);
DefineIndirectBufferLoads(ctx);
@@ -748,7 +746,7 @@ void EmitProgram(EmitterState& state) {
state.lds_base_dwords = EmitBinaryU32(state, spv::OpIMul, index,
ConstantU32(state, LdsDwordCount(state)));
}
EmitMemoryOffsets(state);
EmitMemoryBindings(state);
if (program.blocks.empty()) {
EmitReturn(ctx);
} else if (state.program.dispatcher_fallback) {
@@ -5,6 +5,17 @@
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
bool IsTessellationInput(IR::TessellationAttribute kind) {
using Attribute = IR::TessellationAttribute;
return kind == Attribute::ControlInput || kind == Attribute::EvaluationInput ||
kind == Attribute::PatchInput;
}
bool IsTessellationPatch(IR::TessellationAttribute kind) {
using Attribute = IR::TessellationAttribute;
return kind == Attribute::PatchOutput || kind == Attribute::PatchInput;
}
uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
using Attribute = IR::TessellationAttribute;
@@ -12,8 +23,9 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto& tess = state.input_info.vertex->tess;
const auto variable = state.tess_variables.at(static_cast<uint32_t>(kind));
EXIT_IF(variable == 0);
const auto input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
const bool patch = IsTessellationPatch(kind);
const auto storage =
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
const auto pointer = state.builder.AllocateId();
if (kind == Attribute::Factor) {
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
@@ -26,15 +38,13 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
return pointer;
}
auto address = ctx.Arg(inst, 1);
if (kind == Attribute::PatchOutput) {
address =
EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tess_patch_base));
if (patch) {
address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, tess.patch_base));
}
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
const auto stride = local ? tess.ls_stride : tess.hs_stride;
const auto offset = kind == Attribute::PatchOutput ? address
: EmitBinaryU32(state, spv::OpUMod, address,
ConstantU32(state, stride));
const auto offset =
patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride));
const auto attribute =
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4));
const auto component =
@@ -42,12 +52,14 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2)),
ConstantU32(state, 3));
const auto type = TypePointer(state, storage, TypeU32(state));
if (kind == Attribute::LocalOutput || kind == Attribute::PatchOutput) {
if (kind == Attribute::LocalOutput || patch) {
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute,
component);
} else {
// An HS writes only its own control point but may read any other.
const auto vertex =
kind == Attribute::ControlOutput
kind == Attribute::ControlOutput &&
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute
? EmitLaneId(state)
: EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride));
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute,
@@ -60,20 +72,12 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
void DefineTessellationInterfaces(EmitterState& state) {
using Attribute = IR::TessellationAttribute;
std::array<bool, 6> used {};
uint32_t patch_begin = UINT32_MAX, patch_end = 0;
std::array<bool, IR::NumTessellationAttributes> used {};
for (const auto* block: state.program.blocks) {
for (const auto& inst: *block) {
if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute &&
inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute)
continue;
const auto kind = inst.Arg(0).U32();
used.at(kind) = true;
if (kind == static_cast<uint32_t>(Attribute::PatchOutput)) {
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
patch_begin = std::min(patch_begin, inst.Arg(1).U32());
patch_end = std::max(patch_end, inst.Arg(1).U32() + 4u);
}
if (inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute ||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute)
used.at(inst.Arg(0).U32()) = true;
}
}
if (std::ranges::none_of(used, [](bool value) { return value; })) return;
@@ -83,15 +87,16 @@ void DefineTessellationInterfaces(EmitterState& state) {
};
for (uint32_t index = 0; index < used.size(); index++) {
if (!used[index]) continue;
const auto kind = static_cast<Attribute>(index);
const bool input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
uint32_t type;
const auto kind = static_cast<Attribute>(index);
const bool patch = IsTessellationPatch(kind);
const auto storage =
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
uint32_t type;
if (kind == Attribute::Factor) {
type = array(TypeF32(state), 4u);
} else if (kind == Attribute::PatchOutput) {
state.tess_patch_base = patch_begin;
type = array(TypeU32Vector(state, 4), (patch_end - patch_begin + 15u) / 16u);
} else if (patch) {
// HS and TES share the reflected patch layout so their interfaces match.
type = array(TypeU32Vector(state, 4), (tess.patch_size + 15u) / 16u);
} else {
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
const auto stride = local ? tess.ls_stride : tess.hs_stride;
@@ -112,11 +117,10 @@ void DefineTessellationInterfaces(EmitterState& state) {
state.builder.AddAnnotation(spv::OpDecorate, state.tess_inner_variable,
spv::DecorationPatch);
} else {
state.builder.AddAnnotation(
spv::OpDecorate, variable, spv::DecorationLocation,
kind == Attribute::PatchOutput ? (tess.hs_stride + 15u) / 16u : 0u);
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation,
patch ? (tess.hs_stride + 15u) / 16u : 0u);
}
if (kind == Attribute::Factor || kind == Attribute::PatchOutput) {
if (kind == Attribute::Factor || patch) {
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch);
}
}
@@ -124,7 +128,9 @@ void DefineTessellationInterfaces(EmitterState& state) {
void DefineTessellationExecutionModes(EmitterState& state) {
const auto& tess = state.input_info.vertex->tess;
EXIT_NOT_IMPLEMENTED(tess.domain != 1u || tess.partitioning != 2u ||
// CxVgtTfParam: triangles, integer or fractional-odd partitioning, clockwise output.
EXIT_NOT_IMPLEMENTED(tess.domain != 1u ||
(tess.partitioning != 0u && tess.partitioning != 2u) ||
tess.output_topology != 2u);
state.builder.RequireCapability(spv::CapabilityTessellation);
if (state.program.stage == ShaderType::TessellationControl) {
@@ -132,7 +138,9 @@ void DefineTessellationExecutionModes(EmitterState& state) {
tess.output_control_points);
} else {
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeTriangles);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSpacingFractionalOdd);
state.builder.AddExecutionMode(
state.main_func, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual
: spv::ExecutionModeSpacingFractionalOdd);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeVertexOrderCw);
}
}
@@ -8,6 +8,8 @@
#include <deque>
#include <list>
#include <map>
#include <optional>
#include <set>
#include <span>
namespace Libs::Graphics::ShaderRecompiler::CFG {
@@ -468,8 +470,10 @@ bool ResolveSetpcDwordJumpTable(const Decoder::Program& program, uint32_t setpc_
if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 2u, &entry_count)) {
return false;
}
// Code-table loads are not translated, so a selector that is also the load destination
// keeps its scaled index at the branch.
uint32_t selector_code = UINT32_MAX;
if (!ScalarOperandCode(load.src1, selector_code) || selector_code == offset_code ||
if (!ScalarOperandCode(load.src1, selector_code) ||
ScalarCodeWrittenInRange(program, load_index + 1u, setpc_index, selector_code)) {
return false;
}
@@ -1650,6 +1654,32 @@ Graph BuildGraph(const Decoder::Program& program) {
}
SortUnique(graph.code_table_load_pcs);
if (!indirect_setpc && std::ranges::any_of(program.instructions, [](const auto& inst) {
return inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX;
})) {
// Linked leaf bodies follow the caller in native PC order. Visit them in
// execution order so returning to the caller does not manufacture goto loops.
std::vector<bool> visited(graph.blocks.size(), false);
std::vector<std::pair<uint32_t, bool>> pending {{graph.entry_block, false}};
std::vector<BasicBlock> ordered;
ordered.reserve(graph.blocks.size());
while (!pending.empty()) {
const auto [id, finished] = pending.back();
pending.pop_back();
if (finished) {
ordered.push_back(std::move(graph.blocks[id]));
continue;
}
if (visited[id]) continue;
visited[id] = true;
pending.emplace_back(id, true);
for (const auto successor: graph.blocks[id].successors)
pending.emplace_back(successor, false);
}
std::ranges::reverse(ordered);
ApplyBlockOrder(graph, std::move(ordered));
}
RecomputeAnalyses(graph);
if (indirect_setpc) {
@@ -1680,34 +1710,61 @@ Graph Structurize(const Graph& graph) {
return GotoStructurizer(graph).Run();
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
uint32_t destination = UINT32_MAX;
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
return InstructionWritesScalarCode(inst, code) ||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size) {
// A vector lane mask (comparison or carry) spans wave_size / 32 SGPRs; a lane read writes one.
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
inst.family == Decoder::Family::VOP3P) {
const bool lane_read =
inst.opcode == Opcode::V_READLANE_B32 || inst.opcode == Opcode::V_READFIRSTLANE_B32;
const uint32_t size = lane_read ? 1u : wave_size / 32u;
uint32_t destination = 0;
for (const auto* operand: {&inst.dst, &inst.dst2})
if (ScalarOperandCode(*operand, destination) && code >= destination && code < destination + size)
return true;
}
return InstructionWritesScalarCode(inst, code);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code) {
uint32_t before, uint32_t code, uint32_t wave_size,
std::vector<uint32_t>& crossed_calls) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
});
EXIT_IF(found == graph.blocks.end());
const auto* block = &*found;
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
for (uint32_t i = before; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
if (MayWriteScalarRegister(inst, code)) return i;
}
if (block->id == graph.entry_block) return UINT32_MAX;
EXIT_IF(block->predecessors.size() != 1u);
block = graph.FindBlock(block->predecessors[0]);
std::vector<std::pair<uint32_t, uint32_t>> pending {{found->id, before}};
std::set<std::pair<uint32_t, uint32_t>> visited;
std::optional<uint32_t> definition;
while (!pending.empty()) {
const auto point = pending.back();
pending.pop_back();
if (!visited.insert(point).second) continue;
const auto* block = graph.FindBlock(point.first);
EXIT_IF(block == nullptr);
before = block->inst_end;
uint32_t index = UINT32_MAX;
for (uint32_t i = point.second; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
if (inst.opcode == Opcode::S_SWAPPC_B64) {
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code, wave_size));
crossed_calls.push_back(i);
continue;
}
if (MayWriteScalarRegister(inst, code, wave_size)) { index = i; break; }
}
if (index != UINT32_MAX || block->id == graph.entry_block) {
EXIT_IF(definition && *definition != index);
definition = index;
continue;
}
EXIT_IF(block->predecessors.empty());
for (const auto predecessor: block->predecessors) {
pending.emplace_back(predecessor, graph.blocks[predecessor].inst_end);
}
}
EXIT("scalar shader call source has cyclic reaching definitions");
EXIT_IF(!definition);
return *definition;
}
std::string BranchConditionToString(BranchCondition condition) {
@@ -113,9 +113,10 @@ struct Graph {
};
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code);
uint32_t before, uint32_t code, uint32_t wave_size,
std::vector<uint32_t>& crossed_calls);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
Loaded 100 of 239 files, more files were not shown because too many files have changed in this diff. Show more