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
150 changed files with 9995 additions and 5086 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
+22
View File
@@ -42,6 +42,28 @@ if(NOT KYTY_CLANG_CL)
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)
+23 -1
View File
@@ -337,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 OpenXR::openxr_loader)
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)
@@ -407,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"
@@ -451,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"
@@ -645,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>)
@@ -738,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
@@ -806,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)
+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_ */
+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;
}
+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";
@@ -132,6 +133,7 @@ uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
bool ShaderValidationEnabled();
bool ShaderCacheEnabled();
ShaderOptimizationType GetShaderOptimizationType();
LogDirection GetShaderLogDirection();
std::filesystem::path GetShaderLogFolder();
+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 -5
View File
@@ -99,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());
@@ -188,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 {
@@ -97,6 +98,7 @@ public:
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,
@@ -202,6 +202,7 @@ constexpr auto MakeOpcodeDispatchTable() {
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;
@@ -136,6 +136,7 @@ uint32_t CpOpEventWrite(CommandProcessor&, uint32_t, const 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);
@@ -1582,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);
@@ -1602,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;
@@ -2058,6 +2060,21 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
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();
+54 -11
View File
@@ -281,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);
@@ -789,7 +799,7 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
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;
@@ -801,6 +811,28 @@ 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,
@@ -834,12 +866,11 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
// 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);
// With no SGPRs to patch, the GPU reads the records if the shaders allow. Shaders apply
// the records' offsets only through those SGPRs; Vulkan applies them, which matches
// while they are zero.
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP &&
registers.index_offset == Pm4::SH_NOP && (!indexed || m_index_buffer_size != 0) &&
// 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),
@@ -854,7 +885,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
const auto addr = args_addr + static_cast<uint64_t>(i) * stride_in_bytes;
if (!indexed) {
const auto& args = *reinterpret_cast<const vk::DrawIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0, i);
executor.DrawAuto(m_submit_id, CurrentBuffer(),
{.vertex_count = args.vertexCount,
.instance_count = args.instanceCount,
@@ -866,7 +897,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
const auto& args = *reinterpret_cast<const vk::DrawIndexedIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, static_cast<uint32_t>(args.vertexOffset),
args.firstInstance, args.firstIndex);
args.firstInstance, args.firstIndex, i);
executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = std::min(args.indexCount, max_indices),
.index_addr = IndexAddress(args.firstIndex),
@@ -964,16 +995,28 @@ void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
m_renderer.GetOcclusionQueries().Publish();
// 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);
}
};
// EndOfPipeData: kNone, kImmediate32b, kImmediate64b, kRefClk; 5 reads GDS.
switch (data_sel) {
case 0: break;
case 1: {
const auto value = static_cast<uint32_t>(data);
std::memcpy(address, &value, sizeof(value));
write_label(&value, sizeof(value));
break;
}
case 3: data = Sync::ReadReferenceClock(); [[fallthrough]];
case 2: std::memcpy(address, &data, sizeof(data)); break;
case 2: write_label(&data, sizeof(data)); break;
case 5: {
GetScheduler().Finish();
const auto gds = static_cast<uint32_t>(data);
-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;
};
+13 -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);
@@ -299,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) {
@@ -318,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);
}
});
}
@@ -719,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) {
+8 -9
View File
@@ -13,6 +13,7 @@
#include <array>
#include <map>
#include <mutex>
#include <span>
#include <utility>
#include <vector>
@@ -85,19 +86,16 @@ public:
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 {
@@ -122,7 +120,6 @@ 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);
@@ -144,6 +141,8 @@ private:
// 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;
+103 -105
View File
@@ -251,24 +251,24 @@ 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);
if (image.info.metadata.kind == ImageMetadataKind::Htile) {
m_surface_metas.erase(image.info.metadata.range.address);
}
UnregisterImage(id);
if (m_scheduler.Active()) {
@@ -429,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);
@@ -442,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;
}
@@ -496,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 ||
@@ -515,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,
@@ -558,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);
@@ -787,15 +779,22 @@ 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 &&
@@ -869,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 {}};
}
@@ -1218,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;
}
}
@@ -1313,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);
}
}
@@ -1381,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);
@@ -1405,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;
}
@@ -1430,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;
}
@@ -1543,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;
@@ -1904,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);
}
}
}
@@ -2060,8 +2059,7 @@ void TextureCache::ProcessDownloadImages() {
}
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);
}
}
+1 -1
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,
@@ -184,7 +185,6 @@ 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;
-7
View File
@@ -595,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_;
@@ -425,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();
+4 -1
View File
@@ -139,14 +139,17 @@ public:
}
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 {};
@@ -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;
@@ -484,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);
@@ -604,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) {
@@ -672,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();
}
@@ -683,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};
@@ -725,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)]);
@@ -782,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)]);
@@ -803,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);
@@ -815,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());
@@ -823,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);
@@ -864,8 +867,8 @@ void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const>
bool uses_image_heap = false;
FindBuffers(stages);
for (auto* stage: stages) {
uses_dma |= stage->runtime->program->info.uses_dma;
uses_image_heap |= stage->runtime->program->info.uses_image_heap;
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();
@@ -879,12 +882,9 @@ void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const>
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);
@@ -917,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);
@@ -938,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");
@@ -956,7 +955,7 @@ 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];
@@ -982,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);
@@ -12,6 +12,7 @@
#include "kernel/memory.h"
#include <algorithm>
#include <bit>
#include <xxhash.h>
namespace Libs::Graphics {
@@ -19,7 +20,7 @@ namespace {
namespace IR = ShaderRecompiler::IR;
// Smaller tables are cheap to materialize on each draw and are often transient.
// 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;
@@ -44,6 +45,20 @@ void Widen(size_t& begin, size_t& end, size_t first, size_t last) {
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);
@@ -118,26 +133,25 @@ ImageHeap::~ImageHeap() {
bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& result) {
KYTY_PROFILER_FUNCTION();
const auto size = request.size & ~uint64_t {3};
if (size < MinTableBytes || request.count == 0u || request.step == 0u ||
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 && candidate->size == size;
});
if (memory == m_memories.end()) {
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;
auto created = std::make_unique<Memory>();
created->base = request.base;
created->size = size;
created->words.resize(size / 4u);
created->dead.resize(size / 4u);
created->dirty_pages.assign((Common::AlignUp(request.base + size, TRACKER_PAGE_SIZE) -
Common::AlignDown(request.base, TRACKER_PAGE_SIZE)) /
TRACKER_PAGE_SIZE,
uint8_t {1});
std::scoped_lock lock {m_mutex};
memory = m_memories.insert(m_memories.end(), std::move(created));
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.
@@ -146,27 +160,28 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView&
(*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->count == request.count && candidate->profile == profile;
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 = request.count,
.count = count,
.limit = limit,
.profile = profile,
.resolve_epoch = epoch,
.layout_epoch = epoch});
created->slots.resize(size / 4u + 1u);
created->buffer = std::make_unique<Buffer>(
m_context.GetGraphics(), m_context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress,
size + 4u);
UpdateProbes(*created, 0u, size);
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();
@@ -254,10 +269,12 @@ void ImageHeap::Unwatch(const Memory& memory) {
void ImageHeap::InvalidateMemory(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_mutex};
for (auto& memory: m_memories) {
const auto begin = std::max(address, memory->base);
const auto end = std::min(address + size, memory->base + memory->size);
if (begin >= end) continue;
// 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);
}
@@ -350,12 +367,17 @@ void ImageHeap::Release(View& view, uint32_t entry) {
}
void ImageHeap::Unreference(Element& element) {
if (auto* image = m_context.GetTextureCache().m_slot_images.try_get(element.binding.image_id)) {
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)) {
@@ -376,15 +398,56 @@ uint32_t ImageHeap::Allocate() {
}
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; };
@@ -393,7 +456,7 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
for (const auto& [value, index]: view->elements) {
const auto& binding = m_elements[index].binding;
if (!binding.image_id) continue;
const auto* image = textures.m_slot_images.try_get(binding.image_id);
const auto* image = ImageOf(m_elements[index]);
if (image == nullptr || !image->registered || image->depth_id ||
image->binding.needs_rebind) {
mark(index);
@@ -413,6 +476,19 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
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.
@@ -434,12 +510,15 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
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);
@@ -471,8 +550,7 @@ void ImageHeap::WriteDescriptors() {
void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
vk::PipelineLayout layout) {
KYTY_PROFILER_FUNCTION();
auto& textures = m_context.GetTextureCache();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
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,
@@ -493,7 +571,7 @@ void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
// 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 = textures.m_slot_images.try_get(element.binding.image_id);
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) {
@@ -519,26 +597,42 @@ void ImageHeap::UnmapMemory(uint64_t address, uint64_t size) {
} else {
m_unmapped.push_back({address, size});
}
auto& scheduler = m_context.GetCommandScheduler();
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;
}
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)] {});
}
}
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
@@ -22,10 +22,11 @@ class RenderExecutor;
struct ImageHeapTestAccess;
// Guest image tables that shaders index by GPU-computed offsets, kept resident in one bindless
// sampled-image array. 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.
// 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);
@@ -46,6 +47,8 @@ public:
// 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;
@@ -57,9 +60,10 @@ private:
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;
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;
@@ -69,6 +73,7 @@ private:
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;
@@ -78,6 +83,7 @@ private:
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;
@@ -85,6 +91,7 @@ private:
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 {
@@ -102,6 +109,7 @@ private:
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);
@@ -109,8 +117,10 @@ private:
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;
@@ -118,9 +128,11 @@ private:
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_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;
File diff suppressed because it is too large. Load diff
@@ -4,50 +4,62 @@
#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)
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] = {};
@@ -58,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) == 117);
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:
PipelineCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap);
// 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();
@@ -121,87 +186,106 @@ public:
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"
@@ -90,7 +91,7 @@ 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());
// Address writes reach memory outside the bound buffer descriptors.
@@ -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;
@@ -242,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",
@@ -287,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 =
@@ -303,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] {};
@@ -323,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 {};
@@ -382,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;
@@ -413,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;
@@ -470,17 +294,13 @@ 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);
pipeline.uses_image_heap |= stage.stage.program->info.uses_image_heap;
graphics_stages |= native_stage;
}
if (ps_active) {
EXIT_IF(!ps_input_info->stage);
AddLayoutBindings(descriptor_bindings, *ps_input_info->stage.program,
vk::ShaderStageFlagBits::eFragment);
pipeline.uses_image_heap |= ps_input_info->stage.program->info.uses_image_heap;
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,
@@ -498,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);
@@ -508,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,
@@ -555,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;
@@ -573,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);
@@ -586,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;
@@ -616,9 +425,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
AddLayoutBindings(descriptor_bindings, *input_info.stage.program,
vk::ShaderStageFlagBits::eCompute);
pipeline.uses_image_heap = input_info.stage.program->info.uses_image_heap;
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};
@@ -632,18 +440,12 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
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 (input_info.stage.program->info.uses_gds) {
if (module.info.info.uses_gds) {
info.flags = vk::PipelineCreateFlagBits::eDispatchBase;
}
info.stage = comp_shader_stage_info;
@@ -652,15 +454,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
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
+6 -11
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;
@@ -50,9 +45,10 @@ enum class DrawOffsetSource : uint8_t {
// 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;
uint64_t address = 0;
uint32_t count = 0;
uint32_t stride = 0;
ShaderIndirectOffsets offsets;
};
struct DrawIndexArgs {
@@ -198,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,
@@ -53,7 +53,7 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
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");
@@ -135,7 +135,7 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
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.program;
const auto& program = input.stage.module->info;
const auto& resources = *input.stage.resources;
const auto& fill = resources.uniform_fill;
auto& cache = command.GetContext().GetTextureCache();
@@ -208,7 +208,7 @@ bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
image_clear ? "image clear" : "buffer fill", input.stage.module->info.shader_hash,
descriptor.Base48(), size, packed_clear);
}
return true;
@@ -216,7 +216,7 @@ bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
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;
}
@@ -248,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,
@@ -293,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;
@@ -312,16 +310,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
}
m_context.GetImageHeap().BeginDraw();
const auto compute_program =
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
if (!compute_program) return;
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() &&
(TryConsumeComputeMetaClear(input_info, buffer) ||
@@ -351,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) {
@@ -502,16 +497,14 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
input_info.async_compute = async_compute;
input_info.indirect_args_address = args_addr;
m_context.GetImageHeap().BeginDraw();
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
if (!compute_program) return;
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();
}
@@ -10,10 +10,10 @@
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_image_heap), 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_image_heap(*this, m_page_manager), m_occlusion_queries(graphics, m_command_scheduler) {
@@ -134,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;
}
@@ -143,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();
@@ -31,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);
+57 -120
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 || sh_regs.AlphaToMask();
}
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,
@@ -870,62 +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();
buffer.GetContext().GetImageHeap().BeginDraw();
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) {
@@ -1124,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;
@@ -1203,10 +1143,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
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,
@@ -1238,7 +1175,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (mesh_active) {
draw_data = {draw.index_count,
static_cast<uint32_t>(emit.vertex_offset),
emit.first_instance,
index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(index_source.address >> 32u)};
@@ -1314,8 +1250,9 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
}
}
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need CPU counts, as
// mesh and quad-list draws do. Indirect records carry their own offsets.
// 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,
@@ -1323,8 +1260,9 @@ static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig&
uint32_t first_instance,
const DrawIndirectArgs& indirect) {
if (indirect.address != 0) {
if (vs.stage.program->stage == ShaderType::Mesh ||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) {
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};
@@ -1388,8 +1326,8 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer);
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, topology)) {
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) {
return true;
}
@@ -1410,7 +1348,7 @@ bool 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) {
@@ -1418,7 +1356,8 @@ bool 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);
@@ -1426,7 +1365,6 @@ bool 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 true;
@@ -1437,8 +1375,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
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, topology, *emit, index_source,
primitive_restart);
ExecutePreparedDraw(submit_id, buffer, draw, state, *emit, index_source);
}
ResetBindings();
return emit.has_value();
@@ -1494,12 +1431,11 @@ bool 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 true;
}
DrawRenderState state {};
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
ResetBindings();
return true;
@@ -1507,7 +1443,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
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: "
@@ -1524,7 +1460,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
static_cast<int32_t>(args.first_vertex), args.first_instance,
args.indirect);
if (emit) {
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, *emit, {}, false);
ExecutePreparedDraw(submit_id, buffer, draw, state, *emit, {});
}
ResetBindings();
return emit.has_value();
@@ -1559,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;
}
@@ -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]));
}
@@ -533,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")) {
@@ -51,6 +51,7 @@ struct VulkanExtensions {
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
vk::PhysicalDeviceVulkan11Features features {};
features.storageBuffer16BitAccess = VK_TRUE;
features.shaderDrawParameters = VK_TRUE;
return features;
}
@@ -278,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",
@@ -337,6 +340,7 @@ 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");
@@ -636,6 +640,7 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
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;
@@ -733,6 +738,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
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());
@@ -1111,7 +1125,8 @@ void WindowContext::CreateVulkan() {
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME,
VK_EXT_CONDITIONAL_RENDERING_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);
}
@@ -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()),
@@ -484,6 +467,7 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
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;
source.calls.push_back({.instruction = i});
@@ -520,7 +504,8 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
};
Register = [&](uint32_t before, uint32_t code) -> Value {
std::vector<uint32_t> crossed_calls;
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, 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);
}
@@ -584,14 +569,30 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
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;
@@ -617,12 +618,18 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
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);
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 (!walker.Evaluate(site.target[0], low) || !walker.Evaluate(site.target[1], high))
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.
@@ -646,7 +653,7 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
break;
}
linked.observed_function.resize(function.size);
if (!clean.read_memory(clean.userdata, source.calls[i].address, linked.observed_function) ||
if (!read_code(source.calls[i].address, linked.observed_function) ||
!std::ranges::equal(expected, linked.observed_function)) {
unchanged = false;
break;
@@ -676,8 +683,7 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
uint32_t count = 1;
Decoder::Instruction inst;
for (;;) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(count)))
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);
@@ -698,10 +704,11 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
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) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
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))
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);
@@ -756,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>(
@@ -765,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;
@@ -795,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());
}
@@ -813,7 +819,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
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()));
}
}
@@ -826,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] {
@@ -842,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();
@@ -857,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;
@@ -877,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.
@@ -923,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);
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("%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 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()));
@@ -22,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;
const Spirv::Profile* profile = &Spirv::DefaultProfile;
const char* dump_label = nullptr;
std::span<const uint32_t> user_data;
std::span<const uint32_t> back_code;
ShaderStageInputInfo input_info;
@@ -64,11 +62,13 @@ struct ShaderSource {
IR::ResourcePlan call_targets;
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);
@@ -54,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)),
@@ -92,28 +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) {
// 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 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);
@@ -199,7 +198,8 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, EmitF32ToF16RtzBits(state, arg0), EmitF32ToF16RtzBits(state, arg1),
state, EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg0)),
EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg1)),
ConstantU32(state, 16), ConstantU32(state, 16));
}
@@ -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;
@@ -63,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);
@@ -114,6 +115,18 @@ 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;
@@ -135,9 +148,24 @@ uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& fl
// 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 && row_shift) {
if (flags.bound_control || (flags.fetch_inactive && !row_shift)) {
return writable;
}
// 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,
EmitDppTargetLane(state, flags).valid);
EmitSourceLaneActive(ctx, exec, source));
}
return writable;
}
@@ -264,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;
@@ -303,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;
@@ -317,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));
}
}
@@ -333,12 +364,10 @@ 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) {
@@ -536,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 =
@@ -684,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),
@@ -784,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);
@@ -794,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();
@@ -816,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,
@@ -852,14 +901,11 @@ 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) {
@@ -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;
}
@@ -810,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;
@@ -845,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};
};
@@ -1069,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;
@@ -1081,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;
@@ -1104,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,6 +56,7 @@ 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)
@@ -229,10 +230,12 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& in
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);
@@ -288,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;
@@ -112,6 +112,7 @@ struct EmitterState {
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;
@@ -145,6 +146,7 @@ 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;
@@ -282,6 +284,7 @@ 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;
};
@@ -305,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);
@@ -414,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);
@@ -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);
@@ -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();
@@ -154,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;
@@ -174,6 +175,8 @@ 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);
}
}
}
@@ -257,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;
}
@@ -472,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));
@@ -565,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:
@@ -476,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;
}
}
@@ -526,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:
@@ -595,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) {
@@ -691,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 &&
@@ -793,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);
}
@@ -235,7 +235,7 @@ void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto op = inst.GetOpcode();
if (ctx.half != 0 &&
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
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));
@@ -746,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) {
@@ -1710,21 +1710,25 @@ Graph Structurize(const Graph& graph) {
return GotoStructurizer(graph).Run();
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
// Vector comparisons and carry results can write a scalar pair in wave64.
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 + 1u)
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, std::vector<uint32_t>& crossed_calls) {
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;
@@ -1743,11 +1747,11 @@ uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& grap
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));
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code, wave_size));
crossed_calls.push_back(i);
continue;
}
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
if (MayWriteScalarRegister(inst, code, wave_size)) { index = i; break; }
}
if (index != UINT32_MAX || block->id == graph.entry_block) {
EXIT_IF(definition && *definition != index);
@@ -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, std::vector<uint32_t>& crossed_calls);
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);
@@ -14,12 +14,6 @@ struct MimgSampleInfo {
uint32_t flags = 0;
};
struct MimgGatherInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::UNSUPPORTED;
uint32_t flags = 0;
};
struct MimgAtomicInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::UNSUPPORTED;
@@ -73,7 +67,7 @@ constexpr uint32_t ImageSampleAddressComponents(uint32_t flags, ImageDimension d
if ((flags & ImageSampleFlagBias) != 0) {
components++;
}
if ((flags & ImageSampleFlagLod) != 0) {
if ((flags & (ImageSampleFlagLod | ImageSampleFlagLodClamp)) != 0) {
components++;
}
if ((flags & ImageSampleFlagDerivative) != 0) {
@@ -181,23 +175,72 @@ constexpr MimgSampleInfo MIMG_SAMPLE_OPCODE_LIST[] = {
ImageSampleFlagOffset | ImageSampleFlagAdjust},
};
constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x44u, Opcode::IMAGE_GATHER4_L, ImageSampleFlagLod},
{0x47u, Opcode::IMAGE_GATHER4_LZ, ImageSampleFlagLevelZero},
{0x48u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare},
{0x4cu, Opcode::IMAGE_GATHER4_C_L, ImageSampleFlagCompare | ImageSampleFlagLod},
{0x4fu, Opcode::IMAGE_GATHER4_C_LZ,
ImageSampleFlagCompare | ImageSampleFlagLevelZero},
{0x57u, Opcode::IMAGE_GATHER4_LZ_O,
ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x58u, Opcode::IMAGE_GATHER4_C_O,
ImageSampleFlagCompare | ImageSampleFlagOffset},
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
constexpr MimgSampleInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x40u, "image_gather4", 0},
{0x41u, "image_gather4_cl", ImageSampleFlagLodClamp},
{0x44u, "image_gather4_l", ImageSampleFlagLod},
{0x45u, "image_gather4_b", ImageSampleFlagBias},
{0x46u, "image_gather4_b_cl", ImageSampleFlagBias | ImageSampleFlagLodClamp},
{0x47u, "image_gather4_lz", ImageSampleFlagLevelZero},
{0x48u, "image_gather4_c", ImageSampleFlagCompare},
{0x49u, "image_gather4_c_cl", ImageSampleFlagCompare | ImageSampleFlagLodClamp},
{0x4cu, "image_gather4_c_l", ImageSampleFlagCompare | ImageSampleFlagLod},
{0x4du, "image_gather4_c_b", ImageSampleFlagCompare | ImageSampleFlagBias},
{0x4eu, "image_gather4_c_b_cl",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp},
{0x4fu, "image_gather4_c_lz", ImageSampleFlagCompare | ImageSampleFlagLevelZero},
{0x50u, "image_gather4_o", ImageSampleFlagOffset},
{0x51u, "image_gather4_cl_o", ImageSampleFlagLodClamp | ImageSampleFlagOffset},
{0x54u, "image_gather4_l_o", ImageSampleFlagLod | ImageSampleFlagOffset},
{0x55u, "image_gather4_b_o", ImageSampleFlagBias | ImageSampleFlagOffset},
{0x56u, "image_gather4_b_cl_o",
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagOffset},
{0x57u, "image_gather4_lz_o", ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x58u, "image_gather4_c_o", ImageSampleFlagCompare | ImageSampleFlagOffset},
{0x59u, "image_gather4_c_cl_o",
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagOffset},
{0x5cu, "image_gather4_c_l_o",
ImageSampleFlagCompare | ImageSampleFlagLod | ImageSampleFlagOffset},
{0x5du, "image_gather4_c_b_o",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagOffset},
{0x5eu, "image_gather4_c_b_cl_o",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
ImageSampleFlagOffset},
{0x5fu, "image_gather4_c_lz_o",
ImageSampleFlagCompare | ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
{0xc8u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare | ImageSampleFlagAdjust},
{0xd8u, Opcode::IMAGE_GATHER4_C_O,
{0x61u, "image_gather4h", ImageSampleFlagGatherHorizontal},
{0xc0u, "image_gather4_a", ImageSampleFlagAdjust},
{0xc1u, "image_gather4_cl_a", ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
{0xc5u, "image_gather4_b_a", ImageSampleFlagBias | ImageSampleFlagAdjust},
{0xc6u, "image_gather4_b_cl_a",
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
{0xc8u, "image_gather4_c_a", ImageSampleFlagCompare | ImageSampleFlagAdjust},
{0xc9u, "image_gather4_c_cl_a",
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
{0xcdu, "image_gather4_c_b_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagAdjust},
{0xceu, "image_gather4_c_b_cl_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
ImageSampleFlagAdjust},
{0xd0u, "image_gather4_o_a", ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd1u, "image_gather4_cl_o_a",
ImageSampleFlagLodClamp | ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd5u, "image_gather4_b_o_a",
ImageSampleFlagBias | ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd6u, "image_gather4_b_cl_o_a",
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagOffset |
ImageSampleFlagAdjust},
{0xd8u, "image_gather4_c_o_a",
ImageSampleFlagCompare | ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd9u, "image_gather4_c_cl_o_a",
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagOffset |
ImageSampleFlagAdjust},
{0xddu, "image_gather4_c_b_o_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagOffset |
ImageSampleFlagAdjust},
{0xdeu, "image_gather4_c_b_cl_o_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
ImageSampleFlagOffset | ImageSampleFlagAdjust},
};
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
@@ -219,13 +262,13 @@ constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCO
constexpr auto MIMG_GATHER_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_GATHER_OPCODE_LIST);
constexpr auto MIMG_ATOMIC_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_ATOMIC_OPCODE_LIST);
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgGatherInfo* gather,
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgSampleInfo* gather,
const MimgAtomicInfo* atomic) {
if (sample != nullptr) {
return Opcode::IMAGE_SAMPLE;
}
if (gather != nullptr) {
return gather->decoded;
return Opcode::IMAGE_GATHER4;
}
if (atomic != nullptr) {
return atomic->decoded;
@@ -244,24 +287,10 @@ Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const Mim
}
}
uint32_t DecodeMimgSampleFlags(const MimgSampleInfo* sample, const MimgGatherInfo* gather) {
if (sample != nullptr) {
return sample->flags;
}
if (gather != nullptr) {
return gather->flags;
}
return 0;
}
uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
const MimgSampleInfo* sample, const MimgGatherInfo* gather,
const MimgAtomicInfo* atomic) {
if (sample != nullptr) {
return ImageSampleAddressComponents(sample->flags, dimension);
}
if (gather != nullptr) {
return ImageSampleAddressComponents(gather->flags, dimension);
const MimgSampleInfo* sampled, const MimgAtomicInfo* atomic) {
if (sampled != nullptr) {
return ImageSampleAddressComponents(sampled->flags, dimension);
}
if (atomic != nullptr) {
return ImageCoordComponents(dimension);
@@ -290,33 +319,31 @@ uint32_t CountDmaskComponents(uint32_t dmask) {
} // namespace
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component) {
const auto width = [flags](uint32_t index) {
if ((flags & ImageSampleFlagA16) == 0u) return 32u;
uint32_t cursor = 0;
if ((flags & ImageSampleFlagOffset) != 0u) {
if (index == cursor++) return 32u;
}
if ((flags & ImageSampleFlagBias) != 0u) {
if (index == cursor++) return 16u;
}
if ((flags & ImageSampleFlagCompare) != 0u && index == cursor) return 32u;
return 16u;
};
uint32_t offset = 0;
for (uint32_t index = 0; index < component; index++) {
const auto bits = width(index);
if (bits == 32u) offset = (offset + 31u) & ~31u;
offset += bits;
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
uint32_t component) {
if ((flags & ImageSampleFlagA16) == 0u) return {component * 32u, 32u};
// A16 offsets, bias and PCF reference each take a whole VGPR, the bias as a half in its low
// 16 bits. The other components pack two per VGPR, except that each 1D derivative and the last
// two 3D ones take the low half of their own VGPR.
const bool has_bias = (flags & ImageSampleFlagBias) != 0u;
const uint32_t bias_index = (flags & ImageSampleFlagOffset) != 0u ? 1u : 0u;
const uint32_t whole_vgprs = std::popcount(
flags & (ImageSampleFlagOffset | ImageSampleFlagBias | ImageSampleFlagCompare));
if (component < whole_vgprs) {
return {component * 32u, has_bias && component == bias_index ? 16u : 32u};
}
const auto bits = width(component);
if (bits == 32u) offset = (offset + 31u) & ~31u;
return {offset, bits};
const uint32_t axes =
(flags & ImageSampleFlagDerivative) != 0u ? ImageGradientComponents(dimension) : 0u;
const uint32_t unpaired = axes % 2u * 2u;
const uint32_t paired = axes * 2u - unpaired;
const uint32_t index = component - whole_vgprs;
const uint32_t half = index + std::clamp(index, paired, paired + unpaired) - paired;
return {whole_vgprs * 32u + half * 16u, 16u};
}
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components) {
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components) {
if (components == 0u) return 0u;
const auto last = ImageAddressComponentLayout(flags, components - 1u);
const auto last = ImageAddressComponentLayout(flags, dimension, components - 1u);
return (last.bit_offset + last.bit_width + 31u) / 32u;
}
@@ -329,16 +356,17 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const auto dimension = DecodeImageDimension((word0 >> 3u) & 0x7u);
const uint32_t word_count = 2u + nsa_dwords;
const uint32_t vdata = (word1 >> 8u) & 0xffu;
const uint32_t vaddr = word1 & 0xffu;
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
const uint32_t vdata = (word1 >> 8u) & 0xffu;
const uint32_t vaddr = word1 & 0xffu;
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
const auto* sampled = sample != nullptr ? sample : gather;
inst.pc = pc;
inst.word_count = word_count;
@@ -351,11 +379,11 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.data_dwords = d16 ? (inst.data_components + 1u) / 2u : inst.data_components;
inst.glc = ((word0 >> 13u) & 1u) != 0;
inst.slc = ((word0 >> 25u) & 1u) != 0;
inst.image_sample_flags = DecodeMimgSampleFlags(sample, gather);
inst.image_sample_flags = sampled != nullptr ? sampled->flags : 0u;
if (a16) {
inst.image_sample_flags |= ImageSampleFlagA16;
}
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
if (sampled != nullptr && (word0 & (1u << 12u)) != 0u) {
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
}
inst.image_dimension = dimension;
@@ -364,8 +392,7 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
for (uint32_t i = 0; i < nsa_dwords * 4u; i++) {
inst.image_nsa_addr[i] = (code[word_index + 2u + i / 4u] >> ((i % 4u) * 8u)) & 0xffu;
}
inst.image_address_components =
DecodeMimgAddressComponents(opcode, dimension, sample, gather, atomic);
inst.image_address_components = DecodeMimgAddressComponents(opcode, dimension, sampled, atomic);
SetRawWords(inst, code, word_index, word_count);
if (inst.opcode == Opcode::UNSUPPORTED) {
@@ -389,8 +416,8 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
}
}
const bool supports_d16 = sample != nullptr || gather != nullptr || opcode == 0x00u ||
opcode == 0x01u || opcode == 0x08u || opcode == 0x09u;
const bool supports_d16 = sampled != nullptr || opcode == 0x00u || opcode == 0x01u ||
opcode == 0x08u || opcode == 0x09u;
if (d16 && !supports_d16) {
SetUnsupported(inst, Family::MIMG, opcode, "MIMG opcode does not support D16 data");
}
@@ -407,9 +434,10 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.src_count = 3;
}
const char* MimgSampleOpcodeName(uint32_t opcode) {
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
return sample != nullptr ? sample->name : nullptr;
const char* MimgSampledOpcodeName(uint32_t opcode) {
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
const auto* sampled = sample != nullptr ? sample : Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
return sampled != nullptr ? sampled->name : nullptr;
}
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
@@ -8,15 +8,16 @@ namespace Libs::Graphics::ShaderRecompiler::Decoder {
void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
Instruction& inst);
const char* MimgSampleOpcodeName(uint32_t opcode);
const char* MimgSampledOpcodeName(uint32_t opcode);
struct ImageAddressComponent {
uint32_t bit_offset;
uint32_t bit_width;
};
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component);
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components);
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
uint32_t component);
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components);
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
@@ -22,7 +22,7 @@ constexpr MemoryOpcodeInfo SMEM_OPCODE_LIST[] = {
{0x04u, Opcode::S_LOAD_DWORDX16, 16, 32}, {0x08u, Opcode::S_BUFFER_LOAD_DWORD, 1, 32},
{0x09u, Opcode::S_BUFFER_LOAD_DWORDX2, 2, 32}, {0x0au, Opcode::S_BUFFER_LOAD_DWORDX4, 4, 32},
{0x0bu, Opcode::S_BUFFER_LOAD_DWORDX8, 8, 32}, {0x0cu, Opcode::S_BUFFER_LOAD_DWORDX16, 16, 32},
{0x25u, Opcode::S_MEMREALTIME, 2, 32},
{0x20u, Opcode::S_DCACHE_INV, 0, 0}, {0x25u, Opcode::S_MEMREALTIME, 2, 32},
};
constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
@@ -69,6 +69,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
{0x56u, Opcode::BUFFER_ATOMIC_UMIN_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
@@ -95,14 +96,14 @@ constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
};
constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
{0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true},
{0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32},
{0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32},
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16},
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
{0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true},
{0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32},
{0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32},
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16}, {0x25u, Opcode::FLAT_LOAD_SHORT_D16_HI, 1, 16},
};
constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
@@ -285,6 +286,9 @@ void DecodeSmem(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
if (inst.opcode == Opcode::UNSUPPORTED) {
SetUnsupported(inst, Family::SMEM, opcode, "SMEM opcode is not implemented");
}
if (inst.opcode == Opcode::S_DCACHE_INV) {
return;
}
DecodeScalarDestination(sdst, pc, inst.dst);
if (inst.opcode == Opcode::S_MEMREALTIME) {
@@ -419,9 +423,10 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst);
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) {
// D16 loads reuse partial destinations to preserve the untouched high half.
inst.dst.sdwa_sel = 4u;
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16 ||
inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI) {
// D16 loads reuse partial destinations to preserve the untouched half.
inst.dst.sdwa_sel = inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI ? 5u : 4u;
}
DecodeVectorGpr(addr, inst.src0);
inst.src_count = 1;
@@ -123,10 +123,9 @@ std::string ImageSampleFlagsToString(uint32_t flags) {
}
std::string FormatMimg(const Instruction& inst) {
const char* sample_name =
inst.opcode == Opcode::IMAGE_SAMPLE ? MimgSampleOpcodeName(inst.opcode_id) : nullptr;
const char* sampled_name = MimgSampledOpcodeName(inst.opcode_id);
const std::string_view name =
sample_name != nullptr ? sample_name : magic_enum::enum_name(inst.opcode);
sampled_name != nullptr ? sampled_name : magic_enum::enum_name(inst.opcode);
std::string text =
fmt::format("0x{:08x}: {} {}, {}, {}, {} ; dmask=0x{:x} image_dim={}", inst.pc, name,
OperandToString(inst.dst).c_str(), OperandToString(inst.src0).c_str(),
@@ -138,22 +137,10 @@ std::string FormatMimg(const Instruction& inst) {
if (inst.image_r128) {
text += " r128=1";
}
switch (inst.opcode) {
case Opcode::IMAGE_SAMPLE:
case Opcode::IMAGE_GATHER4_L:
case Opcode::IMAGE_GATHER4_LZ:
case Opcode::IMAGE_GATHER4_C:
case Opcode::IMAGE_GATHER4_C_L:
case Opcode::IMAGE_GATHER4_C_LZ:
case Opcode::IMAGE_GATHER4_LZ_O:
case Opcode::IMAGE_GATHER4_C_O:
case Opcode::IMAGE_GATHER4_C_LZ_O:
case Opcode::IMAGE_GATHER4H:
text += fmt::format(" sample_flags={} addr_components={}",
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
inst.image_address_components);
break;
default: break;
if (sampled_name != nullptr) {
text += fmt::format(" sample_flags={} addr_components={}",
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
inst.image_address_components);
}
if (inst.image_nsa_dwords != 0) {
text += fmt::format(" nsa_dwords={} nsa_addr=", inst.image_nsa_dwords);
@@ -258,7 +245,9 @@ void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand) {
case 126u: operand.kind = OperandKind::ExecLo; return;
case 127u: operand.kind = OperandKind::ExecHi; return;
case 235u: operand.kind = OperandKind::SharedBase; return;
case 236u: operand.kind = OperandKind::SharedLimit; return;
case 237u: operand.kind = OperandKind::PrivateBase; return;
case 238u: operand.kind = OperandKind::PrivateLimit; return;
case 239u: operand.kind = OperandKind::PopsExitingWaveId; return;
case 248u:
operand.kind = OperandKind::FloatInlineConstant;
@@ -459,7 +448,9 @@ std::string OperandToString(const Operand& operand) {
case OperandKind::M0: text = "m0"; break;
case OperandKind::PopsExitingWaveId: text = "pops_exiting_wave_id"; break;
case OperandKind::SharedBase: text = "shared_base"; break;
case OperandKind::SharedLimit: text = "shared_limit"; break;
case OperandKind::PrivateBase: text = "private_base"; break;
case OperandKind::PrivateLimit: text = "private_limit"; break;
case OperandKind::Null: text = "null"; break;
default: text = "unknown"; break;
}
@@ -566,6 +557,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.src0).c_str()));
case Opcode::S_BARRIER:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
case Opcode::S_DCACHE_INV:
case Opcode::BUFFER_GL0_INV:
case Opcode::BUFFER_GL1_INV:
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
@@ -612,15 +604,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::IMAGE_LOAD_MIP:
case Opcode::IMAGE_GET_RESINFO:
case Opcode::IMAGE_GET_LOD:
case Opcode::IMAGE_GATHER4_L:
case Opcode::IMAGE_GATHER4_LZ:
case Opcode::IMAGE_GATHER4_C:
case Opcode::IMAGE_GATHER4_C_L:
case Opcode::IMAGE_GATHER4_C_LZ:
case Opcode::IMAGE_GATHER4_LZ_O:
case Opcode::IMAGE_GATHER4_C_O:
case Opcode::IMAGE_GATHER4_C_LZ_O:
case Opcode::IMAGE_GATHER4H: return WithUnsupportedReason(inst, FormatMimg(inst));
case Opcode::IMAGE_GATHER4: return WithUnsupportedReason(inst, FormatMimg(inst));
case Opcode::S_LOAD_DWORD:
case Opcode::S_LOAD_DWORDX2:
case Opcode::S_LOAD_DWORDX4:
@@ -681,6 +665,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_ATOMIC_SUB:
case Opcode::BUFFER_ATOMIC_SMIN:
case Opcode::BUFFER_ATOMIC_UMIN:
case Opcode::BUFFER_ATOMIC_UMIN_X2:
case Opcode::BUFFER_ATOMIC_SMAX:
case Opcode::BUFFER_ATOMIC_UMAX:
case Opcode::BUFFER_ATOMIC_AND:
@@ -697,6 +682,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::FLAT_LOAD_USHORT:
case Opcode::FLAT_LOAD_SSHORT:
case Opcode::FLAT_LOAD_SHORT_D16:
case Opcode::FLAT_LOAD_SHORT_D16_HI:
case Opcode::FLAT_LOAD_DWORD:
case Opcode::FLAT_LOAD_DWORDX2:
case Opcode::FLAT_LOAD_DWORDX3:
@@ -544,6 +544,7 @@ enum class Opcode {
S_BUFFER_LOAD_DWORDX8,
S_BUFFER_LOAD_DWORDX16,
S_MEMREALTIME,
S_DCACHE_INV,
BUFFER_LOAD_FORMAT_X,
BUFFER_LOAD_FORMAT_XY,
BUFFER_LOAD_FORMAT_XYZ,
@@ -596,6 +597,7 @@ enum class Opcode {
BUFFER_ATOMIC_SUB,
BUFFER_ATOMIC_SMIN,
BUFFER_ATOMIC_UMIN,
BUFFER_ATOMIC_UMIN_X2,
BUFFER_ATOMIC_SMAX,
BUFFER_ATOMIC_UMAX,
BUFFER_ATOMIC_AND,
@@ -612,6 +614,7 @@ enum class Opcode {
FLAT_LOAD_USHORT,
FLAT_LOAD_SSHORT,
FLAT_LOAD_SHORT_D16,
FLAT_LOAD_SHORT_D16_HI,
FLAT_LOAD_DWORD,
FLAT_LOAD_DWORDX2,
FLAT_LOAD_DWORDX3,
@@ -707,15 +710,7 @@ enum class Opcode {
IMAGE_ATOMIC_FMIN,
IMAGE_ATOMIC_FMAX,
IMAGE_SAMPLE,
IMAGE_GATHER4_L,
IMAGE_GATHER4_LZ,
IMAGE_GATHER4_C,
IMAGE_GATHER4_C_L,
IMAGE_GATHER4_C_LZ,
IMAGE_GATHER4_LZ_O,
IMAGE_GATHER4_C_O,
IMAGE_GATHER4_C_LZ_O,
IMAGE_GATHER4H,
IMAGE_GATHER4,
V_INTERP_P1_F32,
V_INTERP_P2_F32,
V_INTERP_MOV_F32,
@@ -766,7 +761,9 @@ enum class OperandKind {
M0,
PopsExitingWaveId,
SharedBase,
SharedLimit,
PrivateBase,
PrivateLimit,
Null,
Vgpr,
};
@@ -5,30 +5,20 @@
namespace Libs::Graphics::ShaderRecompiler::Frontend {
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar,
bool cmpx) {
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar) {
if (scalar) {
ir.SetScc(value);
return;
}
const auto masked = ir.LogicalAnd(ir.GetExec(), value);
if (cmpx) {
const auto mask = BallotMask(masked);
ir.SetExec(masked);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return;
}
WriteMask(inst.dst, masked);
WriteVectorMask(inst.dst, value);
}
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar,
bool cmpx) {
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar, cmpx);
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar) {
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar);
}
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
IR::Type type, bool scalar, bool cmpx, bool swap_operands) {
IR::Type type, bool scalar, bool swap_operands) {
// The equality compares share their IR opcode with the unsigned ones.
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
@@ -49,18 +39,18 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
if (swap_operands) {
std::swap(lhs, rhs);
}
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar);
}
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx) {
bool signed_value) {
const auto lhs = ReadU16AsU32(inst.src0, signed_value);
const auto rhs = ReadU16AsU32(inst.src1, signed_value);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false);
}
void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool half, bool cmpx) {
bool half) {
const auto type = IR::ArgTypeOf(opcode, 0);
const auto lhs =
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, type);
@@ -68,10 +58,10 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, type);
const IR::FPCompareFlags flags{
.flush_input_denorms = !half && type == IR::Type::F32 && flush_f32_inputs};
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false);
}
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered,
bool is64) {
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
@@ -79,16 +69,16 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
const auto rhs = ReadOperand(inst.src1, type);
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
IR::U1(ir.Emit(is_nan_op, {rhs})));
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false);
}
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half) {
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool half) {
const auto value = half ? IR::Value(Read16LaneBits(inst.src0, false))
: ReadOperand(inst.src0, IR::Type::F32);
const auto mask = half ? IR::Value(Read16LaneBits(inst.src1, false))
: ReadOperand(inst.src1, IR::Type::U32);
const auto opcode = half ? IR::ValueOpcode::FPCmpClass16 : IR::ValueOpcode::FPCmpClass32;
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false);
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -51,33 +51,33 @@ void Translator::S_SUBVECTOR_LOOP(const Decoder::Instruction& inst, bool begin)
void Translator::S_SAVEEXEC(const Decoder::Instruction& inst, IR::ValueOpcode operation,
bool negate_exec, bool negate_source, bool write_64) {
if (!write_64) {
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
const auto old = ir.GetExecLo();
const auto src = ReadU32(inst.src0);
const auto combine = [&](IR::U32 old, IR::U32 src) {
const auto lhs = negate_exec ? ir.BitwiseNot(old) : old;
const auto rhs = negate_source ? ir.BitwiseNot(src) : src;
IR::U32 result;
switch (operation) {
case IR::ValueOpcode::LogicalAnd: result = ir.BitwiseAnd(lhs, rhs); break;
case IR::ValueOpcode::LogicalOr: result = ir.BitwiseOr(lhs, rhs); break;
default: EXIT("unsupported SAVEEXEC operation");
}
return operation == IR::ValueOpcode::LogicalAnd ? ir.BitwiseAnd(lhs, rhs)
: ir.BitwiseOr(lhs, rhs);
};
if (!write_64) {
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
const auto old = ir.GetExecLo();
const auto result = combine(old, ReadU32(inst.src0));
WriteRawU32(inst.dst, old);
WriteRawU32(ConditionOperand(Decoder::OperandKind::ExecLo), result);
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
return;
}
const auto old = ir.GetExec();
const auto src = ReadMask(inst.src0);
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
const auto result = IR::U1(ir.Emit(operation, {lhs, rhs}));
WriteMask(inst.dst, old, true);
const auto mask = BallotMask(result);
ir.SetExec(result);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
// EXEC words combine bitwise; the per-lane EXEC combines the per-lane masks, so helper lanes
// stay enabled.
const auto old = ir.GetExec();
const auto src = ReadMask(inst.src0);
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
const auto src_mask = ReadU32Pair(inst.src0);
const std::array old_mask {ir.GetExecLo(), ir.GetExecHi()};
const std::array mask {combine(old_mask[0], src_mask[0]), combine(old_mask[1], src_mask[1])};
WriteMask(inst.dst, old, old_mask, true);
WriteMask(ConditionOperand(Decoder::OperandKind::ExecLo), IR::U1(ir.Emit(operation, {lhs, rhs})),
mask);
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
}
@@ -95,7 +95,7 @@ void Translator::ADD_U32(const Decoder::Instruction& inst, bool vector, bool use
}
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), carry));
WriteVectorMask(inst.dst2, carry);
} else {
ir.SetScc(carry);
}
@@ -108,7 +108,7 @@ void Translator::SUB_U32(const Decoder::Instruction& inst, bool vector, bool rev
const auto borrow = ir.UGreaterThan(rhs, lhs);
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
WriteVectorMask(inst.dst2, borrow);
} else {
ir.SetScc(borrow);
}
@@ -126,7 +126,7 @@ void Translator::SUBB_U32(const Decoder::Instruction& inst, bool vector, bool re
const auto borrow = ir.LogicalOr(borrow0, borrow1);
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
WriteVectorMask(inst.dst2, borrow);
} else {
ir.SetScc(borrow);
}
@@ -465,9 +465,13 @@ void Translator::V_PERMLANE16_B32(const Decoder::Instruction& inst, bool x16) {
.fetch_inactive = inst.dst.op_sel,
.bound_control = inst.dst.op_sel_hi,
};
const auto result =
ir.Emit(IR::ValueOpcode::Permlane16U32,
{ReadU32(inst.src0), ReadU32(inst.src1), ReadU32(inst.src2), ir.GetExec()}, flags);
// An EXEC-disabled source lane without FI disables the write unless BOUND_CTRL reads zero.
const auto inactive = flags.fetch_inactive || flags.bound_control
? IR::U32(IR::Value(0u))
: ir.GetVectorReg(static_cast<IR::VectorReg>(inst.dst.reg));
const auto result = ir.Emit(
IR::ValueOpcode::Permlane16U32,
{ReadU32(inst.src0), ReadU32(inst.src1), ReadU32(inst.src2), ir.GetExec(), inactive}, flags);
auto dst = DestinationOperand(inst);
dst.op_sel = false;
dst.op_sel_hi = false;
@@ -49,18 +49,11 @@ void Translator::V_CVT_F16_16(const Decoder::Instruction& inst, bool signed_valu
}
void Translator::V_CVT_16_F16(const Decoder::Instruction& inst, bool signed_value) {
const auto value = ReadF16AsF32(inst.src0);
// The 32-bit conversion already saturates and maps NaN to zero.
if (signed_value) {
const auto converted = ir.Emit(IR::ValueOpcode::ConvertS32F32, {value});
const auto low = ir.Emit(IR::ValueOpcode::SMax32, {converted, IR::Value(0xffff8000u)});
Write16Bits(DestinationOperand(inst),
IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {low, IR::Value(0x7fffu)})));
return;
}
const auto converted = ir.Emit(IR::ValueOpcode::ConvertU32F32, {value});
Write16Bits(DestinationOperand(inst),
IR::U32(ir.Emit(IR::ValueOpcode::UMin32, {converted, IR::Value(0xffffu)})));
const auto converted = IR::U32(
ir.Emit(signed_value ? IR::ValueOpcode::ConvertS32F32 : IR::ValueOpcode::ConvertU32F32,
{ReadF16AsF32(inst.src0)}));
Write16Bits(DestinationOperand(inst), Saturate16(converted, signed_value));
}
void Translator::V_CVT_RPI_I32_F32(const Decoder::Instruction& inst) {
@@ -130,14 +123,30 @@ void Translator::V_CVT_PK_U8_F32(const Decoder::Instruction& inst) {
{ReadU32(SourceAt(inst, 2)), byte, shift, IR::Value(8u)}));
}
void Translator::V_CVT_PK_16_32(const Decoder::Instruction& inst, bool signed_value) {
WriteOperand(DestinationOperand(inst),
PackU16Lanes(Saturate16(ReadU32(inst.src0), signed_value),
Saturate16(ReadU32(inst.src1), signed_value)));
}
void Translator::V_PACK_B32_F16(const Decoder::Instruction& inst) {
WriteOperand(DestinationOperand(inst),
PackU16Lanes(Read16LaneBits(inst.src0, false), Read16LaneBits(inst.src1, false)));
}
IR::U32 Translator::PackU16Lanes(IR::U32 low, IR::U32 high) {
// A zero high lane zero-extends; descriptor tracking bounds a mask, not a bitfield insert.
if (high == IR::Value(0u)) return ir.BitwiseAnd(low, IR::U32(IR::Value(0xffffu)));
return IR::U32(
ir.Emit(IR::ValueOpcode::BitFieldInsert, {low, high, IR::Value(16u), IR::Value(16u)}));
}
IR::U32 Translator::Saturate16(IR::U32 value, bool signed_value) {
if (!signed_value) {
return IR::U32(ir.Emit(IR::ValueOpcode::UMin32, {value, IR::Value(0xffffu)}));
}
const auto low = ir.Emit(IR::ValueOpcode::SMax32, {value, IR::Value(0xffff8000u)});
return IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {low, IR::Value(0x7fffu)}));
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -1,6 +1,7 @@
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include <array>
#include <bit>
#include <utility>
namespace Libs::Graphics::ShaderRecompiler::Frontend {
@@ -118,6 +119,33 @@ void Translator::FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode op
WriteOperand(DestinationOperand(inst), ir.Emit(opcode, {ReadOperand(inst.src0, type)}));
}
void Translator::V_FRACT(const Decoder::Instruction& inst, IR::Type type) {
// x - floor(x) rounds to 1.0 for a tiny negative x; v_fract returns the largest value below
// one instead. The ordered compare lets NaN through.
const auto destination = DestinationOperand(inst);
if (type == IR::Type::F64) {
const auto result = ir.Emit(IR::ValueOpcode::FPFract64, {ReadOperand(inst.src0, type)});
const auto below_one = ir.Emit(IR::ValueOpcode::BitCastF64U64,
{IR::Value(uint64_t {0x3fefffffffffffff})});
const auto above = ir.Emit(IR::ValueOpcode::FPOrdGreaterThan64, {result, below_one});
WriteOperand(destination, ir.Emit(IR::ValueOpcode::SelectF64, {above, below_one, result}));
return;
}
const bool half = type == IR::Type::F16;
const auto source = half ? ReadF16AsF32(inst.src0) : IR::F32(ReadOperand(inst.src0, type));
const auto result = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {source}));
// Clamp in the destination precision: 0x3f7fe000 is the half 0x3bff.
const auto below_one =
IR::F32(IR::Value::F32(std::bit_cast<float>(half ? 0x3f7fe000u : 0x3f7fffffu)));
const auto above = IR::U1(ir.Emit(IR::ValueOpcode::FPOrdGreaterThan32, {result, below_one}));
const auto clamped = SelectF32(above, below_one, result);
if (half) {
WriteF16(destination, clamped);
} else {
WriteOperand(destination, clamped);
}
}
void Translator::FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool reverse) {
std::array<IR::Value, 2> args;
@@ -25,8 +25,7 @@ void Translator::V_MAD_I16(const Decoder::Instruction& inst) {
const auto rhs = ReadU16AsU32(inst.src1, true);
auto result = ir.IAdd(ir.IMul(lhs, rhs), ReadU16AsU32(inst.src2, true));
if (inst.dst.clamp) {
result = IR::U32(ir.Emit(IR::ValueOpcode::SMax32, {result, IR::Value(0xffff8000u)}));
result = IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {result, IR::Value(0x7fffu)}));
result = Saturate16(result, true);
}
Write16Bits(DestinationOperand(inst), result);
}
@@ -108,19 +107,7 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
default: return false;
}
};
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
const auto mask = WriteMask(inst.dst, invocation_result, true);
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
return;
}
// Descriptor tracking reasons about each scalar word independently.
const auto lhs = ReadU32Pair(inst.src0);
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
}
const auto lhs = ReadU32Pair(inst.src0);
std::array<IR::U32, 2> result;
if (unary) {
result = {ir.BitwiseNot(lhs[0]), ir.BitwiseNot(lhs[1])};
@@ -133,11 +120,21 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
result[component] = negate_result ? ir.BitwiseNot(IR::U32(value)) : IR::U32(value);
}
}
WriteU32Pair(inst.dst, result);
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
ir.SetThreadBitScalarReg(dst, invocation_result);
ir.SetScalarMaskTag(dst, mask_valid);
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
WriteMask(inst.dst, invocation_result, result, true);
} else {
// Descriptor tracking reasons about each scalar word independently.
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
}
WriteU32Pair(inst.dst, result);
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
ir.SetThreadBitScalarReg(dst, invocation_result);
ir.SetScalarMaskTag(dst, mask_valid);
}
}
ir.SetScc(ir.INotEqual(ir.BitwiseOr(result[0], result[1]), IR::U32(IR::Value(0u))));
}
@@ -293,7 +290,8 @@ void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
if (inst.dst2.kind != Decoder::OperandKind::Null &&
inst.dst2.kind != Decoder::OperandKind::Unknown) {
// The 32x32 product fits in 64 bits, so only the addition can carry.
WriteMask(inst.dst2, IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product})));
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
WriteVectorMask(inst.dst2, carry);
}
}
@@ -473,7 +471,7 @@ void Translator::S_BITCMP_B32(const Decoder::Instruction& inst, bool expected) {
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(31u)));
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {value, offset, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
}
void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
@@ -484,7 +482,7 @@ void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
ir.Select(ir.ULessThan(offset, IR::U32(IR::Value(32u))), value[0], value[1]);
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {word, word_bit, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
}
void Translator::V_ALIGN_B32(const Decoder::Instruction& inst, bool byte_offset) {
@@ -322,8 +322,8 @@ IR::Value Translator::MakeImageAddress(const Decoder::Instruction& inst,
const Decoder::Operand& base) {
std::array<IR::Value, 13> components {};
components.fill(IR::Value(0u));
const auto count =
Decoder::ImageAddressDwordCount(inst.image_sample_flags, inst.image_address_components);
const auto count = Decoder::ImageAddressDwordCount(
inst.image_sample_flags, inst.image_dimension, inst.image_address_components);
EXIT_IF(count > components.size());
const auto nsa_components =
std::min(inst.image_nsa_dwords * 4u, Decoder::MaxImageNsaAddressComponents);
@@ -863,9 +863,10 @@ void Translator::DS_WRITE2(const Decoder::Instruction& inst) {
void Translator::DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
const auto memory = MemoryInfoFromDecoded(inst);
WriteOperand(inst.dst, ir.Emit(opcode,
{ReadU32(MemorySourceAt(inst, 0)), ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
const auto exec = ExecMask();
WriteOperand(inst.dst,
ir.Emit(opcode, {ReadU32(MemorySourceAt(inst, 0)), exec[0], exec[1], ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
}
void Translator::DS_ADDTID(const Decoder::Instruction& inst, bool write) {
@@ -901,10 +902,7 @@ void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
}
const auto exec = ir.GetExec();
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
const auto low = ir.CompositeExtract(ballot, 0u);
const auto high = ir.CompositeExtract(ballot, 1u);
const auto [low, high] = ExecMask();
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
const auto first = ir.Select(
ir.INotEqual(low, IR::U32(IR::Value(0u))),
@@ -943,6 +941,8 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
void Translator::EmitMemory(const Decoder::Instruction& inst) {
switch (inst.opcode) {
// Translated scalar loads have no cache of their own to invalidate.
case Decoder::Opcode::S_DCACHE_INV: return;
case Decoder::Opcode::BUFFER_GL0_INV:
case Decoder::Opcode::BUFFER_GL1_INV:
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
@@ -1031,6 +1031,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMin32);
case Decoder::Opcode::BUFFER_ATOMIC_UMIN:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin32);
case Decoder::Opcode::BUFFER_ATOMIC_UMIN_X2:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin64);
case Decoder::Opcode::BUFFER_ATOMIC_SMAX:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMax32);
case Decoder::Opcode::BUFFER_ATOMIC_UMAX:
@@ -1148,6 +1150,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::FLAT_LOAD_USHORT:
case Decoder::Opcode::FLAT_LOAD_SSHORT:
case Decoder::Opcode::FLAT_LOAD_SHORT_D16:
case Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI:
case Decoder::Opcode::FLAT_LOAD_DWORD:
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
@@ -1167,15 +1170,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::IMAGE_STORE:
case Decoder::Opcode::IMAGE_STORE_MIP: return IMAGE_STORE(inst);
case Decoder::Opcode::IMAGE_SAMPLE: return IMAGE_SAMPLE(inst);
case Decoder::Opcode::IMAGE_GATHER4_L:
case Decoder::Opcode::IMAGE_GATHER4_LZ:
case Decoder::Opcode::IMAGE_GATHER4_C:
case Decoder::Opcode::IMAGE_GATHER4_C_L:
case Decoder::Opcode::IMAGE_GATHER4_C_LZ:
case Decoder::Opcode::IMAGE_GATHER4_LZ_O:
case Decoder::Opcode::IMAGE_GATHER4_C_O:
case Decoder::Opcode::IMAGE_GATHER4_C_LZ_O:
case Decoder::Opcode::IMAGE_GATHER4H: return IMAGE_GATHER(inst);
case Decoder::Opcode::IMAGE_GATHER4: return IMAGE_GATHER(inst);
case Decoder::Opcode::DS_MIN_F32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicFMin32, false);
@@ -73,43 +73,41 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_CMP_EQ_U32:
case O::S_CMP_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LG_U32:
case O::S_CMP_LG_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true);
return;
case O::S_CMP_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true);
return;
case O::S_CMP_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true,
false);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true);
return;
case O::S_CMP_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true);
return;
case O::S_CMP_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true,
false);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true);
return;
case O::S_CMP_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true);
return;
case O::S_CMP_LG_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true);
return;
case O::S_AND_B64:
@@ -129,6 +129,20 @@ std::array<IR::U32, 2> Translator::BallotMask(IR::U1 value) {
program.wave_size == 64u ? ir.CompositeExtract(mask, 1) : IR::U32(IR::Value(0u))};
}
std::array<IR::U32, 2> Translator::ExecMask() {
return {ir.GetExecLo(), program.wave_size == 64u ? ir.GetExecHi() : IR::U32(IR::Value(0u))};
}
// Pixel EXEC words exclude the helper invocations that the per-lane EXEC keeps enabled.
std::array<IR::U32, 2> Translator::VectorMask(IR::U1 value) {
auto mask = BallotMask(value);
if (program.stage == ShaderType::Pixel) {
const auto exec = ExecMask();
mask = {ir.BitwiseAnd(mask[0], exec[0]), ir.BitwiseAnd(mask[1], exec[1])};
}
return mask;
}
IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
switch (operand.kind) {
case Decoder::OperandKind::LiteralConstant:
@@ -138,6 +152,8 @@ IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
case Decoder::OperandKind::SharedBase:
case Decoder::OperandKind::PrivateBase:
case Decoder::OperandKind::PopsExitingWaveId: return IR::U32(IR::Value(0u));
case Decoder::OperandKind::SharedLimit:
case Decoder::OperandKind::PrivateLimit: return IR::U32(IR::Value(0xffffffffu));
case Decoder::OperandKind::Sgpr:
return ir.GetScalarReg(static_cast<IR::ScalarReg>(operand.reg));
case Decoder::OperandKind::Vgpr:
@@ -388,21 +404,9 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
switch (operand.kind) {
case Decoder::OperandKind::Scc: ir.SetScc(IR::U1(value)); return;
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: {
const auto mask = BallotMask(IR::U1(value));
ir.SetExec(IR::U1(value));
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return;
}
case Decoder::OperandKind::ExecHi:
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccHi: {
const auto mask = BallotMask(IR::U1(value));
ir.SetVcc(IR::U1(value));
ir.SetVccLo(mask[0]);
ir.SetVccHi(mask[1]);
return;
}
case Decoder::OperandKind::VccHi: EXIT("lane masks are written through WriteMask");
default:
WriteRawU32(operand, ir.Select(IR::U1(value), IR::U32(IR::Value(1u)),
IR::U32(IR::Value(0u))));
@@ -471,17 +475,17 @@ IR::U32 Translator::ReadU32(const Decoder::Operand& operand) {
}
std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand) {
if (operand.kind == Decoder::OperandKind::PrivateBase ||
operand.kind == Decoder::OperandKind::SharedBase) {
return {IR::U32(IR::Value(0u)), IR::U32(IR::Value(
operand.kind == Decoder::OperandKind::PrivateBase ? Decoder::PrivateApertureHigh
: Decoder::SharedApertureHigh))};
}
if (operand.kind == Decoder::OperandKind::ExecLo) {
return {ir.GetExecLo(), ir.GetExecHi()};
}
if (operand.kind == Decoder::OperandKind::VccLo) {
return {ir.GetVccLo(), ir.GetVccHi()};
// Each aperture limit is its base plus 0xffffffff.
switch (operand.kind) {
case Decoder::OperandKind::SharedBase:
case Decoder::OperandKind::SharedLimit:
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::SharedApertureHigh))};
case Decoder::OperandKind::PrivateBase:
case Decoder::OperandKind::PrivateLimit:
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::PrivateApertureHigh))};
case Decoder::OperandKind::ExecLo: return {ir.GetExecLo(), ir.GetExecHi()};
case Decoder::OperandKind::VccLo: return {ir.GetVccLo(), ir.GetVccHi()};
default: break;
}
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits =
@@ -639,7 +643,7 @@ IR::U1 Translator::ReadMask(const Decoder::Operand& operand) {
}
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: return ir.GetExec();
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccLo: return ir.GetVcc();
case Decoder::OperandKind::VccHi:
return program.wave_size == 32u
? ThreadBit({ReadRawU32(operand), IR::U32(IR::Value(0u))})
@@ -677,9 +681,8 @@ IR::U1 Translator::ReadMaskValid(const Decoder::Operand& operand) {
}
}
std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
bool write_64) {
const auto mask = BallotMask(value);
void Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
const std::array<IR::U32, 2>& mask, bool write_64) {
switch (operand.kind) {
case Decoder::OperandKind::Sgpr: {
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
@@ -700,43 +703,43 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
}
return mask;
return;
}
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: {
ir.SetExec(value);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return mask;
return;
}
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccHi: {
if (!write_64 && program.wave_size == 32u) {
if (operand.kind == Decoder::OperandKind::VccHi && !write_64 &&
program.wave_size == 32u) {
WriteRawU32(operand, mask[0]);
return mask;
return;
}
ir.SetVcc(value);
ir.SetVccLo(mask[0]);
ir.SetVccHi(mask[1]);
return mask;
if (write_64 || program.wave_size == 64u) {
ir.SetVccHi(mask[1]);
}
return;
}
case Decoder::OperandKind::Scc: ir.SetScc(value); return mask;
case Decoder::OperandKind::Scc: ir.SetScc(value); return;
default:
WriteRawU32(operand, mask[0]);
WriteRawU32(OffsetOperand(operand, 1), mask[1]);
return mask;
return;
}
}
void Translator::WriteCompareResult(const Decoder::Operand& operand, IR::U1 value) {
if (operand.kind == Decoder::OperandKind::Scc) {
WriteOperand(operand, value);
return;
}
void Translator::WriteVectorMask(const Decoder::Operand& operand, IR::U1 value) {
// VALU instructions only write lanes enabled by the incoming EXEC mask. This is
// especially important for V_CMPX: replacing EXEC with the ungated comparison would
// reactivate lanes disabled by an enclosing divergent region.
WriteMask(operand, ir.LogicalAnd(ir.GetExec(), value));
const auto active = ir.LogicalAnd(ir.GetExec(), value);
WriteMask(operand, active, VectorMask(active));
}
void Translator::AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source,
@@ -1153,7 +1156,10 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
}
entry_ir.SetExec(initial_exec);
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
// Pixel EXEC words start with the live (non-helper) lanes; helpers keep the per-lane EXEC.
const auto initial_mask = options.stage == ShaderType::Pixel
? entry_ir.Emit(IR::ValueOpcode::LiveMask)
: entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u));
@@ -1238,12 +1244,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
primitive_count, u32(8)),
vertex_count)));
// Instance IDs exclude the start instance, which shaders add from user data.
if (mesh.fast_launch) {
// Fast launch broadcasts the subgroup's base vertex and instance to every lane.
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5), entry_ir.IAdd(draw(1), group));
entry_ir.SetVectorReg(
static_cast<IR::VectorReg>(6),
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(6),
builtin(IR::StageInputKind::WorkgroupId, 1));
} else {
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
// center in every subgroup; strip winding follows the global primitive.
@@ -1271,14 +1277,14 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.ShiftLeftLogical(second, u32(18))));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
entry_ir.ShiftLeftLogical(third, u32(2)));
const auto index_bytes = draw(3);
const auto index_bytes = draw(2);
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
const auto index_low = draw(4);
const auto index_low = draw(3);
const auto byte_offset = entry_ir.IAdd(entry_ir.BitwiseAnd(index_low, u32(3)),
entry_ir.IMul(input_vertex, index_bytes));
const auto index_resource = entry_ir.Emit(
IR::ValueOpcode::GetAddressResource,
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(5)});
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(4)});
const auto memory_index = static_cast<uint32_t>(result.memory_info.size());
result.memory_info.push_back({.kind = IR::ResourceKind::Global});
const auto packed_index = entry_ir.Emit(
@@ -1292,9 +1298,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.IMul(index_bytes, u32(8))}));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
entry_ir.IAdd(draw(1), entry_ir.Select(indexed, index, input_vertex)));
entry_ir.SetVectorReg(
static_cast<IR::VectorReg>(8),
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
builtin(IR::StageInputKind::WorkgroupId, 1));
}
} else if (options.stage == ShaderType::Local) {
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3), IR::U32(IR::Value(64u)));
@@ -1384,10 +1389,27 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
IR::U32(IR::Value(options.wave_size << 12u)));
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
// Vulkan adds an indirect record's bases to the indices; the hardware passes them
// only in the user SGPRs the packet patches.
const auto& offsets = options.input_info.vertex->indirect_offsets;
const auto index = [&](uint8_t slot, IR::StageInputKind kind,
IR::StageInputKind base_kind) {
if (offsets == ShaderIndirectOffsets {}) {
return builtin(kind);
}
const auto base = builtin(base_kind);
if (slot < options.user_data_count) {
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(options.user_data_base + slot),
base);
}
return entry_ir.ISub(builtin(kind), base);
};
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
builtin(IR::StageInputKind::VertexIndex));
index(offsets.vertex, IR::StageInputKind::VertexIndex,
IR::StageInputKind::BaseVertex));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
builtin(IR::StageInputKind::InstanceIndex));
index(offsets.instance, IR::StageInputKind::InstanceIndex,
IR::StageInputKind::BaseInstance));
}
}
const bool flush_f32_inputs = options.stage == ShaderType::Compute &&
@@ -27,6 +27,8 @@ private:
Decoder::Operand ScalarDestinationOperand(const Decoder::Operand& operand, uint32_t offset);
Decoder::Operand PlainOperand(const Decoder::Operand& operand);
std::array<IR::U32, 2> BallotMask(IR::U1 value);
std::array<IR::U32, 2> ExecMask();
std::array<IR::U32, 2> VectorMask(IR::U1 value);
IR::U32 ReadRawU32(const Decoder::Operand& operand);
IR::U32 ReadScalarCode(uint32_t code);
IR::U32 ApplyBitSourceModifiers(const Decoder::Operand& operand, IR::U32 value);
@@ -52,9 +54,9 @@ private:
IR::U32 ConditionBit(const Decoder::Operand& operand);
IR::U1 ReadMask(const Decoder::Operand& operand);
IR::U1 ReadMaskValid(const Decoder::Operand& operand);
std::array<IR::U32, 2> WriteMask(const Decoder::Operand& operand, IR::U1 value,
bool write_64 = false);
void WriteCompareResult(const Decoder::Operand& operand, IR::U1 value);
void WriteMask(const Decoder::Operand& operand, IR::U1 value,
const std::array<IR::U32, 2>& mask, bool write_64 = false);
void WriteVectorMask(const Decoder::Operand& operand, IR::U1 value);
IR::MemoryFlags AddMemoryInfo(const IR::MemoryInfo& memory, uint32_t pc);
IR::ExportFlags AddExportInfo(const Decoder::Instruction& inst);
@@ -114,17 +116,16 @@ private:
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 PackU16Lanes(IR::U32 low, IR::U32 high);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
IR::U32 Saturate16(IR::U32 value, bool signed_value);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar);
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
bool scalar, bool cmpx, bool swap_operands = false);
bool scalar, bool swap_operands = false);
void EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
bool signed_value);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool half = false);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
void V_CVT_F32_I32(const Decoder::Instruction& inst);
@@ -141,6 +142,7 @@ private:
void V_CVT_PKRTZ_F16_F32(const Decoder::Instruction& inst);
void V_CVT_PKNORM_F32(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
void V_CVT_PK_U8_F32(const Decoder::Instruction& inst);
void V_CVT_PK_16_32(const Decoder::Instruction& inst, bool signed_value);
void V_PACK_B32_F16(const Decoder::Instruction& inst);
void PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool quiet_snan);
@@ -151,6 +153,7 @@ private:
void Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
void FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
void V_FRACT(const Decoder::Instruction& inst, IR::Type type);
void FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse);
void FloatTernary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
@@ -25,444 +25,310 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_PERMLANEX16_B32: V_PERMLANE16_B32(inst, true); return;
case O::V_CMP_F_I32:
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_T_I32:
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_U32:
case O::V_CMP_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, false);
return;
case O::V_CMPX_EQ_U32:
case O::V_CMPX_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false);
return;
case O::V_CMP_NE_U32:
case O::V_CMP_NE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, false);
return;
case O::V_CMPX_NE_U32:
case O::V_CMPX_NE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false);
return;
case O::V_CMP_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false);
return;
case O::V_CMP_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false);
return;
case O::V_CMP_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false);
return;
case O::V_CMP_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false);
return;
case O::V_CMP_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_F_I64:
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_F_U64:
case O::V_CMPX_F_I64:
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64:
case O::V_CMPX_T_I64:
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_EQ_I64:
case O::V_CMPX_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false);
return;
case O::V_CMP_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false);
return;
case O::V_CMP_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false);
return;
case O::V_CMP_NE_I64:
case O::V_CMP_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_NE_I64:
case O::V_CMPX_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false);
return;
case O::V_CMP_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false);
return;
case O::V_CMP_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false);
return;
// The IR has no signed 64-bit greater-than: a > b is b < a, and a >= b is b <= a.
case O::V_CMP_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false);
return;
case O::V_CMP_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false);
return;
case O::V_CMP_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
case O::V_CMPX_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false);
return;
case O::V_CMP_EQ_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, false);
return;
case O::V_CMPX_EQ_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true);
return;
case O::V_CMP_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
return;
case O::V_CMPX_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false);
return;
case O::V_CMP_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
return;
case O::V_CMPX_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true);
return;
case O::V_CMP_GT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, false);
return;
case O::V_CMPX_GT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false);
return;
case O::V_CMP_GE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false, false);
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false);
return;
case O::V_CMP_LT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, false);
return;
case O::V_CMPX_LT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, true);
return;
case O::V_CMPX_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false);
return;
case O::V_CMP_LE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false, false);
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false);
return;
case O::V_CMP_GT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, false);
return;
case O::V_CMPX_GT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true);
return;
case O::V_CMP_GE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, false);
return;
case O::V_CMPX_GE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true);
return;
case O::V_CMP_LT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, false);
return;
case O::V_CMPX_LT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true);
return;
case O::V_CMP_LE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, false);
return;
case O::V_CMPX_LE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true);
return;
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, false);
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false);
return;
case O::V_CMP_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, false);
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false);
return;
case O::V_CMP_LE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, false);
return;
case O::V_CMPX_LE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, true);
return;
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
return;
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
return;
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
return;
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
return;
case O::V_CMP_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
return;
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
return;
case O::V_CMP_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
return;
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false);
return;
case O::V_CMP_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false);
return;
case O::V_CMP_F_F64:
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_TRU_F64:
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_LT_F64:
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false);
return;
case O::V_CMP_GT_F64:
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false);
return;
case O::V_CMP_LG_F64:
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false);
return;
case O::V_CMP_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
return;
case O::V_CMPX_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false);
return;
case O::V_CMP_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
return;
case O::V_CMPX_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false);
return;
case O::V_CMP_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
return;
case O::V_CMPX_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false);
return;
case O::V_CMP_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
return;
case O::V_CMPX_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false);
return;
case O::V_CMP_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
return;
case O::V_CMPX_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false);
return;
case O::V_CMP_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
return;
case O::V_CMPX_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
return;
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false);
return;
case O::V_CMP_O_F64:
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMP_U_F64:
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_LG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, false);
return;
case O::V_CMPX_LG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false);
return;
case O::V_CMP_GT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, false);
return;
case O::V_CMPX_GT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false);
return;
case O::V_CMP_GE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, false);
return;
case O::V_CMPX_GE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false);
return;
case O::V_CMP_LT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, false);
return;
case O::V_CMPX_LT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false);
return;
case O::V_CMP_LE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, false);
return;
case O::V_CMPX_LE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false);
return;
case O::V_CMP_NLG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, false);
return;
case O::V_CMPX_NLG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false);
return;
case O::V_CMP_NEQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, false);
return;
case O::V_CMPX_NEQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false);
return;
case O::V_CMP_NLE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, false);
return;
case O::V_CMPX_NLE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false);
return;
case O::V_CMP_NLT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, false);
return;
case O::V_CMPX_NLT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false);
return;
case O::V_CMP_NGE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, false);
return;
case O::V_CMPX_NGE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false);
return;
case O::V_CMP_NGT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, false);
return;
case O::V_CMPX_NGT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false);
return;
case O::V_CMP_EQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, false);
return;
case O::V_CMPX_EQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true);
return;
case O::V_CMP_LG_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true, false);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true);
return;
case O::V_CMP_GT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, false);
return;
case O::V_CMPX_GT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true);
return;
case O::V_CMP_GE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, false);
return;
case O::V_CMPX_GE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true);
return;
case O::V_CMP_LT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, false);
return;
case O::V_CMPX_LT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true);
return;
case O::V_CMP_LE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, false);
return;
case O::V_CMPX_LE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true);
return;
case O::V_CMP_NGE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true, false);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true);
return;
case O::V_CMP_NGT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, false);
return;
case O::V_CMPX_NGT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true);
return;
case O::V_CMPX_NLE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true);
return;
case O::V_CMP_NEQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, false);
return;
case O::V_CMPX_NEQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true);
return;
case O::V_CMP_NLT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, false);
return;
case O::V_CMPX_NLT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true);
return;
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CMP_O_F32:
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true); return;
case O::V_CMP_U_F32:
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false); return;
case O::V_CMP_CLASS_F32:
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst); return;
case O::V_CMP_CLASS_F16:
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return;
@@ -495,8 +361,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
return;
case O::V_CVT_PK_U8_F32: V_CVT_PK_U8_F32(inst); return;
case O::V_PACK_B32_F16: V_PACK_B32_F16(inst); return;
case O::V_CVT_PK_U16_U32:
case O::V_CVT_PK_I16_I32: return PackB16(inst, false, false);
case O::V_CVT_PK_U16_U32: return V_CVT_PK_16_32(inst, false);
case O::V_CVT_PK_I16_I32: return V_CVT_PK_16_32(inst, true);
case O::V_LSHLREV_B16:
return Integer16Shift(inst, IR::ValueOpcode::ShiftLeftLogical32, false);
@@ -563,7 +429,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false);
case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false);
case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false);
case O::V_FRACT_F16: return Float16Unary(inst, IR::ValueOpcode::FPFract32, false);
case O::V_FRACT_F16: return V_FRACT(inst, IR::Type::F16);
case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin);
case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos);
case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false);
@@ -574,8 +440,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32);
case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64);
case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32);
case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32);
case O::V_FRACT_F64: return FloatUnary(inst, IR::ValueOpcode::FPFract64);
case O::V_FRACT_F32: return V_FRACT(inst, IR::Type::F32);
case O::V_FRACT_F64: return V_FRACT(inst, IR::Type::F64);
case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32);
case O::V_TRUNC_F64: return FloatUnary(inst, IR::ValueOpcode::FPTrunc64);
case O::V_CEIL_F32: return FloatUnary(inst, IR::ValueOpcode::FPCeil32);
@@ -118,15 +118,14 @@ ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
}
Program::~Program() {
// Planning expressions can refer to block values but outlive block storage in the base class.
// Planning expressions can refer to block values but outlive block storage in the base class,
// and values cross block boundaries: detach everything before any storage is destroyed.
for (auto& inst: value_storage) {
inst.Invalidate();
inst.DropArgs();
}
// Values may cross block boundaries. Detach all arguments before any block starts destroying
// its instruction storage so reverse-use links always point to live definitions.
for (auto* block: blocks) {
for (auto& inst: *block) {
inst.Invalidate();
inst.DropArgs();
}
}
}
@@ -169,6 +168,8 @@ bool EquivalentSelectorDomain(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& right) {
return left.selector == right.selector && left.signed_count == right.signed_count &&
left.workgroup_axis == right.workgroup_axis &&
left.workgroup_multiplier == right.workgroup_multiplier &&
left.workgroup_shift == right.workgroup_shift &&
EquivalentValue(program, left.key_count, right.key_count) &&
EquivalentValue(program, left.selector_first, right.selector_first) &&
EquivalentValue(program, left.selector_mask, right.selector_mask);
@@ -0,0 +1,184 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
#include "common/assert.h"
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/rectListShader.h"
#include <algorithm>
#include <bit>
#include <optional>
#include <span>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <vector>
// Shader cache records. Io(io, value) appends value to a Writer or reads it back from a Reader
// field by field, so padding never reaches the bytes; instructions travel as plan-local indices.
namespace Libs::Graphics::ShaderRecompiler::IR {
struct Writer {
std::vector<uint8_t> bytes;
std::unordered_map<const Inst*, uint32_t> index; // Instructions that values may refer to.
void Raw(const void* data, size_t size) {
const auto* begin = static_cast<const uint8_t*>(data);
bytes.insert(bytes.end(), begin, begin + size);
}
};
struct Reader {
std::span<const uint8_t> bytes;
std::vector<Inst*> insts; // Instructions by index.
bool ok = true; // False once a read ran past the end or was invalid.
void Raw(void* data, size_t size) {
ok = ok && size <= bytes.size();
if (!ok) return;
std::copy_n(bytes.data(), size, static_cast<uint8_t*>(data));
bytes = bytes.subspan(size);
}
// Every element takes at least one byte, which bounds counts read from damaged records.
uint32_t Count(uint32_t count) {
ok = ok && count <= bytes.size();
return ok ? count : 0u;
}
};
template <class S> constexpr bool Reading = std::is_same_v<S, Reader>;
template <class T, template <class...> class Of> constexpr bool IsA = false;
template <template <class...> class Of, class... A> constexpr bool IsA<Of<A...>, Of> = true;
template <class T, class U> concept Is = std::is_same_v<std::remove_const_t<T>, U>;
template <class S, class T> void Io(S& io, T& value);
template <class S, class... T> void IoAll(S& io, T&&... values) {
(Io(io, values), ...);
}
// The value's Type, Opaque for an instruction, then the instruction index or the zero-extended
// immediate.
inline void IoValue(Writer& writer, const Value& value) {
auto type = Type::Opaque;
uint64_t bits = 0;
if (const auto* inst = value.TryInstruction()) {
bits = writer.index.at(inst);
} else {
type = value.GetType();
switch (type) {
case Type::Void: break;
case Type::ScalarReg: bits = RegIndex(value.ScalarRegister()); break;
case Type::VectorReg: bits = RegIndex(value.VectorRegister()); break;
case Type::U1: bits = value.U1(); break;
case Type::U8: bits = value.U8(); break;
case Type::U16: bits = value.U16(); break;
case Type::U32: bits = value.U32(); break;
case Type::U64: bits = value.U64(); break;
case Type::F16: bits = value.F16Bits(); break;
case Type::F32: bits = std::bit_cast<uint32_t>(value.F32Value()); break;
default: EXIT("value type %u has no cache encoding\n", static_cast<uint32_t>(type));
}
}
IoAll(writer, type, bits);
}
inline void IoValue(Reader& reader, Value& value) {
auto type = Type::Void;
uint64_t bits = 0;
IoAll(reader, type, bits);
switch (type) {
case Type::Void: value = {}; break;
case Type::Opaque:
reader.ok = reader.ok && bits < reader.insts.size();
if (reader.ok) value = Value(reader.insts[bits]);
break;
case Type::ScalarReg: value = Value(static_cast<ScalarReg>(bits)); break;
case Type::VectorReg: value = Value(static_cast<VectorReg>(bits)); break;
case Type::U1: value = Value(bits != 0u); break;
case Type::U8: value = Value(static_cast<uint8_t>(bits)); break;
case Type::U16: value = Value(static_cast<uint16_t>(bits)); break;
case Type::U32: value = Value(static_cast<uint32_t>(bits)); break;
case Type::U64: value = Value(bits); break;
case Type::F16: value = Value::F16(static_cast<uint16_t>(bits)); break;
case Type::F32: value = Value::F32(std::bit_cast<float>(static_cast<uint32_t>(bits))); break;
default: reader.ok = false; break;
}
}
// Scalars and standard containers; structs have their own overloads below.
template <class S, class T> void Io(S& io, T& value) {
using U = std::remove_const_t<T>;
static_assert(!std::is_pointer_v<U>, "pointers do not persist");
if constexpr (std::is_arithmetic_v<U> || std::is_enum_v<U>) {
io.Raw(&value, sizeof(U));
} else if constexpr (std::is_same_v<U, Value>) {
IoValue(io, value);
} else if constexpr (IsA<U, std::vector> || IsA<U, std::basic_string>) {
auto count = static_cast<uint32_t>(value.size());
Io(io, count);
if constexpr (Reading<S>) value.resize(io.Count(count));
using E = typename U::value_type;
if constexpr (std::is_arithmetic_v<E> || std::is_enum_v<E>) {
io.Raw(value.data(), value.size() * sizeof(E));
} else {
for (auto& element: value) Io(io, element);
}
} else if constexpr (IsA<U, std::optional>) {
bool has = value.has_value();
Io(io, has);
if constexpr (Reading<S>) {
if (has) value = typename U::value_type {}; // Not emplace(): clang rejects nested aggregates.
else value.reset();
}
if (has) Io(io, *value);
} else {
static_assert(requires { std::tuple_size<U>::value; }, "struct without an Io overload");
std::apply([&](auto&... elements) { IoAll(io, elements...); }, value); // std::array, std::pair
}
}
// Binds every field, so that a field added later fails to compile until it is listed.
#define KYTY_IO_FIELDS(Struct, ...) \
template <class S, Is<Struct> T> void Io(S& io, T& object) { \
auto& [__VA_ARGS__] = object; \
IoAll(io, __VA_ARGS__); \
}
KYTY_IO_FIELDS(MemoryInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y)
KYTY_IO_FIELDS(BufferResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
KYTY_IO_FIELDS(ImageResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w)
KYTY_IO_FIELDS(SamplerResource, a, b, c, d, e, f, g, h, i, j, k)
KYTY_IO_FIELDS(SampledResourcePair, a, b, c)
KYTY_IO_FIELDS(StageInput, a, b, c, d, e)
KYTY_IO_FIELDS(StageOutput, a, b, c, d)
KYTY_IO_FIELDS(ShaderInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
A, B, C, D, E, F, G, H, I, J, K)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::CountedRows, a, b)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks, a, b, c)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j, k,
l)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::PointerTable, a, b, c, d)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p,
q, r)
KYTY_IO_FIELDS(DescriptorSource, a, b, c)
KYTY_IO_FIELDS(SrtRead, a, b)
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d, e)
KYTY_IO_FIELDS(UniformFill, a, b, c, d, e, f)
KYTY_IO_FIELDS(UniformFillPlan, a, b, c, d)
KYTY_IO_FIELDS(ResourceSpecialization::Buffer, a, b, c, d, e, f, g, h)
KYTY_IO_FIELDS(ResourceSpecialization::Image, a, b, c, d, e, f, g, h, i, j, k)
KYTY_IO_FIELDS(ResourceSpecialization::Sampler, a, b, c)
KYTY_IO_FIELDS(ResourceSpecialization, a, b, c)
KYTY_IO_FIELDS(Bindings, a, b, c, d, e)
KYTY_IO_FIELDS(CompiledShaderInfo, a, b, c, d, e, f, g, h, i, j)
KYTY_IO_FIELDS(Spirv::Profile, a, b, c)
KYTY_IO_FIELDS(ShaderTessellationInputInfo, a, b, c, d, e, f, g, h, i)
KYTY_IO_FIELDS(RectListParameter, a, b, c)
#undef KYTY_IO_FIELDS
} // namespace Libs::Graphics::ShaderRecompiler::IR
#endif /* EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_ */
+32 -29
View File
@@ -14,7 +14,6 @@
#include <array>
#include <bit>
#include <deque>
#include <list>
#include <memory>
#include <optional>
@@ -28,8 +27,6 @@ class CodeGenerator;
namespace Libs::Graphics::ShaderRecompiler::IR {
class SrtWalker;
// Finite descriptor domains also bound the arithmetic used to prove their indices.
constexpr uint32_t MaxIndirectDescriptorProbes = 65536u;
@@ -254,6 +251,8 @@ enum class StageInputKind {
LocalInvocationId,
LocalInvocationIndex,
GlobalInvocationId,
BaseVertex,
BaseInstance,
Parameter,
};
@@ -368,7 +367,7 @@ static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 58u);
struct PushData {
static constexpr uint32_t DwordCount = 32;
static constexpr uint32_t MeshDrawDwordCount = 6;
static constexpr uint32_t MeshDrawDwordCount = 5;
static constexpr uint32_t NoStart = UINT32_MAX;
std::array<uint32_t, DwordCount> dwords {};
@@ -498,10 +497,16 @@ struct Bindings {
uint32_t dispatch_thread_dword = PushData::NoStart;
uint32_t memory_offset_dword = 0;
[[nodiscard]] uint32_t ShaderDataDwords() const {
// Packed 8-bit binding offsets, then each binding's bound range in bytes: OpArrayLength
// reads 0 for ranges of 2 GiB or more on NVIDIA.
[[nodiscard]] uint32_t MemorySizeDword() const {
return memory_offset_dword +
(descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)] + 3u) / 4u;
}
[[nodiscard]] uint32_t ShaderDataDwords() const {
return MemorySizeDword() +
descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)];
}
[[nodiscard]] bool UsesPushData() const {
return push_data_start_dword != PushData::NoStart;
}
@@ -544,6 +549,7 @@ struct ShaderInfo {
bool subgroup_shuffle = false;
bool subgroup_arithmetic = false;
bool subgroup_local_invocation_id = false;
bool helper_invocation = false;
bool compute_derivatives = false;
bool image_gather_extended = false;
bool function_lds = false;
@@ -568,6 +574,13 @@ struct DescriptorSource {
bool operator==(const CountedRows& other) const = default;
};
struct MaskRanks {
uint32_t source = UINT32_MAX;
uint32_t stride = 0; // Byte spacing of the mask words its load can address.
uint32_t offset = 0;
bool operator==(const MaskRanks& other) const = default;
};
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
@@ -575,10 +588,12 @@ struct DescriptorSource {
uint32_t bit_offset = 0;
uint32_t bit_mask = UINT32_MAX;
bool global = false;
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
// Four or eight DWORD bitmap words per record; set ranks subtract from the last key.
bool bitset = false;
std::optional<CountedRows> rows;
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
std::optional<uint32_t> excluded; // A guard keeps this key from the read.
std::optional<MaskRanks> ranks; // Records are the bits some mask word sets.
bool operator==(const SelectorRead& other) const = default;
};
@@ -602,8 +617,12 @@ struct DescriptorSource {
bool indexed = false;
bool signed_count = false;
uint32_t workgroup_axis = UINT32_MAX;
// Nonzero shift: workgroup keys are (selector_first + ID) * multiplier >> shift.
uint32_t workgroup_multiplier = 0;
uint32_t workgroup_shift = 0;
Value key_count;
// First selector index, or first descriptor key for a direct or bitmap range.
// First selector index, workgroup key base, or first descriptor key for a direct or bitmap
// range.
Value selector_first;
Value selector_mask;
std::vector<uint32_t> sources;
@@ -630,6 +649,7 @@ struct ResourceBlock {
Value condition;
std::vector<uint32_t> successors;
std::vector<uint32_t> sources;
std::vector<uint32_t> images;
std::vector<uint32_t> srt_reads;
};
@@ -658,24 +678,10 @@ struct UniformFillPlan {
// condition and fill values without translated blocks, plus reusable evaluation scratch.
struct ResourcePlan {
struct SelectorKeys {
uint32_t source = UINT32_MAX;
std::vector<uint32_t> values;
bool resolved = false;
};
struct EvaluationContext {
struct Entry {
uint64_t value = 0;
uint64_t generation = 0;
};
std::vector<Entry> values;
uint64_t generation = 0;
const SrtWalker* session = nullptr;
Value active_mask;
uint8_t read_policy = 0;
};
ResourcePlan();
~ResourcePlan();
@@ -695,21 +701,18 @@ struct ResourcePlan {
std::vector<ResourceBlock> control_flow;
std::unordered_map<const Inst*, std::vector<uint32_t>> control_phi_predecessors;
std::vector<SrtRead> srt_reads;
bool requires_specialization_memory = false;
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
bool capture_specialization_reads = false;
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
ShaderInfo info;
UniformFillPlan uniform_fill;
// x86 walker over the retained SRT, descriptor and condition values.
std::unique_ptr<Xbyak::CodeGenerator> srt_code;
// GPU-thread scratch for nested clean/EXEC memos, activity and material keys.
mutable std::deque<EvaluationContext> evaluation_contexts;
mutable uint32_t evaluation_value_count = 0;
mutable std::vector<uint8_t> active_sources;
mutable std::vector<uint8_t> visited_blocks;
mutable std::vector<uint8_t> reachable_blocks;
mutable std::vector<uint32_t> pending_blocks;
uint32_t srt_user_data = 0; // User-data DWORDs the walker reads.
uint32_t srt_captures = 0; // Scratch offset of loaded addresses.
// GPU-thread scratch for walker results and material keys.
mutable std::vector<uint64_t> srt_scratch;
mutable std::vector<uint32_t> material_keys;
mutable std::vector<SelectorKeys> selector_keys;
};
+15 -74
View File
@@ -25,22 +25,6 @@ Value Value::F32(float value) {
return Value(Type::F32, std::bit_cast<uint32_t>(value));
}
bool Value::IsEmpty() const {
return type == Type::Void;
}
bool Value::IsImmediate() const {
return type != Type::Opaque;
}
bool Value::IsIdentity() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
}
bool Value::IsPhi() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
}
Type Value::GetType() const {
if (IsPhi()) {
return inst->Flags<Type>();
@@ -56,19 +40,6 @@ Inst* Value::Instruction() const {
return inst;
}
Inst* Value::TryInstruction() const {
return type == Type::Opaque ? inst : nullptr;
}
Inst* Value::ResolveInstruction() const {
EXIT_IF(type != Type::Opaque);
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
}
Value Value::Resolve() const {
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
}
ScalarReg Value::ScalarRegister() const {
EXIT_IF(type != Type::ScalarReg);
return scalar_reg;
@@ -94,11 +65,6 @@ uint16_t Value::U16() const {
return imm_u16;
}
uint32_t Value::U32() const {
EXIT_IF(type != Type::U32);
return imm_u32;
}
uint64_t Value::U64() const {
EXIT_IF(type != Type::U64);
return imm_u64;
@@ -152,10 +118,6 @@ Inst::~Inst() {
ClearArgs();
}
ValueOpcode Inst::GetOpcode() const {
return opcode;
}
Type Inst::GetType() const {
if (opcode == ValueOpcode::Phi) {
return static_cast<Type>(flags);
@@ -178,51 +140,25 @@ size_t Inst::UseCount() const {
return uses.size();
}
size_t Inst::NumArgs() const {
return num_args == PhiArity ? phi_args.size() : num_args;
}
size_t Inst::NumPhiBlocks() const {
return num_args == PhiArity ? phi_args.size() : 0;
}
Value Inst::Arg(size_t index) const {
EXIT_IF(index >= NumArgs());
if (num_args <= InlineArity) {
return fixed_args[index];
}
return num_args == PhiArity ? phi_args[index].second : large_args[index];
}
Block* Inst::PhiBlock(size_t index) const {
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
return phi_args[index].first;
}
Block* Inst::Parent() const {
return parent;
}
const std::vector<Use>& Inst::Uses() const {
return uses;
}
void Inst::SetParent(Block* block) {
parent = block;
}
void Inst::SetArg(size_t index, Value value) {
const auto old = Arg(index);
if (auto* old_inst = old.TryInstruction(); old_inst != nullptr) {
auto& arg = ArgSlot(*this, index);
if (auto* old_inst = arg.TryInstruction(); old_inst != nullptr) {
RemoveUse(old_inst, index);
}
if (num_args <= InlineArity) {
fixed_args[index] = value;
} else if (num_args == PhiArity) {
phi_args[index].second = value;
} else {
large_args[index] = value;
}
arg = value;
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
AddUse(new_inst, index);
}
@@ -238,11 +174,15 @@ void Inst::AddPhiOperand(Block* predecessor, Value value) {
}
void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
const auto old_uses = uses;
for (const auto& use: old_uses) {
use.user->SetArg(use.operand, replacement);
}
// Detach the arguments first (a phi can use itself), then move every use over in order.
Invalidate();
const auto old_uses = std::exchange(uses, {});
for (const auto& use: old_uses) {
ArgSlot(*use.user, use.operand) = replacement;
}
if (auto* target = replacement.TryInstruction(); target != nullptr) {
target->uses.insert(target->uses.end(), old_uses.begin(), old_uses.end());
}
if (preserve) {
opcode = ValueOpcode::Identity;
num_args = 1;
@@ -256,9 +196,6 @@ void Inst::Invalidate() {
}
void Inst::AddUse(Inst* used, size_t operand) {
const auto found = std::ranges::find_if(
used->uses, [&](const Use& use) { return use.user == this && use.operand == operand; });
EXIT_IF(found != used->uses.end());
used->uses.push_back({this, operand});
}
@@ -275,6 +212,10 @@ void Inst::ClearArgs() {
RemoveUse(value_inst, index);
}
}
DropArgs();
}
void Inst::DropArgs() {
if (num_args == PhiArity) {
std::destroy_at(&phi_args);
std::construct_at(&fixed_args);
+54 -14
View File
@@ -33,14 +33,14 @@ public:
static Value F16(uint16_t bits);
static Value F32(float value);
[[nodiscard]] bool IsEmpty() const;
[[nodiscard]] bool IsImmediate() const;
[[nodiscard]] bool IsEmpty() const { return type == Type::Void; }
[[nodiscard]] bool IsImmediate() const { return type != Type::Opaque; }
[[nodiscard]] bool IsIdentity() const;
[[nodiscard]] bool IsPhi() const;
[[nodiscard]] Type GetType() const;
[[nodiscard]] Inst* Instruction() const;
[[nodiscard]] Inst* TryInstruction() const;
[[nodiscard]] Inst* TryInstruction() const { return type == Type::Opaque ? inst : nullptr; }
[[nodiscard]] Inst* ResolveInstruction() const;
[[nodiscard]] Value Resolve() const;
[[nodiscard]] ScalarReg ScalarRegister() const;
@@ -112,7 +112,7 @@ public:
Inst(Inst&&) = delete;
Inst& operator=(Inst&&) = delete;
[[nodiscard]] ValueOpcode GetOpcode() const;
[[nodiscard]] ValueOpcode GetOpcode() const { return opcode; }
[[nodiscard]] Type GetType() const;
[[nodiscard]] bool MayHaveSideEffects() const;
[[nodiscard]] bool HasUses() const;
@@ -121,15 +121,11 @@ public:
[[nodiscard]] size_t NumPhiBlocks() const;
[[nodiscard]] Value Arg(size_t index) const;
[[nodiscard]] Block* PhiBlock(size_t index) const;
[[nodiscard]] Block* Parent() const;
[[nodiscard]] const std::vector<Use>& Uses() const;
// Runtime indices belong to the resource plan that owns this instruction.
[[nodiscard]] uint32_t EvaluationIndex(uint32_t& count) const {
if (evaluation_index == UINT32_MAX) {
evaluation_index = count++;
}
return evaluation_index;
}
[[nodiscard]] Block* Parent() const { return parent; }
[[nodiscard]] const std::vector<Use>& Uses() const { return uses; }
// SRT walker output index, assigned by the resource plan that owns this instruction.
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
void SetWalkOutput(uint32_t index) const { walk_output = index; }
// Backend definitions belong to one emission. Wave64 emulation needs one per half.
[[nodiscard]] uint32_t Definition(uint32_t half) const { return definitions[half]; }
void SetDefinition(uint32_t half, uint32_t definition) const {
@@ -142,6 +138,8 @@ public:
void AddPhiOperand(Block* predecessor, Value value);
void ReplaceUsesWith(Value replacement, bool preserve = true);
void Invalidate();
// Drops the arguments without updating their use lists, for teardown that destroys both sides.
void DropArgs();
template <typename T>
requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v<T>)
@@ -161,6 +159,8 @@ public:
private:
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
template <typename Self>
static auto& ArgSlot(Self& self, size_t index);
void AddUse(Inst* used, size_t operand);
void RemoveUse(Inst* used, size_t operand);
void ClearArgs();
@@ -171,7 +171,7 @@ private:
ValueOpcode opcode;
uint8_t num_args;
bool live = false;
mutable uint32_t evaluation_index = UINT32_MAX;
mutable uint32_t walk_output = UINT32_MAX;
mutable std::array<uint32_t, 2> definitions {};
uint64_t flags;
Block* parent = nullptr;
@@ -185,4 +185,44 @@ private:
static_assert(sizeof(Inst) <= 120, "Inst operand and backend storage unintentionally increased");
// Called per operand by the IR passes and the resource walker; inline, as the build has no LTO.
inline bool Value::IsIdentity() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
}
inline bool Value::IsPhi() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
}
inline Inst* Value::ResolveInstruction() const {
EXIT_IF(type != Type::Opaque);
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
}
inline Value Value::Resolve() const {
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
}
inline uint32_t Value::U32() const {
EXIT_IF(type != Type::U32);
return imm_u32;
}
inline size_t Inst::NumArgs() const {
return num_args == PhiArity ? phi_args.size() : num_args;
}
template <typename Self>
auto& Inst::ArgSlot(Self& self, size_t index) {
EXIT_IF(index >= self.NumArgs());
if (self.num_args <= InlineArity) {
return self.fixed_args[index];
}
return self.num_args == PhiArity ? self.phi_args[index].second : self.large_args[index];
}
inline Value Inst::Arg(size_t index) const {
return ArgSlot(*this, index);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -123,6 +123,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
case ValueOpcode::BufferAtomicISub32:
case ValueOpcode::BufferAtomicSMin32:
case ValueOpcode::BufferAtomicUMin32:
case ValueOpcode::BufferAtomicUMin64:
case ValueOpcode::BufferAtomicSMax32:
case ValueOpcode::BufferAtomicUMax32:
case ValueOpcode::BufferAtomicAnd32:
@@ -140,6 +141,7 @@ uint32_t BufferComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicUMin64:
case ValueOpcode::BufferAtomicAnd64:
case ValueOpcode::BufferAtomicOr64:
case ValueOpcode::LoadBufferU32x2:
@@ -96,6 +96,7 @@ VALUE_OPCODE(WqmU64, U64, U64)
// Core value operations used to expand architectural side effects.
VALUE_OPCODE(SelectU1, U1, U1, U1, U1)
VALUE_OPCODE(SelectF32, F32, U1, F32, F32)
VALUE_OPCODE(SelectF64, F64, U1, F64, F64)
VALUE_OPCODE(IAdd32, U32, U32, U32)
VALUE_OPCODE(IAdd64, U64, U64, U64)
VALUE_OPCODE(ISub32, U32, U32, U32)
@@ -218,11 +219,13 @@ VALUE_OPCODE(FPFract64, F64, F64)
VALUE_OPCODE(LaneId, U32)
VALUE_OPCODE(Ballot, U32x4, U1)
VALUE_OPCODE(LiveMask, U32x4)
VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
VALUE_OPCODE(ReadLane, U32, U32, U32)
VALUE_OPCODE(SubgroupUMinU32, U32, U32)
VALUE_OPCODE(SubgroupUMaxU32, U32, U32)
VALUE_OPCODE(WriteLane, U32, U32, U32, U32)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1, U32)
VALUE_OPCODE(PermuteU32, U32, U32, U32, U1)
VALUE_OPCODE(BpermuteU32, U32, U32, U32, U1)
@@ -266,6 +269,7 @@ VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicSMax32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMax32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicAnd32, U32, BufferResource, U32, U32, U32, U32, U1)
@@ -308,8 +312,8 @@ VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
VALUE_OPCODE(SharedAtomicXor32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicMaskedOr32, Void, U32, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U32, U32, U1)
VALUE_OPCODE(SwizzleU32, U32, U32, U32, U1)
VALUE_OPCODE(ImageQueryDimensions, U32x4, ImageResource, ImageAddress)
@@ -167,6 +167,16 @@ bool ReplaceBinaryIdentity(Inst& inst, Type type, uint64_t identity) {
return false;
}
// Whether one operand is the other's negation.
bool IsComplementPair(const Inst& inst, ValueOpcode negation) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
const auto* negate = Arg(inst, arg).TryInstruction();
if (negate != nullptr && negate->GetOpcode() == negation &&
Arg(*negate, 0) == Arg(inst, arg ^ 1u)) return true;
}
return false;
}
bool FoldSelect(Inst& inst) {
const auto condition = Arg(inst, 0);
const auto true_value = Arg(inst, 1);
@@ -626,8 +636,9 @@ void FoldInstruction(Block& block, Block::iterator instruction,
case ValueOpcode::BitwiseXor32:
if (!FoldU32(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) {
return opcode == ValueOpcode::BitwiseOr32 ? a | b : a ^ b;
})) {
ReplaceBinaryIdentity(inst, Type::U32, 0u);
}) && !ReplaceBinaryIdentity(inst, Type::U32, 0u) &&
IsComplementPair(inst, ValueOpcode::BitwiseNot32)) {
Replace(inst, Value(UINT32_MAX));
}
return;
case ValueOpcode::BitwiseNot32: {
@@ -802,6 +813,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
Replace(inst, lhs.U1() ? lhs : rhs);
} else if (IsImmediate(rhs, Type::U1)) {
Replace(inst, rhs.U1() ? rhs : lhs);
} else if (IsComplementPair(inst, ValueOpcode::LogicalNot)) {
Replace(inst, Value(true));
} else {
const auto* left = lhs.TryInstruction();
const auto* right = rhs.TryInstruction();
@@ -892,8 +905,10 @@ private:
if (inst.GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
!Immediate(Arg(inst, 1), 0u)) return {};
const auto* source = Arg(inst, 0).TryInstruction();
return source != nullptr && source->GetOpcode() == ValueOpcode::Ballot
? Arg(*source, 0) : Value {};
if (source == nullptr) return {};
// Helper lanes keep the per-lane EXEC, so the live mask projects to true.
if (source->GetOpcode() == ValueOpcode::LiveMask) return Value(true);
return source->GetOpcode() == ValueOpcode::Ballot ? Arg(*source, 0) : Value {};
}
bool CanProject(Value value) {
@@ -99,89 +99,66 @@ Value GetRealValue(PhiMap& phi_map, Value source, uint32_t lane, uint32_t wave_s
return Value(&phi);
}
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction.
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction. Wave64 first joins
// the two rows of each half; a wave32 half is one row.
// Fragment subgroups may have no physical invocation at either endpoint.
class UMinReductionMatcher {
class WaveReductionMatcher {
public:
explicit UMinReductionMatcher(Block* block): block(block) {}
WaveReductionMatcher(const Block* block, ValueOpcode operation)
: block(block), operation(operation) {}
Value Match(const Inst& minimum) {
const auto* low = Get(minimum.Arg(0), ValueOpcode::ReadLane);
const auto* high = Get(minimum.Arg(1), ValueOpcode::ReadLane);
Value Match(const Inst& reduction, uint32_t wave_size) const {
const auto* low = Get(reduction.Arg(0), ValueOpcode::ReadLane);
const auto* high = Get(reduction.Arg(1), ValueOpcode::ReadLane);
if (low == nullptr || high == nullptr || low->Arg(0).Resolve() != high->Arg(0).Resolve()) {
return {};
}
const auto lane = [](Value value, uint32_t expected) {
const auto lane = [&](Value value, uint32_t expected) {
value = value.Resolve();
return value.IsImmediate() && value.GetType() == Type::U32 &&
value.U32() % 64u == expected;
value.U32() % wave_size == expected;
};
if (!((lane(low->Arg(1), 31) && lane(high->Arg(1), 63)) ||
(lane(low->Arg(1), 63) && lane(high->Arg(1), 31)))) {
const auto half = wave_size / 2u;
if (!((lane(low->Arg(1), half - 1u) && lane(high->Arg(1), wave_size - 1u)) ||
(lane(low->Arg(1), wave_size - 1u) && lane(high->Arg(1), half - 1u)))) {
return {};
}
const auto* joined = Get(Unwrap(low->Arg(0)), ValueOpcode::UMin32);
if (wave_size == 32u) return MatchScan(low->Arg(0));
const auto* joined = Get(low->Arg(0), operation);
if (joined == nullptr) return {};
for (size_t index = 0; index < 2; index++) {
auto attempt = *this;
const auto* cross = attempt.Get(attempt.Unwrap(joined->Arg(index)), ValueOpcode::Permlane16U32);
const auto* cross = Get(joined->Arg(index), ValueOpcode::Permlane16U32);
if (cross == nullptr) continue;
const auto flags = cross->Flags<PermlaneFlags>();
auto prefix = attempt.Unwrap(joined->Arg(1 - index));
const auto flags = cross->Flags<PermlaneFlags>();
const auto prefix = joined->Arg(1 - index).Resolve();
if (!flags.x16 || flags.fetch_inactive || !flags.bound_control ||
cross->Arg(1).Resolve() != Value(UINT32_MAX) ||
cross->Arg(2).Resolve() != Value(UINT32_MAX) ||
!attempt.FullExec(cross->Arg(3)) || attempt.Unwrap(cross->Arg(0)) != prefix) {
cross->Arg(2).Resolve() != Value(UINT32_MAX) || !FullExec(cross->Arg(3)) ||
cross->Arg(0).Resolve() != prefix) {
continue;
}
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
prefix = attempt.MatchRow(prefix, shift);
}
prefix = attempt.Unwrap(prefix);
if (prefix.IsEmpty() || attempt.saved_exec.IsEmpty()) return prefix;
const auto* seed = attempt.Get(prefix, ValueOpcode::SelectU32);
if (seed != nullptr && seed->Arg(2).Resolve() == Value(UINT32_MAX)) {
const auto predicate = seed->Arg(0).Resolve();
const auto* subset = attempt.Get(predicate, ValueOpcode::LogicalAnd);
if (predicate == attempt.saved_exec ||
(subset != nullptr && (subset->Arg(0).Resolve() == attempt.saved_exec ||
subset->Arg(1).Resolve() == attempt.saved_exec))) {
return prefix;
}
}
if (const auto seed = MatchScan(prefix); !seed.IsEmpty()) return seed;
}
return {};
}
private:
// The row scan's input.
Value MatchScan(Value prefix) const {
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
prefix = MatchRow(prefix, shift);
}
return prefix;
}
Inst* Get(Value value, ValueOpcode opcode) const {
auto* inst = value.Resolve().TryInstruction();
return inst != nullptr && inst->Parent() == block && inst->GetOpcode() == opcode
? inst : nullptr;
}
bool FullExec(Value predicate) {
predicate = predicate.Resolve();
if (predicate == Value(true)) return true;
const auto* either = Get(predicate, ValueOpcode::LogicalOr);
if (either == nullptr) return false;
for (size_t index = 0; index < 2; index++) {
const auto* negate = Get(either->Arg(index), ValueOpcode::LogicalNot);
if (negate == nullptr || negate->Arg(0).Resolve() != either->Arg(1 - index).Resolve()) continue;
const auto original = negate->Arg(0).Resolve();
if (saved_exec.IsEmpty()) saved_exec = original;
return saved_exec == original;
}
return false;
}
Value Unwrap(Value value) {
while (const auto* select = Get(value, ValueOpcode::SelectU32)) {
if (!FullExec(select->Arg(0))) break;
value = select->Arg(1);
}
return value.Resolve();
}
// Constant propagation folds the x | ~x EXEC of s_orn2_saveexec to true.
static bool FullExec(Value predicate) { return predicate.Resolve() == Value(true); }
static bool IsRowShift(const Inst& inst, uint32_t shift) {
const auto flags = inst.Flags<DppMoveFlags>();
@@ -189,22 +166,24 @@ private:
flags.bank_mask == 0xf && !flags.fetch_inactive && !flags.bound_control && !flags.dpp8;
}
Value MatchRow(Value value, uint32_t shift) {
const auto* update = Get(Unwrap(value), ValueOpcode::DppUpdateU32);
Value MatchRow(Value value, uint32_t shift) const {
const auto* update = Get(value, ValueOpcode::DppUpdateU32);
if (update == nullptr || !IsRowShift(*update, shift) || !FullExec(update->Arg(2))) return {};
const auto* minimum = Get(update->Arg(0), ValueOpcode::UMin32);
if (minimum == nullptr) return {};
const auto* combined = Get(update->Arg(0), operation);
if (combined == nullptr) return {};
for (size_t index = 0; index < 2; index++) {
const auto* move = Get(minimum->Arg(index), ValueOpcode::DppMoveU32);
const auto* move = Get(combined->Arg(index), ValueOpcode::DppMoveU32);
if (move == nullptr || !IsRowShift(*move, shift) || !FullExec(move->Arg(1))) continue;
const auto previous = Unwrap(minimum->Arg(1 - index));
if (Unwrap(move->Arg(0)) == previous && Unwrap(update->Arg(1)) == previous) return previous;
const auto previous = combined->Arg(1 - index).Resolve();
if (move->Arg(0).Resolve() == previous && update->Arg(1).Resolve() == previous) {
return previous;
}
}
return {};
}
Block* block;
Value saved_exec;
const Block* block;
ValueOpcode operation;
};
} // namespace
@@ -217,18 +196,22 @@ ReadLaneStats EliminateReadLane(Program& program, uint32_t wave_size) {
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (wave_size == 64u && inst.GetOpcode() == ValueOpcode::UMin32) {
const auto seed = UMinReductionMatcher(block).Match(inst);
const auto opcode = inst.GetOpcode();
if (opcode == ValueOpcode::UMin32 || opcode == ValueOpcode::UMax32) {
const auto seed = WaveReductionMatcher(block, opcode).Match(inst, wave_size);
if (!seed.IsEmpty()) {
const auto position = std::find_if(block->begin(), block->end(),
[&](const Inst& candidate) { return &candidate == &inst; });
auto& reduction = *block->PrependNewInst(position, ValueOpcode::SubgroupUMinU32, {seed});
const auto reduce = opcode == ValueOpcode::UMin32
? ValueOpcode::SubgroupUMinU32
: ValueOpcode::SubgroupUMaxU32;
auto& reduction = *block->PrependNewInst(position, reduce, {seed});
inst.ReplaceUsesWith(Value(&reduction));
stats.rewritten_reductions++;
}
continue;
}
if (inst.GetOpcode() != ValueOpcode::ReadLane) {
if (opcode != ValueOpcode::ReadLane) {
continue;
}
const auto selector = inst.Arg(1).Resolve();
@@ -3,6 +3,7 @@
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/shaderBindings.h"
@@ -186,35 +187,6 @@ const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
return &program.descriptor_sources[source];
}
void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
const DescriptorSource* source, Value extra = {}) {
if (source == nullptr && extra.IsEmpty()) {
return;
}
std::vector<Value> pending;
if (source != nullptr) {
pending.assign(source->dwords.begin(), source->dwords.begin() + source->dword_count);
}
if (!extra.IsEmpty()) pending.push_back(extra);
std::vector<const Inst*> visited;
while (!pending.empty()) {
auto* inst = pending.back().Resolve().TryInstruction();
pending.pop_back();
if (inst == nullptr || std::ranges::find(visited, inst) != visited.end()) {
continue;
}
visited.push_back(inst);
if (planned_reads.contains(inst)) {
auto flags = inst->Flags<SrtReadFlags>();
flags.clean = 1u;
inst->SetFlags(flags);
}
for (size_t arg = 0; arg < inst->NumArgs(); arg++) {
pending.push_back(inst->Arg(arg));
}
}
}
bool ReadTable(uint64_t base, uint64_t size, uint64_t offset,
const SrtRuntime& runtime, std::span<uint32_t> words) {
const auto count = std::min<uint64_t>(words.size(), offset < size ? (size - offset) / 4u : 0u);
@@ -309,22 +281,22 @@ bool CanReadIndexedSelector(const ShaderBufferResource& material, uint32_t offse
uint64_t {UINT32_MAX} + 1u;
}
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
bool EvaluateIndirectCount(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect,
const SrtRuntime& runtime, uint32_t& count) {
if (indirect.workgroup_axis != UINT32_MAX) {
if (runtime.workgroup_counts.size() != 3u || indirect.workgroup_axis >= 3u) return false;
count = std::ranges::find(runtime.workgroup_counts, 0u) != runtime.workgroup_counts.end()
? 0u : runtime.workgroup_counts[indirect.workgroup_axis];
return true;
}
if (!clean.Evaluate(indirect.key_count, count)) return false;
if (!WalkedValue(program, indirect.key_count, count)) return false;
if (indirect.signed_count && std::bit_cast<int32_t>(count) < 0) count = 0;
return true;
}
bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect,
const DescriptorValue& material_value, const SrtRuntime& runtime,
SrtWalker& clean, std::vector<uint32_t>& keys) {
std::vector<uint32_t>& keys) {
const auto* selector = &*indirect.selector;
keys.clear();
const auto read_selector = [&](uint64_t base, uint64_t size, uint64_t offset,
@@ -359,11 +331,23 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
keys.resize(count);
return true;
};
// Reads the key of each record whose rank is set.
const auto read_ranks = [&](uint64_t base, uint64_t size, uint64_t first, uint64_t step,
uint32_t ranks) {
keys.reserve(std::popcount(ranks));
for (; ranks != 0u; ranks &= ranks - 1u) {
const auto offset = first + std::countr_zero(ranks) * step;
uint32_t key = 0;
if (offset > UINT32_MAX || !read_selector(base, size, offset, {&key, 1})) return false;
keys.push_back(key);
}
return true;
};
if (selector->rows) {
const auto& rows = *selector->rows;
DescriptorValue count_value;
ShaderBufferResource counts, material;
if (!clean.EvaluateDescriptor(rows.source, count_value) ||
if (!WalkedDescriptor(program, rows.source, count_value) ||
!DecodeBufferDescriptor(count_value, counts) ||
!DecodeBufferDescriptor(material_value, material) || selector->stride != 4u ||
selector->offset != 0u || rows.stride == 0u || (rows.stride & 3u) != 0u ||
@@ -392,7 +376,7 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
if (candidate_end - first > MaxIndirectDescriptorProbes) break;
end = candidate_end;
}
// Coalesce small gaps, but interpret only selected prefixes. The strict
// Coalesce small gaps, but interpret only selected prefixes. The specialization
// reader captures the entire fetched span for writable-alias rejection.
batch.resize(end - first);
if (!ReadScalarTable(material.Base48(), material.GetSize(), first * 4u, runtime, batch))
@@ -405,19 +389,21 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
}
} else if (selector->bitset) {
ShaderBufferResource material;
std::array<uint32_t, 4> masks {};
std::array<uint32_t, 8> storage {};
const auto record_words = std::min<size_t>(selector->stride / 4u, storage.size());
const auto masks = std::span(storage).first(record_words);
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) ||
!clean.Evaluate(indirect.selector_first, first) ||
!EvaluateIndirectCount(indirect, runtime, clean, count) || count != masks.size() * 32u ||
selector->stride != sizeof(masks) || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
!WalkedValue(program, indirect.selector_first, first) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count != masks.size() * 32u ||
selector->stride != masks.size_bytes() || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != selector->stride ||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
(material.Base48() & 3u) != 0u) return false;
if (material.Format() != Prospero::BufferFormat::kInvalid) {
const auto words = material.GetSize() / sizeof(uint32_t);
// Bitmap storage can grow with resolution while its candidate domain stays
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
// Bitmap storage can grow with resolution while its candidate domain stays at 32 keys
// per record word. Bound scratch memory, preserving each coherent read's capture.
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
@@ -462,8 +448,8 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
base = material.Base48();
size = material.GetSize();
}
if (!clean.Evaluate(indirect.selector_first, first) ||
!EvaluateIndirectCount(indirect, runtime, clean, count) || count > MaxIndirectDescriptorProbes)
if (!WalkedValue(program, indirect.selector_first, first) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count > MaxIndirectDescriptorProbes)
return false;
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
@@ -473,35 +459,48 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
uint64_t {first} + count > material.NumRecords())) return false;
const auto last = count != 0u ? uint64_t {first} + count - 1u : 0u;
if (last > UINT32_MAX) return false;
// Out-of-bounds scalar reads return zero.
const auto end = count != 0u ? last * step + selector->offset + 4u : 0u;
if (end > uint64_t {UINT32_MAX} + 1u ||
(indirect.workgroup_axis == UINT32_MAX && end > size))
return false;
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
if (end > uint64_t {UINT32_MAX} + 1u) return false;
const auto start = uint64_t {first} * step + selector->offset;
if (selector->ranks) {
// A scan selects the ranks some word of its mask's address class sets.
const auto& mask = *selector->ranks;
DescriptorValue mask_value;
ShaderBufferResource masks;
if (count > 32u || !WalkedDescriptor(program, mask.source, mask_value) ||
!DecodeBufferDescriptor(mask_value, masks)) return false;
// Each chunk holds whole strides of the power-of-two class, or one of its words.
const auto words = mask.stride / 4u;
const auto chunk =
words <= MaxIndirectDescriptorProbes ? MaxIndirectDescriptorProbes : 1u;
uint32_t ranks = 0;
for (auto at = uint64_t {mask.offset}; at + 4u <= masks.GetSize();
at += uint64_t {std::max(chunk, words)} * 4u) {
keys.resize(std::min<uint64_t>((masks.GetSize() - at) / 4u, chunk));
if (!ReadScalarTable(masks.Base48(), masks.GetSize(), at, runtime, keys))
return false;
for (size_t word = 0; word < keys.size(); word += words) ranks |= keys[word];
}
keys.clear();
if (count < 32u) ranks &= (1u << count) - 1u;
if (!read_ranks(base, size, start, step, ranks)) return false;
} else if (!read_keys(base, size, start, step, count)) {
return false;
}
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
if (material_value.dword_count != 2u ||
!clean.Evaluate(indirect.selector_mask, mask) ||
!EvaluateIndirectCount(indirect, runtime, clean, count) || count == 0u || count > 32u) {
!WalkedValue(program, indirect.selector_mask, mask) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count == 0u || count > 32u) {
return false;
}
if (count < 32u) mask &= (1u << count) - 1u;
const auto material_base =
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(selector->offset) +
static_cast<uint64_t>(index) * selector->stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
{&key, 1})) return false;
keys.push_back(key);
mask &= mask - 1u;
}
if (!read_ranks(material_base, UINT64_MAX, selector->offset, selector->stride, mask))
return false;
} else if (selector->stride == 0u) {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) ||
@@ -531,6 +530,7 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
for (auto& key: keys)
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
if (selector->excluded) std::erase(keys, *selector->excluded);
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
@@ -541,8 +541,7 @@ template <typename Specialization, typename Normalize>
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
uint32_t source_index,
uint32_t resource_index, uint32_t dword_count, uint32_t read_dwords,
const SrtRuntime& runtime, SrtWalker& clean,
ResourceSnapshot& snapshot,
const SrtRuntime& runtime, ResourceSnapshot& snapshot,
std::vector<DescriptorValue>& descriptors,
std::vector<Specialization>& specializations,
Normalize&& normalize) {
@@ -605,8 +604,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (selector == nullptr) keys.clear();
DescriptorValue material_value;
if ((selector != nullptr && (byte_keys || !selector_keys->resolved) &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
!WalkedDescriptor(program, selector->source, material_value)) ||
!WalkedDescriptor(program, indirect.table_source, table_value)) {
return false;
}
if (table_value.dword_count == 2u) {
@@ -659,7 +658,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
: uint64_t {1} << 32u;
if (!indirect.key_count.IsEmpty()) {
uint32_t key_count = 0u;
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
if (!EvaluateIndirectCount(program, indirect, runtime, key_count)) return false;
domain = std::min(domain, uint64_t {key_count});
}
if (indexed_pointer &&
@@ -771,8 +770,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (selector == nullptr) {
uint32_t key_count = 0;
uint32_t first_key = 0;
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
if (!indirect.selector_first.IsEmpty() && !clean.Evaluate(indirect.selector_first, first_key))
if (!EvaluateIndirectCount(program, indirect, runtime, key_count)) return false;
if (!indirect.selector_first.IsEmpty() && !WalkedValue(program, indirect.selector_first, first_key))
return false;
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
(indirect.selector_first.IsEmpty() && key_count != 0u &&
@@ -785,10 +784,16 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::iota(keys.begin(), keys.end(), first_key);
if (!keys.empty() && keys.back() < keys.front())
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
if (indirect.workgroup_shift != 0u) {
for (auto& key: keys)
key = static_cast<uint32_t>(uint64_t {key} * indirect.workgroup_multiplier >>
indirect.workgroup_shift);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
} else {
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
if (!selector_keys->resolved) {
if (!ResolveSelectorKeys(indirect, material_value, runtime, clean, keys)) return false;
if (!ResolveSelectorKeys(program, indirect, material_value, runtime, keys)) return false;
selector_keys->resolved = true;
}
}
@@ -827,7 +832,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::span(candidate.dwords).first(read_dwords))) {
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
}
} else if (!clean.EvaluateDescriptor(sources[entry], candidate)) {
} else if (!WalkedDescriptor(program, sources[entry], candidate)) {
return false;
}
if (!normalize(candidate))
@@ -1070,6 +1075,26 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
return dimension == Decoder::ImageDimension::Dim1D ||
dimension == Decoder::ImageDimension::Dim2D;
};
// The descriptor's type selects the address layout. A 2D or 3D candidate reads the leading
// coordinates of an equal or larger dimension, but an array with more than one slice reads
// the next as its slice. Derivative, LOD, clamp and mip operands shift with the dimension.
const auto leading = [&](Decoder::ImageDimension dimension, const DescriptorValue& value) {
const bool sliced =
static_cast<Prospero::ImageType>((value.dwords[3] >> 28u) & 0xfu) ==
Prospero::ImageType::kColor2DArray &&
(value.dwords[4] & 0x1fffu) != ((value.dwords[4] >> 16u) & 0x1fffu);
return !sliced &&
(dimension == Decoder::ImageDimension::Dim2D || dimension == base.dimension);
};
const bool leading_coordinates = (base.dimension == Decoder::ImageDimension::Dim2D ||
base.dimension == Decoder::ImageDimension::Dim3D) &&
leading(image_class.dimension, snapshot.images[exemplar]) &&
std::ranges::none_of(program.memory_info, [&](const MemoryInfo& memory) {
return memory.kind == ResourceKind::Image && memory.resource == root_index &&
(memory.image_has_mip || (memory.image_sample_flags &
(Decoder::ImageSampleFlagLod | Decoder::ImageSampleFlagDerivative |
Decoder::ImageSampleFlagLodClamp)) != 0u);
});
for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) {
auto& image = specialization.images[candidate];
if (image.indirect_root != root_index) {
@@ -1089,8 +1114,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
base.dimension == Decoder::ImageDimension::Dim2D &&
!image.cube && !image_class.cube && is_1d_or_2d(image.dimension) &&
is_1d_or_2d(image_class.dimension);
const bool prefix_coordinates =
leading_coordinates && leading(image.dimension, snapshot.images[candidate]);
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
!unfiltered_coordinates) ||
!unfiltered_coordinates && !prefix_coordinates) ||
image.conversion_format != image_class.conversion_format ||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
return SpecializationFail(
@@ -1193,14 +1220,16 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
block.sources.push_back(program.info.buffers.at(memory.resource).source);
} else {
block.sources.push_back(program.info.images.at(memory.resource).source);
block.images.push_back(memory.resource);
if (image.needs_sampler) {
block.sources.push_back(program.info.samplers.at(memory.sampler).source);
}
}
}
std::ranges::sort(block.sources);
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
block.sources.end());
for (auto* list: {&block.sources, &block.images}) {
std::ranges::sort(*list);
list->erase(std::unique(list->begin(), list->end()), list->end());
}
}
for (auto& block: blocks) block.condition = predicate(block.condition);
if (std::ranges::none_of(
@@ -1341,6 +1370,17 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
return result;
}
// State an extracted or loaded plan derives from its fields: one key set per selector group, which
// extraction numbers densely in order of first use (false when they are not), and the walker.
static bool DerivePlanState(ResourcePlan& plan) {
for (const auto group: plan.selector_groups) {
if (group == plan.selector_keys.size()) plan.selector_keys.emplace_back();
if (group != UINT32_MAX && group >= plan.selector_keys.size()) return false;
}
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
return true;
}
ResourcePlan ExtractResourcePlan(const Program& program) {
ResourcePlan plan;
plan.stage = program.stage;
@@ -1380,16 +1420,13 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (const auto found = cloned.find(source); found != cloned.end()) {
return Value(found->second);
}
if (source->GetOpcode() == ValueOpcode::LoadBufferU32) {
plan.requires_specialization_memory = true;
plan.capture_specialization_reads = true;
}
if (source->GetOpcode() == ValueOpcode::LoadBufferU32) plan.capture_specialization_reads = true;
auto& target =
plan.value_storage.emplace_back(source->GetOpcode(), source->Flags<uint64_t>());
if (source->GetOpcode() == ValueOpcode::LoadAddressU32 ||
source->GetOpcode() == ValueOpcode::ReadConstBuffer ||
source->GetOpcode() == ValueOpcode::LoadBufferU32) {
target.SetFlags(SrtReadFlags {.index = source->Flags<MemoryFlags>().index});
target.SetFlags(MemoryFlags {.index = source->Flags<MemoryFlags>().index});
}
cloned.emplace(source, &target);
if (source->GetOpcode() == ValueOpcode::Phi) {
@@ -1411,7 +1448,6 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
target.dword_count = source.dword_count;
target.indirect_descriptor = source.indirect_descriptor;
if (target.indirect_descriptor.has_value()) {
plan.requires_specialization_memory = true;
capture_indirect_reads = true;
target.indirect_descriptor->key_count = Clone(target.indirect_descriptor->key_count);
target.indirect_descriptor->selector_first =
@@ -1425,17 +1461,18 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
// Destination tables share keys when their complete selector domains agree.
// Resolve those domains lazily at dispatch time, without repeated guest reads.
std::vector<uint32_t> group_sources; // The first source of each group.
plan.selector_groups.resize(plan.descriptor_sources.size(), UINT32_MAX);
for (uint32_t source = 0; source < plan.descriptor_sources.size(); ++source) {
const auto& descriptor = program.descriptor_sources[source].indirect_descriptor;
if (!descriptor || !descriptor->selector) continue;
const auto& a = *descriptor;
uint32_t group = 0;
for (; group < plan.selector_keys.size(); ++group) {
const auto& b = *program.descriptor_sources[plan.selector_keys[group].source].indirect_descriptor;
for (; group < group_sources.size(); ++group) {
const auto& b = *program.descriptor_sources[group_sources[group]].indirect_descriptor;
if (EquivalentSelectorDomain(program, a, b)) break;
}
if (group == plan.selector_keys.size()) plan.selector_keys.push_back({.source = source});
if (group == group_sources.size()) group_sources.push_back(source);
plan.selector_groups[source] = group;
}
plan.srt_reads.reserve(program.srt_reads.size());
@@ -1523,57 +1560,16 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
// Every former flat alias now targets its retained raw read. Mark those reads
// on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
std::unordered_set<const Inst*> planned_reads;
for (const auto& read: plan.srt_reads) {
auto* inst = read.value.ResolveInstruction();
EXIT_IF(inst == nullptr || (inst->GetOpcode() != ValueOpcode::LoadAddressU32 &&
inst->GetOpcode() != ValueOpcode::ReadConstBuffer));
planned_reads.insert(inst);
}
for (const auto& source: plan.descriptor_sources) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
if (indirect.selector) {
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
if (indirect.selector->rows)
MarkCleanReads(planned_reads, Source(plan, indirect.selector->rows->source));
}
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
if (indirect.sources.empty()) {
MarkCleanReads(planned_reads, Source(plan, indirect.table_source));
} else {
for (const auto candidate: indirect.sources) {
MarkCleanReads(planned_reads, Source(plan, candidate));
}
}
}
// Clean writable addresses prove that shader writes cannot overlap captured reads.
for (const auto& buffer: plan.info.buffers) {
if (buffer.written) MarkCleanReads(planned_reads, Source(plan, buffer.source));
}
for (const auto& image: plan.info.images) {
if (image.written) MarkCleanReads(planned_reads, Source(plan, image.source));
}
}
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
CompileSrtWalker(plan);
DerivePlanState(plan);
return plan;
}
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization) {
for (auto& keys: program.selector_keys) keys.resolved = false;
if (!program.resource_tracking_complete ||
((program.requires_specialization_memory || !program.source_reads.empty()) &&
runtime.read_specialization_memory == nullptr)) {
return false;
}
if (!program.resource_tracking_complete) return false;
const auto uses_workgroup_count = [](const DescriptorSource& source) {
return source.indirect_descriptor && source.indirect_descriptor->workgroup_axis != UINT32_MAX;
};
@@ -1582,20 +1578,16 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
std::ranges::any_of(program.descriptor_sources, uses_workgroup_count));
auto& reads = snapshot.specialization_reads;
reads = program.source_reads;
SrtReadCapture capture {runtime, reads};
auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
SrtWalker clean(program, CleanRuntime(observed));
SrtWalker walker(program, observed,
capture_reads || program.requires_specialization_memory ? &clean : nullptr);
if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) {
if (!WalkSrt(program, runtime, snapshot.flattened_srt, capture_reads ? &reads : nullptr)) {
return SpecializationFail(fmt::format("SRT refresh complete={} reads={} user_data={}",
program.srt_plan_complete, program.srt_reads.size(), runtime.user_data.size()));
}
const auto active = std::span<const uint8_t>(program.active_sources);
SrtReadCapture capture {runtime, reads};
auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
std::array<uint32_t, 3> workgroup_counts {};
if (runtime.indirect_args_address != 0u) {
for (uint32_t source = 0; source < program.descriptor_sources.size(); ++source) {
if ((!active.empty() && !active[source]) ||
if (!WalkedActive(program, source) ||
!uses_workgroup_count(program.descriptor_sources[source])) continue;
if ((runtime.indirect_args_address & 3u) != 0u ||
!ReadTable(runtime.indirect_args_address, sizeof(workgroup_counts), 0u,
@@ -1610,24 +1602,24 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
std::array<uint32_t, 4> stored {};
bool uniform_fill = words != 0;
if (uniform_fill && !fill.pattern_size.IsEmpty())
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
uniform_fill = WalkedValue(program, fill.pattern_size, words) && words >= 1 && words <= 4;
uint32_t elements = 0;
if (uniform_fill && !fill.element_count.IsEmpty())
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
uniform_fill = WalkedValue(program, fill.element_count, elements) && elements != 0;
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
uniform_fill = WalkedValue(program, fill.values[i], stored[i]) && stored[i] == stored[0];
}
if (uniform_fill) {
snapshot.uniform_fill = fill.fill;
snapshot.uniform_fill.value = stored[0];
snapshot.uniform_fill.bounded_elements = elements;
}
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
const auto evaluate = [&](uint32_t source, DescriptorValue& value) {
if (source >= program.descriptor_sources.size()) {
return false;
}
if (active.empty() || active[source]) {
return (capture_reads && written ? clean : walker).EvaluateDescriptor(source, value) ||
if (WalkedActive(program, source)) {
return WalkedDescriptor(program, source, value) ||
SpecializationFail(fmt::format("descriptor evaluation source={}", source));
}
value = {};
@@ -1651,9 +1643,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (source == nullptr) return false;
if (source->indirect_descriptor.has_value()) {
snapshot.buffers[i] = {.dword_count = 4u};
if (active.empty() || active[base.source]) {
if (WalkedActive(program, base.source)) {
if (!MaterializeIndirectDescriptor(
program, base.source, i, 4u, 4u, observed, clean, snapshot,
program, base.source, i, 4u, 4u, observed, snapshot,
snapshot.buffers, specialization.buffers,
[&](DescriptorValue& value) {
return NormalizeIndirectBuffer(
@@ -1661,7 +1653,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}))
return resource_failed("buffer", i, base.source, snapshot.buffers.size());
}
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
} else if (!evaluate(base.source, snapshot.buffers[i])) {
return false;
}
}
@@ -1711,13 +1703,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
if (source->indirect_descriptor.has_value()) {
snapshot.images[i] = {.dword_count = 8u};
if (!active.empty() && !active[image.source]) {
if (!WalkedActive(program, image.source) || !WalkedImageActive(program, i)) {
continue;
}
const auto& indirect = *source->indirect_descriptor;
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, snapshot, snapshot.images,
specialization.images, [&](DescriptorValue& value) {
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
@@ -1728,10 +1720,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return resource_failed("image", i, image.source, snapshot.images.size());
}
} else {
if (!evaluate(image.source, snapshot.images[i], image.written)) {
if (!evaluate(image.source, snapshot.images[i])) {
return false;
}
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
// An image listed only by unreached blocks reads zero, not its source's descriptor.
if (!WalkedImageActive(program, i) ||
!ValidImageDescriptor(snapshot.images[i], image.r128)) {
snapshot.images[i].dwords.fill(0);
}
}
@@ -1749,8 +1743,8 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
const auto* indirect = descriptor->indirect_descriptor ? &*descriptor->indirect_descriptor : nullptr;
if (indirect != nullptr && indirect->sources.empty()) {
snapshot.samplers[i] = {.dword_count = 4u};
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
if (WalkedActive(program, source) && !MaterializeIndirectDescriptor(
program, source, i, 4u, 4u, observed, snapshot, snapshot.samplers,
specialization.samplers,
[](DescriptorValue&) { return true; }))
return resource_failed("sampler", i, source, snapshot.samplers.size());
@@ -1777,8 +1771,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
uint32_t ImageHeapKind(const ImageResource& root, DescriptorValue& value) {
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, and
// entries that need shader-side conversion, read as null instead of failing the draw.
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, entries of
// another dimension and entries that need shader-side conversion read as null instead of
// failing the draw.
const auto format = static_cast<Prospero::BufferFormat>((value.dwords[1] >> 20u) & 0x1ffu);
const auto tile = static_cast<Prospero::TileMode>((value.dwords[3] >> 20u) & 0x1fu);
const auto numeric_class =
@@ -2065,4 +2060,86 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
image_remap.Apply(images);
}
namespace {
using ControlPhis = decltype(ResourcePlan::control_phi_predecessors);
// Instructions come first, so that any value can refer to them by index, then their arguments.
// Plan Phis have no blocks; a control Phi lists the plan blocks of its arguments after them.
void IoValues(Writer& writer, const std::list<Inst>& values, const ControlPhis& phis) {
IoAll(writer, static_cast<uint32_t>(values.size()));
for (const auto& inst: values) {
writer.index.emplace(&inst, static_cast<uint32_t>(writer.index.size()));
IoAll(writer, inst.GetOpcode(), inst.Flags<uint64_t>());
}
for (const auto& inst: values) {
if (inst.GetOpcode() != ValueOpcode::Phi) {
for (size_t index = 0; index < inst.NumArgs(); ++index) IoAll(writer, inst.Arg(index));
continue;
}
std::vector<Value> args;
for (size_t index = 0; index < inst.NumArgs(); ++index) {
EXIT_IF(inst.PhiBlock(index) != nullptr);
args.push_back(inst.Arg(index));
}
const auto found = phis.find(&inst);
IoAll(writer, args, found != phis.end() ? found->second : std::vector<uint32_t> {});
}
}
void IoValues(Reader& reader, std::list<Inst>& values, ControlPhis& phis) {
uint32_t count = 0;
Io(reader, count);
count = reader.Count(count);
for (uint32_t index = 0; index < count && reader.ok; ++index) {
ValueOpcode opcode {};
uint64_t flags = 0;
IoAll(reader, opcode, flags);
reader.ok = reader.ok && opcode < ValueOpcode::Count;
if (reader.ok) reader.insts.push_back(&values.emplace_back(opcode, flags));
}
for (auto* inst: reader.insts) {
if (inst->GetOpcode() != ValueOpcode::Phi) {
for (size_t index = 0; index < inst->NumArgs(); ++index) {
Value arg;
Io(reader, arg);
inst->SetArg(index, arg);
}
continue;
}
std::vector<Value> args;
std::vector<uint32_t> predecessors;
IoAll(reader, args, predecessors);
for (const auto arg: args) inst->AddPhiOperand(nullptr, arg);
if (!predecessors.empty()) phis.emplace(inst, std::move(predecessors));
}
}
// Binds every plan field: one added later fails to compile here until it is stored or derived.
template <class S, class P> void IoPlan(S& io, P& plan) {
auto& [stage, shader_hash, user_data_base, user_data_count, values, memory_info, descriptor_sources,
selector_groups, control_flow, control_phis, srt_reads, source_reads, capture_specialization_reads,
srt_plan_complete, resource_tracking_complete, info, uniform_fill,
// Derived on load.
srt_code, srt_user_data, srt_captures, srt_scratch, material_keys, selector_keys] = plan;
IoValues(io, values, control_phis);
IoAll(io, stage, shader_hash, user_data_base, user_data_count, memory_info, descriptor_sources,
selector_groups, control_flow, srt_reads, source_reads, capture_specialization_reads,
srt_plan_complete, resource_tracking_complete, info, uniform_fill);
}
} // namespace
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan) {
Writer writer;
IoPlan(writer, plan);
return std::move(writer.bytes);
}
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan) {
Reader reader {bytes};
IoPlan(reader, plan);
return reader.ok && reader.bytes.empty() && DerivePlanState(plan);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -53,6 +53,11 @@ struct ResourceSpecialization {
// its values and is independent of the translated shader CFG.
ResourcePlan ExtractResourcePlan(const Program& program);
// Shader cache record of a plan; loading it into a default-constructed plan rebuilds the walker
// and scratch state like extraction. A failed load leaves a plan that must not be used.
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan);
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan);
// Refreshes cached resources and specialization in place. A failed refresh must not be used.
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization);
@@ -7,6 +7,8 @@
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstdio>
#include <fmt/format.h>
#include <map>
#include <numeric>
@@ -1014,7 +1016,9 @@ private:
(!shared_image_heap && (a.table_offset != b.table_offset ||
a.table_immediate != b.table_immediate ||
a.table_stride != b.table_stride)) || a.indexed != b.indexed ||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
a.workgroup_axis != b.workgroup_axis ||
a.workgroup_multiplier != b.workgroup_multiplier ||
a.workgroup_shift != b.workgroup_shift || a.sources != b.sources ||
a.signed_count != b.signed_count ||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
@@ -1136,31 +1140,63 @@ private:
LaneQuantifier lanes = LaneQuantifier::All;
};
// The immediate that edge proves value differs from on every lane, if any.
std::optional<uint32_t> ExcludedBy(const EdgePredicate& edge, Value value) const {
if (edge.lanes != LaneQuantifier::All) return {};
const auto* test = edge.condition.TryInstruction();
if (test != nullptr && test->NumArgs() == 2u &&
((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) ||
(test->GetOpcode() == ValueOpcode::IEqual32 && !edge.positive))) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
uint32_t immediate;
if (ImmediateU32(test->Arg(arg), immediate) &&
EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return immediate;
}
}
return {};
}
bool NonzeroOnEntry(Value value, const Block* block) const {
if (block == nullptr) return false;
bool witnessed = false;
const bool guarded = GuardedOnEntry(block, [](const Block* current) {
return current->ImmPredecessors().size() != 1u;
}, [&](const EdgePredicate& edge) {
if (edge.lanes != LaneQuantifier::All) return false;
const auto* test = edge.condition.TryInstruction();
if (test != nullptr && test->NumArgs() == 2u &&
((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) ||
(test->GetOpcode() == ValueOpcode::IEqual32 && !edge.positive))) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
uint32_t immediate;
if (ImmediateU32(test->Arg(arg), immediate) && immediate == 0u &&
EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return witnessed = true;
}
}
return false;
return ExcludedBy(edge, value) == 0u && (witnessed = true);
});
return guarded && witnessed;
}
uint32_t WorkgroupAxis(Value key) const {
// The key that every path from its definition to block rules out, if any.
std::optional<uint32_t> ExcludedKeyOnEntry(Value key, const Block* block) const {
const auto* definition = key.Resolve().TryInstruction();
if (definition == nullptr) return {};
std::optional<uint32_t> excluded;
const auto defined = [&](const Block* current) { return current == definition->Parent(); };
const bool guarded = GuardedOnEntry(block, defined, [&](const EdgePredicate& edge) {
const auto immediate = ExcludedBy(edge, key);
if (!immediate || (excluded && excluded != immediate)) return false;
excluded = immediate;
return true;
});
return guarded ? excluded : std::nullopt;
}
// The workgroup ID axis of key; with first, also of a runtime first + workgroup ID sum.
uint32_t WorkgroupAxis(Value key, Value* first = nullptr) const {
const auto* builtin = key.Resolve().TryInstruction();
uint32_t axis = UINT32_MAX;
if (first != nullptr && builtin != nullptr && builtin->GetOpcode() == ValueOpcode::IAdd32) {
for (uint32_t arm = 0; arm < 2u; ++arm) {
axis = WorkgroupAxis(builtin->Arg(arm));
const auto base = builtin->Arg(arm ^ 1u);
if (axis == UINT32_MAX ||
!ValidateRuntimeValue(m_program, base, RuntimeValueType::Integer)) continue;
*first = base;
return axis;
}
return UINT32_MAX;
}
if (m_program.stage != ShaderType::Compute || builtin == nullptr ||
builtin->GetOpcode() != ValueOpcode::GetBuiltin || builtin->NumArgs() != 2u ||
builtin->Arg(0) != Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) ||
@@ -1168,6 +1204,27 @@ private:
return axis;
}
// Matches key = (first + workgroup ID) * multiplier >> shift with shift >= 32; first and the
// division are optional.
bool MatchWorkgroupKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
uint32_t bits = 0, multiplier = 0;
const auto* shift = key.Resolve().TryInstruction();
if (shift != nullptr && shift->GetOpcode() == ValueOpcode::ShiftRightLogical32) {
const auto* high = shift->Arg(0).Resolve().TryInstruction();
if (high != nullptr && high->GetOpcode() == ValueOpcode::UMulHi &&
ImmediateU32(shift->Arg(1), bits) && bits < 32u &&
ImmediateU32(high->Arg(1), multiplier)) {
key = high->Arg(0);
bits += 32u;
}
}
indirect.workgroup_axis = WorkgroupAxis(key, &indirect.selector_first);
if (indirect.workgroup_axis == UINT32_MAX) return false;
indirect.workgroup_multiplier = multiplier;
indirect.workgroup_shift = bits;
return true;
}
bool MatchTableOffset(Value value, Value& key, uint32_t& offset, uint32_t& stride) const {
offset = 0;
for (;;) {
@@ -1672,6 +1729,8 @@ private:
uint32_t initial_arm;
};
static bool ZeroBased(uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; }
std::optional<Induction> MatchInduction(Value key, const auto& accepts_step) const {
key = key.Resolve();
const auto* phi = key.TryInstruction();
@@ -1733,8 +1792,7 @@ private:
}
uint32_t PostTestLoopMaximum(Value key) const {
const auto induction = MatchInduction(key,
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; });
const auto induction = MatchInduction(key, ZeroBased);
if (!induction) return UINT32_MAX;
const auto [phi, step, initial_arm] = *induction;
const auto* incoming = phi->PhiBlock(initial_arm ^ 1u);
@@ -1767,8 +1825,7 @@ private:
const Inst* BoundedLoop(Value key, const Block* use, const auto& accepts_bound,
LaneQuantifier lanes = LaneQuantifier::Any) const {
return BoundedInduction(key, use,
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; },
return BoundedInduction(key, use, ZeroBased,
[&](const Inst& compare, uint32_t initial) {
return (compare.GetOpcode() == ValueOpcode::SLessThan32 ||
compare.GetOpcode() == ValueOpcode::ULessThan32) &&
@@ -1793,6 +1850,29 @@ private:
});
}
// A zero-based key that only per-lane data bounds takes the count all uniform zero-based
// loops in the program share, assuming each lane's count stays within it.
bool ProgramLoopCount(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
if (!MatchInduction(key, ZeroBased) || BoundedLoop(key, nullptr) != nullptr) return false;
const Inst* count = nullptr;
for (auto* block: m_program.blocks) {
for (auto& phi: *block) {
if (phi.GetOpcode() != ValueOpcode::Phi) break;
const auto* compare = BoundedLoop(Value(&phi), nullptr);
if (compare == nullptr) continue;
if (count != nullptr && !EquivalentValue(m_program, compare->Arg(1), count->Arg(1)))
return false;
count = compare;
}
}
if (count == nullptr) return false;
std::printf("Warning: shader 0x%016llx bounds a per-lane table loop by its uniform loops' "
"count\n", static_cast<unsigned long long>(m_program.shader_hash));
indirect.key_count = count->Arg(1);
indirect.signed_count = count->GetOpcode() == ValueOpcode::SLessThan32;
return true;
}
Value ActiveValue(Value value, Value guard) const {
value = value.Resolve();
while (const auto* select = value.TryInstruction()) {
@@ -1829,6 +1909,10 @@ private:
}
bool Dominates(const Block* initial, const Block* use, Value witness = {}) const {
// The proven local-zero invocation fails every all-lane bypass condition that excludes it,
// such as an all-inactive or an all-nonzero bypass.
const auto* negated = SimplifyGuard(witness).TryInstruction();
const bool nonzero = negated != nullptr && negated->GetOpcode() == ValueOpcode::LogicalNot;
std::vector<const Block*> pending {use};
for (size_t index = 0; index < pending.size(); ++index) {
const auto* current = pending[index];
@@ -1837,9 +1921,9 @@ private:
for (const auto* previous: current->ImmPredecessors()) {
if (!witness.IsEmpty()) {
const auto edge = ConditionalEdge(previous, current);
// The proven local-zero invocation makes an all-inactive bypass impossible.
if (edge && edge->lanes == LaneQuantifier::All && !edge->positive &&
Implies(witness, edge->condition)) continue;
if (edge && edge->lanes == LaneQuantifier::All &&
(edge->positive ? nonzero && Implies(edge->condition, negated->Arg(0))
: Implies(witness, edge->condition))) continue;
}
if (std::ranges::find(pending, previous) == pending.end()) pending.push_back(previous);
}
@@ -1948,7 +2032,10 @@ private:
bool LocalZeroWitness(Value guard) const {
const auto* equal = SimplifyGuard(guard).TryInstruction();
if (equal == nullptr || equal->GetOpcode() != ValueOpcode::IEqual32) return false;
const bool negated = equal != nullptr && equal->GetOpcode() == ValueOpcode::LogicalNot;
if (negated) equal = SimplifyGuard(equal->Arg(0)).TryInstruction();
const auto expected = negated ? ValueOpcode::INotEqual32 : ValueOpcode::IEqual32;
if (equal == nullptr || equal->GetOpcode() != expected) return false;
for (uint32_t arg = 0; arg < 2u; ++arg) {
if (equal->Arg(arg).Resolve() != Value(0u)) continue;
Value induction;
@@ -1982,6 +2069,10 @@ private:
EquivalentValue(m_program, test->Arg(0), value)) return test->Arg(1);
if ((positive ? op == ValueOpcode::UGreaterThan32 : op == ValueOpcode::ULessThanEqual32) &&
EquivalentValue(m_program, test->Arg(1), value)) return test->Arg(0);
uint32_t limit = 0;
if ((positive ? op == ValueOpcode::ULessThanEqual32 : op == ValueOpcode::UGreaterThan32) &&
ImmediateU32(test->Arg(1), limit) && limit != UINT32_MAX &&
EquivalentValue(m_program, test->Arg(0), value)) return Value(limit + 1u);
const auto* scan = value.Resolve().TryInstruction();
if (finite_bitscan && scan != nullptr && scan->GetOpcode() == ValueOpcode::FindILsb32 &&
(positive ? op == ValueOpcode::INotEqual32 : op == ValueOpcode::IEqual32)) {
@@ -2056,7 +2147,7 @@ private:
const auto* read = ActiveValue(value, active).TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
m_program.stage != ShaderType::Compute ||
read->Arg(1).Resolve() != Value(true)) return {};
!Implies(active, read->Arg(1))) return {};
uint64_t group_size = 1u;
for (const auto dimension: m_input.compute->threads_num) {
if (dimension == 0u || dimension > m_input.compute->wave_size) return {};
@@ -2087,10 +2178,16 @@ private:
const auto* limit = BoundedLoop(read_offset.index, read->Parent(),
[](const Inst& compare, uint32_t) { return compare.GetOpcode() == ValueOpcode::ULessThan32; });
if (limit == nullptr) return {};
auto counter_active = active;
const auto* counter_read = ActiveValue(limit->Arg(1), active).TryInstruction();
// Every lane reads the same counter word, so a selected lane carries it.
if (counter_read != nullptr && counter_read->GetOpcode() == ValueOpcode::ReadFirstLane) {
counter_active = counter_read->Arg(1);
counter_read = ActiveValue(counter_read->Arg(0), counter_active).TryInstruction();
}
uint32_t consumed_counter = 0;
if (counter_read == nullptr || counter_read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
counter_read->Arg(1).Resolve() != Value(true) ||
!Implies(counter_active, counter_read->Arg(1)) ||
!constant_address(*counter_read, consumed_counter)) return {};
struct Initializer { const Inst* inst; uint32_t address; uint32_t words; };
@@ -2304,25 +2401,23 @@ private:
((offset | stride | memory->offset) & 3u) == 0u &&
uint64_t {offset} + memory->offset <= UINT32_MAX;
if (affine) {
workgroup_axis = WorkgroupAxis(index);
if (workgroup_axis == UINT32_MAX) {
const auto* sum = index.Resolve().TryInstruction();
if (sum != nullptr && sum->GetOpcode() == ValueOpcode::IAdd32) {
for (uint32_t arm = 0; arm < 2u; ++arm) {
const auto axis = WorkgroupAxis(sum->Arg(arm));
const auto base = sum->Arg(arm ^ 1u);
if (axis == UINT32_MAX ||
!ValidateRuntimeValue(m_program, base, RuntimeValueType::Integer)) continue;
workgroup_axis = axis;
first = base;
break;
}
}
}
workgroup_axis = WorkgroupAxis(index, &first);
const auto* bound = BoundedLoop(index, read->Parent());
if (bound != nullptr && bound->GetOpcode() == ValueOpcode::ULessThan32) {
count = bound->Arg(1);
} else if (workgroup_axis == UINT32_MAX && !require_bound) {
// Other bounded indices, such as bit scans, read the records up to their maximum;
// a bit scan of a nonzero mask load reads only the ranks its words set.
const auto maximum = SelectorMaximum(index);
if (maximum == 31u) selector.ranks = BitScanRanks(index);
if (maximum < MaxIndirectDescriptorProbes) count = Value(maximum + 1u);
}
// A record selected from a shared append list is below the list's bound.
const auto* lane = index.Resolve().TryInstruction();
if (count.IsEmpty() && lane != nullptr &&
lane->GetOpcode() == ValueOpcode::ReadFirstLane &&
HasActiveLane(lane->Arg(1), lane->Parent()))
count = SharedLoadBound(lane->Arg(0), lane->Arg(1));
}
// Raw pointers have no V# bounds for enumerating an unbounded selector domain.
if (memory->kind == ResourceKind::ScalarAddress && count.IsEmpty() &&
@@ -2498,7 +2593,8 @@ private:
// Each equation is an inductive subset obligation. Phis require every incoming
// value; AND needs either operand. Grounding rejects cycles without an initial
// value, then the greatest fixed point checks every initial and carried edge.
bool BitsetMaskSubset(Value mask, Value active, Value word, const Inst& read) const {
bool BitsetMaskSubset(Value mask, Value active, Value word,
std::span<const Inst* const> reads) const {
struct State {
Value value;
Value guard;
@@ -2510,7 +2606,10 @@ private:
bool all = true;
std::vector<size_t> terms;
};
const auto load_guard = read.Arg(4).Resolve();
// A scalar load reads one word without EXEC; four-word loads take their guard.
const bool scalar = reads[0]->GetOpcode() == ValueOpcode::ReadConstBuffer;
const auto load_guard = scalar ? Value(true) : reads[0]->Arg(4).Resolve();
const auto words = static_cast<uint32_t>(reads.size()) * (scalar ? 1u : 4u);
std::vector<State> states(2);
std::vector<Equation> equations(2);
const auto add = [&](State state) {
@@ -2521,9 +2620,9 @@ private:
equations.emplace_back();
return states.size() - 1u;
};
std::array<size_t, 5> roots {add({.value = active})};
for (uint32_t component = 0; component < 4u; ++component)
roots[component + 1u] = add({mask, active, component, component});
std::vector<size_t> roots {add({.value = active})};
for (uint32_t component = 0; component < words; ++component)
roots.push_back(add({mask, active, component, component}));
for (size_t index = 2; index < states.size(); ++index) {
const auto state = states[index];
const auto* inst = state.value.TryInstruction();
@@ -2573,11 +2672,14 @@ private:
switch (inst->GetOpcode()) {
case ValueOpcode::CompositeExtractU32x4: {
uint32_t component;
if (inst->Arg(0).Resolve().TryInstruction() == &read &&
ImmediateU32(inst->Arg(1), component) && component == state.component)
if (inst->Arg(0).Resolve().TryInstruction() == reads[state.component / 4u] &&
ImmediateU32(inst->Arg(1), component) && component == state.component % 4u)
equation.terms = {1u};
break;
}
case ValueOpcode::ReadConstBuffer:
if (scalar && inst == reads[state.component]) equation.terms = {1u};
break;
case ValueOpcode::Phi:
// The predecessor may be an earlier outer iteration. Establish
// this register's invariant without the current word or EXEC.
@@ -2598,17 +2700,17 @@ private:
const auto* masked = key.TryInstruction();
uint32_t bound = 0;
if (masked != nullptr && masked->GetOpcode() == ValueOpcode::BitwiseAnd32 &&
DescendingLoop(word, masked->Parent(), bound) && bound == 3u) {
DescendingLoop(word, masked->Parent(), bound) && bound == words - 1u) {
for (uint32_t side = 0; side < 2u; ++side) {
uint32_t bits;
if (ImmediateU32(masked->Arg(side), bits) && (bits & 3u) == 3u)
if (ImmediateU32(masked->Arg(side), bits) && (bits & bound) == bound)
key = masked->Arg(side ^ 1u).Resolve();
}
}
return EquivalentValue(m_program, key, word);
});
uint32_t literal;
if (ImmediateU32(equality, literal) && literal < 4u) selected_word = literal;
if (ImmediateU32(equality, literal) && literal < words) selected_word = literal;
equation.terms.push_back(term(inst->Arg(1), inst->Arg(0), selected_word));
}
if (!selected || !*selected)
@@ -2646,7 +2748,53 @@ private:
return std::ranges::all_of(roots, [&](size_t root) { return valid[root]; });
}
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds,
// The mask words whose set bits are every record a bit scan of index selects: the scanned
// value keeps only bits of one scalar load, which reads a word its address can reach.
std::optional<DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks>
BitScanRanks(Value index) {
const auto* scan = index.Resolve().TryInstruction();
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindILsb32) return {};
const auto* masked = scan->Arg(0).Resolve().TryInstruction();
if (masked == nullptr) return {};
std::vector<const Inst*> values {masked};
const Inst* load = nullptr;
for (size_t i = 0; i < values.size() && load == nullptr; ++i) {
const auto* value = values[i];
if (value->GetOpcode() == ValueOpcode::ReadConstBuffer) load = value;
if (value->GetOpcode() != ValueOpcode::Phi &&
value->GetOpcode() != ValueOpcode::BitwiseAnd32) continue;
for (size_t arg = 0; arg < value->NumArgs(); ++arg) {
const auto* next = value->Arg(arg).Resolve().TryInstruction();
if (next != nullptr && std::ranges::find(values, next) == values.end())
values.push_back(next);
}
}
uint32_t memory_index = 0;
const auto* memory = load != nullptr ? ScalarReadMemory(*load, memory_index) : nullptr;
if (memory == nullptr || memory->offset > INT32_MAX ||
!MemoryIndexBelongsTo(memory_index, *load) ||
!BitsetMaskSubset(scan->Arg(0), Value(true), Value(0u), std::array {load})) return {};
auto address = load->Arg(1);
auto active = Value(true);
const auto* lane = address.Resolve().TryInstruction();
if (lane != nullptr && lane->GetOpcode() == ValueOpcode::ReadFirstLane) {
if (!HasActiveLane(lane->Arg(1), lane->Parent())) return {};
active = lane->Arg(1);
address = lane->Arg(0);
}
// A left shift keeps the address a multiple of its scale; other addresses read any word.
const auto* shift = ActiveValue(address, active).TryInstruction();
uint32_t bits = 0;
if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftLeftLogical32 ||
!ImmediateU32(shift->Arg(1), bits) || bits >= 32u) bits = 0;
DescriptorSource source;
if (!MakeRuntimeTableSource(*load, source)) return {};
return DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks {
.source = InternSource(source), .stride = std::max(1u << bits, 4u),
.offset = memory->offset & ~3u};
}
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds, uint32_t words,
DescriptorSource::IndirectDescriptor& indirect) {
const auto selected_value = [&](Value value) {
for (;;) {
@@ -2670,7 +2818,7 @@ private:
}
const auto* word_inst = word.TryInstruction();
if (word_inst == nullptr || std::ranges::none_of(bounds, [&](const auto& bound) {
return bound.first == word_inst && bound.second == 3u;
return bound.first == word_inst && bound.second == words - 1u;
})) return false;
// FFBH's leading-zero count is inverted again to recover the set-bit rank.
for (uint32_t inverse = 0; inverse < 2u; ++inverse) {
@@ -2682,21 +2830,34 @@ private:
const auto* scan = bit.TryInstruction();
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindUMsb32 ||
!ImpliesNonzero(active, scan->Arg(0))) return false;
const auto bitmap_load = [&](const Inst& read, uint32_t offset) {
const auto index = read.Flags<MemoryFlags>().index;
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(read.Parent()) ||
index >= m_program.memory_info.size()) return false;
const auto& memory = m_program.memory_info[index];
return memory.kind == ResourceKind::Buffer && !memory.typed && !memory.formatted &&
memory.idxen && !memory.offen && memory.offset == offset &&
memory.data_bits == 32u && memory.data_dwords == 4u &&
read.Arg(2).Resolve() == Value(0u) && read.Arg(3).Resolve() == Value(0u);
};
for (const auto* block: m_program.blocks) {
for (const auto& read: *block) {
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(block)) continue;
const auto index = read.Flags<MemoryFlags>().index;
if (index >= m_program.memory_info.size()) continue;
const auto& memory = m_program.memory_info[index];
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset != 0u || memory.data_bits != 32u ||
memory.data_dwords != 4u || read.Arg(2).Resolve() != Value(0u) ||
read.Arg(3).Resolve() != Value(0u) ||
!BitsetMaskSubset(scan->Arg(0), active, word, read)) continue;
if (!bitmap_load(read, 0u)) continue;
// An eight-word record continues in a second four-word load of the same buffer and EXEC.
std::array<const Inst*, 2> reads {&read};
if (words == 8u)
for (const auto* other: m_program.blocks)
for (const auto& load: *other)
if (reads[1] == nullptr && bitmap_load(load, 16u) &&
EquivalentValue(m_program, load.Arg(0), read.Arg(0)) &&
EquivalentValue(m_program, load.Arg(4), read.Arg(4))) reads[1] = &load;
const auto parts = std::span(reads).first(words / 4u);
DescriptorSource source;
if (!MakeRuntimeTableSource(read, source)) continue;
if (std::ranges::find(parts, nullptr) != parts.end() ||
!BitsetMaskSubset(scan->Arg(0), active, word, parts) ||
!MakeRuntimeTableSource(read, source)) continue;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead {
.source = InternSource(source), .stride = 16u, .offset = 0u, .bitset = true});
.source = InternSource(source), .stride = words * 4u, .offset = 0u, .bitset = true});
return true;
}
}
@@ -2726,7 +2887,8 @@ private:
first.SetArg(1, Value(maximum));
indirect.selector_first = Value(&first);
indirect.key_count = Value(maximum + 1u);
if (maximum == 127u) MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, indirect);
if (maximum == 127u || maximum == 255u)
MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, (maximum + 1u) / 32u, indirect);
return true;
}
@@ -2993,6 +3155,47 @@ private:
return true;
}
// The EXEC predicate of the image access through use; empty for any other use.
Value AccessPredicate(const Use& use, bool sampler) const {
const auto& access = *use.user;
if (use.operand != (sampler ? 1u : 0u) ||
ImageOpcodeInfoOf(access.GetOpcode()).access == ImageAccess::None) return {};
if (sampler && (access.GetOpcode() != ValueOpcode::ImageSampleRaw ||
(m_program.memory_info[access.Flags<MemoryFlags>().index].image_sample_flags &
Decoder::ImageSampleFlagAdjust) != 0u)) return {};
const auto mask = access.Arg(access.NumArgs() - 1u);
return mask.GetType() == Type::U1 ? mask : Value {};
}
// A key no bound covers takes its maximum from a signed clamp to a constant, on the key or on
// the key plus a constant in an earlier-tracked sibling read of the same table. Negative keys
// and keys past that maximum read the first row.
uint32_t ClampedTableMaximum(Value key, const DescriptorSource& table,
const DescriptorSource::IndirectDescriptor& indirect) const {
uint32_t row = 0, offset = 0;
const auto clamped = [&](Value value) {
const auto* min = value.Resolve().TryInstruction();
if (min != nullptr && min->GetOpcode() == ValueOpcode::SMin32)
for (uint32_t arg = 0; arg < 2u; ++arg)
if (ImmediateU32(min->Arg(arg), row)) return min->Arg(arg ^ 1u);
return Value {};
};
if (!clamped(key).IsEmpty()) return row;
for (const auto& sibling: m_indirect_descriptors) {
const auto& read = *m_sources[sibling.source].indirect_descriptor;
const auto* add = clamped(sibling.key).Resolve().TryInstruction();
if (add == nullptr || add->GetOpcode() != ValueOpcode::IAdd32 ||
m_sources[read.table_source] != table ||
read.table_offset != indirect.table_offset ||
read.table_stride != indirect.table_stride ||
read.table_immediate != indirect.table_immediate) continue;
for (uint32_t arg = 0; arg < 2u; ++arg)
if (ImmediateU32(add->Arg(arg), offset) &&
EquivalentValue(m_program, add->Arg(arg ^ 1u), key)) return row;
}
return UINT32_MAX;
}
bool TryMakeTableDescriptor(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan) {
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
@@ -3019,6 +3222,7 @@ private:
} else if (!plan.table_indexed &&
MatchScalarMaterialKey(key, indirect, material_source,
!sampler && table_source.dword_count == 4u, sampler)) {
indirect.selector->excluded = ExcludedKeyOnEntry(key, handle.Parent());
// Keep live descriptor payloads, but project descriptor-only SMEM onto
// the GPU key using the same plan as other indirect image reads.
plan.retain_reads |= table_source.dword_count == 4u;
@@ -3080,36 +3284,42 @@ private:
if (indirect.key_count.IsEmpty() &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
!MatchReversedImageRange(key, indirect)) {
const auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) return false;
indirect.key_count = Value(maximum + 1u);
!MatchReversedImageRange(key, indirect) && !MatchWorkgroupKey(key, indirect)) {
auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) {
// Each image access bounds the key on the lanes it executes.
maximum = 0u;
for (const auto& use: handle.Uses()) {
const auto active = AccessPredicate(use, false);
if (active.IsEmpty()) {
maximum = UINT32_MAX;
break;
}
maximum = std::max(maximum, SelectorMaximum(key, {}, 0, active));
}
}
if (maximum >= MaxIndirectDescriptorProbes)
maximum = ClampedTableMaximum(key, table_source, indirect);
if (maximum < MaxIndirectDescriptorProbes)
indirect.key_count = Value(maximum + 1u);
else if (!ProgramLoopCount(key, indirect))
return false;
}
if ((table_offset & 3u) != 0u) return false;
} else {
// Keep the GPU selector and descriptor reads, and preserve the proven scalar
// offset domain within the bounded image table.
if (sampler || plan.split_offsets) return false;
const auto active = [&](const Use& use, bool sampler) -> Value {
const auto& access = *use.user;
if (use.operand != (sampler ? 1u : 0u) ||
ImageOpcodeInfoOf(access.GetOpcode()).access == ImageAccess::None) return {};
if (sampler && (access.GetOpcode() != ValueOpcode::ImageSampleRaw ||
(m_program.memory_info[access.Flags<MemoryFlags>().index].image_sample_flags &
Decoder::ImageSampleFlagAdjust) != 0u)) return {};
const auto mask = access.Arg(access.NumArgs() - 1u);
return mask.GetType() == Type::U1 ? mask : Value {};
};
bool shared_selector = false;
if (!plan.table_indexed) {
for (const auto& owner: m_indirect_descriptors) {
const auto& selected = *m_sources[owner.source].indirect_descriptor;
if (!selected.selector || !EquivalentValue(m_program, key, owner.key) ||
!std::ranges::all_of(handle.Uses(), [&](const Use& target) {
const auto target_active = active(target, false);
const auto target_active = AccessPredicate(target, false);
return !target_active.IsEmpty() &&
std::ranges::any_of(owner.handle->Uses(), [&](const Use& witness) {
const auto owner_active = active(witness,
const auto owner_active = AccessPredicate(witness,
owner.handle->GetOpcode() == ValueOpcode::GetSamplerResource);
return !owner_active.IsEmpty() && AvailableBefore(*witness.user, *target.user) &&
Implies(target_active, owner_active);
@@ -3430,6 +3640,42 @@ private:
return result;
}
// Bounds of an F32 value on the lanes where guard holds; NaN when unknown.
std::pair<float, float> FloatRange(Value value, Value guard, uint32_t depth = 0) const {
constexpr std::pair unknown {NAN, NAN};
value = ActiveValue(value, guard);
if (value.IsImmediate() && value.GetType() == Type::F32)
return {value.F32Value(), value.F32Value()};
const auto* inst = value.TryInstruction();
// Each multiply-add evaluates three operands; keep the search shallow.
if (depth > 12u || inst == nullptr) return unknown;
const auto range = [&](uint32_t arg) {
return FloatRange(inst->Arg(arg), guard, depth + 1u);
};
switch (inst->GetOpcode()) {
case ValueOpcode::BitCastF32U32:
case ValueOpcode::BitCastU32F32: return range(0);
// Clamping maps NaN to zero.
case ValueOpcode::FPSaturate32: return {0.0f, 1.0f};
case ValueOpcode::FPFloor32: {
const auto [low, high] = range(0);
return {std::floor(low), std::floor(high)};
}
case ValueOpcode::FPMad32: {
// The rounded multiply and add are monotonic, so the bounds lie at the corners.
const auto a = range(0), b = range(1), c = range(2);
if (!std::isfinite(a.first) || !std::isfinite(a.second) ||
!std::isfinite(b.first) || !std::isfinite(b.second)) return unknown;
const auto [low, high] = std::minmax({a.first * b.first, a.first * b.second,
a.second * b.first, a.second * b.second});
const std::pair bounds {low + c.first, high + c.second};
if (!std::isfinite(bounds.first) || !std::isfinite(bounds.second)) return unknown;
return bounds;
}
default: return unknown;
}
}
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
Value guard = {}) const {
if (depth > 32u) return UINT32_MAX;
@@ -3445,9 +3691,13 @@ private:
switch (inst->GetOpcode()) {
case ValueOpcode::ReadFirstLane:
return SelectedSelectorDomain(*inst, guard).maximum;
case ValueOpcode::ReadLane:
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
return PossibleU32Bits(inst->Arg(0));
case ValueOpcode::ReadLane: {
// ReadLane ignores EXEC; only context-free bounds on every source lane apply,
// unless guard proves a waterfall key equal to the executing lanes' local value.
const auto local = EqualLocalKey(guard, value);
return local.IsEmpty() ? PossibleU32Bits(inst->Arg(0))
: SelectorMaximum(local, bounds, depth + 1u, guard);
}
case ValueOpcode::Phi: {
const auto initial = InitialCandidateMask(value);
if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard);
@@ -3474,6 +3724,18 @@ private:
NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
case ValueOpcode::UMin32:
case ValueOpcode::BitwiseAnd32: return std::min(maximum(0), maximum(1));
case ValueOpcode::SMin32: {
// Signed and unsigned minimums agree on nonnegative operands.
const auto left = maximum(0), right = maximum(1);
if (std::max(left, right) <= INT32_MAX) return std::min(left, right);
break;
}
case ValueOpcode::ConvertS32F32: {
// The conversion truncates toward zero.
const auto [low, high] = FloatRange(inst->Arg(0), guard);
if (low >= 0.0f && high < 0x1p31f) return static_cast<uint32_t>(high);
break;
}
case ValueOpcode::SelectU32:
if (!guard.IsEmpty() && Implies(guard, inst->Arg(0))) return maximum(1);
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u, guard)
@@ -175,6 +175,8 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) {
case StageInputKind::GlobalInvocationId:
AddInput(info, kind, 0, 3, "gl_GlobalInvocationID");
break;
case StageInputKind::BaseVertex: AddInput(info, kind, 0, 1, "gl_BaseVertex"); break;
case StageInputKind::BaseInstance: AddInput(info, kind, 0, 1, "gl_BaseInstance"); break;
case StageInputKind::PackedAncillary:
case StageInputKind::Parameter: break;
}
@@ -366,7 +368,6 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
info.function_lds |= kind == ResourceKind::Lds &&
program.stage != ShaderType::Compute && program.stage != ShaderType::Mesh;
if (shared_access == SharedAccess::Append || shared_access == SharedAccess::Consume) {
info.subgroup_ballot = true;
info.subgroup_shuffle = true;
info.subgroup_local_invocation_id = true;
}
@@ -445,6 +446,8 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
case StageInputKind::InstanceIndex:
case StageInputKind::FrontFacing:
case StageInputKind::LocalInvocationIndex:
case StageInputKind::BaseVertex:
case StageInputKind::BaseInstance:
if (component != 0u) {
return Fail("typed scalar builtin component is out of range");
}
@@ -517,7 +520,9 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
inst.Flags<CFG::BranchCondition>() != CFG::BranchCondition::ScalarInstruction;
break;
case ValueOpcode::Ballot: info.subgroup_ballot = true; break;
case ValueOpcode::SubgroupUMinU32: info.subgroup_arithmetic = true; break;
case ValueOpcode::LiveMask: info.subgroup_ballot = info.helper_invocation = true; break;
case ValueOpcode::SubgroupUMinU32:
case ValueOpcode::SubgroupUMaxU32: info.subgroup_arithmetic = true; break;
case ValueOpcode::DppMoveU32:
case ValueOpcode::ReadFirstLane:
case ValueOpcode::ReadLane: {
@@ -576,7 +581,7 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
info.has_bitwise_xor = info.uses_lds = info.uses_gds = info.uses_flattened_srt =
info.uses_dispatch_threads = info.uses_swizzle = false;
info.bvh = info.subgroup_ballot = info.subgroup_vote = info.subgroup_barrier =
info.subgroup_shuffle = info.subgroup_arithmetic =
info.subgroup_shuffle = info.subgroup_arithmetic = info.helper_invocation =
info.subgroup_local_invocation_id = info.compute_derivatives = info.image_gather_extended =
info.function_lds = info.function_scratch = info.pixel_valid_mask = info.buffer_int64_atomics =
info.buffer_u8 = info.buffer_u16 = info.shared_int64_atomics = info.coherent_buffers = info.float64 = false;
File diff suppressed because it is too large. Load diff
@@ -8,8 +8,6 @@
namespace Libs::Graphics::ShaderRecompiler::IR {
class Value;
using SrtMemoryReader = bool (*)(void* userdata, uint64_t address, std::span<uint32_t> values);
// Image descriptors at base + first + k * step, k < count, within size bytes that a sampled
@@ -42,7 +40,6 @@ protected:
struct SrtRuntime {
std::span<const uint32_t> user_data;
uint64_t shader_base = 0;
SrtMemoryReader read_memory = nullptr;
void* userdata = nullptr;
SrtMemoryReader read_specialization_memory = nullptr;
std::span<const uint32_t> workgroup_counts;
@@ -57,20 +54,12 @@ public:
SrtRuntime ObservedRuntime();
private:
static bool ReadStrict(void* userdata, uint64_t address, std::span<uint32_t> values);
static bool ReadOrdinary(void* userdata, uint64_t address, std::span<uint32_t> values);
static bool Read(void* userdata, uint64_t address, std::span<uint32_t> values);
SrtRuntime m_source;
std::vector<std::pair<uint64_t, uint64_t>>& m_ranges;
};
// Retained reads no longer need the guest instruction PC. A clean read evaluates
// its address and bounds through the strict reader as well as the final DWORD.
struct SrtReadFlags {
uint32_t index = 0;
uint32_t clean = 0;
};
enum class RuntimeValueType { Any, Integer, ImmutableInteger };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
@@ -78,9 +67,21 @@ bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
// Uniform across a compute wave, including values derived from its workgroup ID.
// This does not imply that the host can evaluate the value.
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value);
void CompileSrtWalker(ResourcePlan& program);
// Uses the strict reader for values that affect shader specialization.
SrtRuntime CleanRuntime(SrtRuntime runtime);
// Compiles the plan's x86 walker. Without values it walks the plan's resources; otherwise it
// only evaluates the values, with every block reachable.
void CompileSrtWalker(ResourcePlan& plan, std::span<const Value> values = {});
// Runs the walker. flat receives the scalar reads of reachable blocks and reads, when non-null,
// one DWORD range per guest load. False when a reachable read is unknown.
bool WalkSrt(const ResourcePlan& plan, const SrtRuntime& runtime, std::vector<uint32_t>& flat,
std::vector<std::pair<uint64_t, uint64_t>>* reads);
// Results of the last walk; false when unknown.
bool WalkedValue(const ResourcePlan& plan, Value value, uint32_t& result);
bool WalkedDescriptor(const ResourcePlan& plan, uint32_t source, DescriptorValue& result);
bool WalkedActive(const ResourcePlan& plan, uint32_t source);
bool WalkedImageActive(const ResourcePlan& plan, uint32_t image);
// Length of the guest load at rip if it belongs to the walker running on this thread, else 0.
// The exception handler completes such a load as a zero read.
uint32_t SrtWalkerLoadLength(uint64_t rip);
// Nonnegative affine coefficients: element zero is constant, remaining elements are
// caller-selected leaves. Runtime range proofs must additionally exclude U32 wrap.
@@ -124,43 +125,6 @@ std::optional<std::array<uint64_t, Size>> AffineIndex(
return left;
}
// One memoized evaluation session shared by the entire shader resource refresh.
class SrtWalker {
public:
SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
SrtWalker* clean_evaluator = nullptr);
~SrtWalker();
SrtWalker(const SrtWalker&) = delete;
SrtWalker& operator=(const SrtWalker&) = delete;
bool Evaluate(Value value, uint32_t& result);
bool EvaluateDescriptor(uint32_t source, DescriptorValue& result);
// Refreshes reachable scalar reads and active descriptor sources in one walk.
bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
private:
SrtWalker(SrtWalker& parent, Value active_mask, bool clean = false,
SrtWalker* clean_evaluator = nullptr);
ResourcePlan::EvaluationContext& AcquireContext();
void ResetUnknowns();
static float Float32(uint64_t bits);
bool EvaluateWide(Value value, uint64_t& result);
bool Arg(const Inst& inst, size_t index, uint64_t& result);
bool EvaluatePhi(const Inst& inst, uint64_t& result);
bool EvaluateExtract(const Inst& inst, uint64_t& result);
bool EvaluateRawRead(const Inst& inst, uint64_t& result);
bool EvaluateInst(const Inst& inst, uint64_t& result);
const ResourcePlan& m_program;
SrtRuntime m_runtime;
SrtWalker* m_clean_evaluator = nullptr;
Value m_active_mask;
const std::vector<uint8_t>* m_reachable_blocks = nullptr;
const SrtWalker* m_session;
uint8_t m_read_policy;
ResourcePlan::EvaluationContext& m_context;
};
} // namespace Libs::Graphics::ShaderRecompiler::IR
#endif /* EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SRTWALKER_H_ */
@@ -348,14 +348,20 @@ void VisitInstruction(Pass& pass, Block* block, Inst& inst) {
} // namespace
void RewriteToSsa(const BlockList& blocks) {
Pass pass;
Pass pass;
std::unordered_map<Block*, size_t> filled_predecessors;
for (auto* block: blocks) {
if (block->ImmPredecessors().empty()) {
pass.Seal(block);
}
for (auto& inst: *block) {
VisitInstruction(pass, block, inst);
}
}
for (auto* block: blocks) {
pass.Seal(block);
for (auto* successor: block->ImmSuccessors()) {
if (++filled_predecessors[successor] == successor->ImmPredecessors().size()) {
pass.Seal(successor);
}
}
}
}
+59 -69
View File
@@ -18,45 +18,9 @@ using ShaderRecompiler::Spirv::Builder;
constexpr uint32_t SpirvVersion15 = 0x00010500u;
struct Parameter {
uint32_t input_location = 0;
uint32_t output_location = 0;
bool flat = false;
};
std::vector<Parameter> GetParameters(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info) {
if (pixel_info == nullptr) {
return {};
}
EXIT_IF(pixel_info->input_num > ShaderVertexInputInfo::RES_MAX);
EXIT_IF(pixel_info->stage.program == nullptr);
std::vector<uint32_t> active_inputs;
for (const auto& input: pixel_info->stage.program->info.inputs) {
if (input.kind == ShaderRecompiler::IR::StageInputKind::Parameter) {
active_inputs.push_back(input.location);
}
}
std::vector<Parameter> parameters;
std::array<bool, 32> output_locations {};
for (const auto input: active_inputs) {
const auto input_location = ShaderPixelParameterMappedLocation(*pixel_info, input);
const auto output_location = ShaderPixelParameterLocation(*pixel_info, active_inputs, input);
if ((vertex_info.stage.program->param_export_mask & (1u << input_location)) != 0 &&
!output_locations[output_location]) {
parameters.push_back({input_location, output_location,
ShaderPixelParameterIsFlat(*pixel_info, input)});
output_locations[output_location] = true;
}
}
return parameters;
}
class RectListEmitter {
public:
RectListEmitter(const std::vector<Parameter>& parameters_, spv::ExecutionModel model)
RectListEmitter(const std::vector<RectListParameter>& parameters_, spv::ExecutionModel model)
: parameters(parameters_) {
builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450);
@@ -315,42 +279,68 @@ private:
Result(spv::OpFAdd, vec4_float_type, p1, p2));
}
Builder builder {SpirvVersion15};
const std::vector<Parameter>& parameters;
std::vector<uint32_t> interfaces;
std::vector<uint32_t> inputs;
std::vector<uint32_t> outputs;
uint32_t main = 0;
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t uint_type = 0;
uint32_t int_type = 0;
uint32_t float_type = 0;
uint32_t vec2_bool_type = 0;
uint32_t vec2_float_type = 0;
uint32_t vec3_float_type = 0;
uint32_t vec4_float_type = 0;
uint32_t function_type = 0;
uint32_t per_vertex_type = 0;
uint32_t ptr_input_float = 0;
uint32_t ptr_input_vec4_float = 0;
uint32_t ptr_output_float = 0;
uint32_t ptr_output_vec4_float = 0;
uint32_t gl_in = 0;
uint32_t gl_out = 0;
uint32_t tess_inner = 0;
uint32_t tess_outer = 0;
uint32_t tess_coord = 0;
uint32_t invocation_id = 0;
Builder builder {SpirvVersion15};
const std::vector<RectListParameter>& parameters;
std::vector<uint32_t> interfaces;
std::vector<uint32_t> inputs;
std::vector<uint32_t> outputs;
uint32_t main = 0;
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t uint_type = 0;
uint32_t int_type = 0;
uint32_t float_type = 0;
uint32_t vec2_bool_type = 0;
uint32_t vec2_float_type = 0;
uint32_t vec3_float_type = 0;
uint32_t vec4_float_type = 0;
uint32_t function_type = 0;
uint32_t per_vertex_type = 0;
uint32_t ptr_input_float = 0;
uint32_t ptr_input_vec4_float = 0;
uint32_t ptr_output_float = 0;
uint32_t ptr_output_vec4_float = 0;
uint32_t gl_in = 0;
uint32_t gl_out = 0;
uint32_t tess_inner = 0;
uint32_t tess_outer = 0;
uint32_t tess_coord = 0;
uint32_t invocation_id = 0;
};
} // namespace
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info) {
const auto parameters = GetParameters(vertex_info, pixel_info);
RectListEmitter control(parameters, spv::ExecutionModelTessellationControl);
RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation);
std::vector<RectListParameter> RectListParameters(
const ShaderPixelInputInfo& pixel_info, const ShaderRecompiler::IR::CompiledShaderInfo& program) {
EXIT_IF(pixel_info.input_num > ShaderVertexInputInfo::RES_MAX);
std::vector<uint32_t> active_inputs;
for (const auto& input: program.info.inputs) {
if (input.kind == ShaderRecompiler::IR::StageInputKind::Parameter) {
active_inputs.push_back(input.location);
}
}
std::vector<RectListParameter> parameters;
for (const auto input: active_inputs) {
parameters.push_back({ShaderPixelParameterMappedLocation(pixel_info, input),
ShaderPixelParameterLocation(pixel_info, active_inputs, input),
ShaderPixelParameterIsFlat(pixel_info, input)});
}
return parameters;
}
RectListShaders BuildRectListShaders(std::span<const RectListParameter> parameters,
uint32_t param_export_mask) {
std::vector<RectListParameter> exported;
std::array<bool, 32> output_locations {};
for (const auto& parameter: parameters) {
if ((param_export_mask & (1u << parameter.input_location)) != 0 &&
!output_locations[parameter.output_location]) {
exported.push_back(parameter);
output_locations[parameter.output_location] = true;
}
}
RectListEmitter control(exported, spv::ExecutionModelTessellationControl);
RectListEmitter evaluation(exported, spv::ExecutionModelTessellationEvaluation);
return {control.EmitControl(), evaluation.EmitEvaluation()};
}
+18 -3
View File
@@ -2,20 +2,35 @@
#define EMULATOR_SRC_GRAPHICS_SHADER_RECTLISTSHADER_H_
#include <cstdint>
#include <span>
#include <vector>
namespace Libs::Graphics {
struct ShaderPixelInputInfo;
struct ShaderVertexInputInfo;
namespace ShaderRecompiler::IR {
struct CompiledShaderInfo;
} // namespace ShaderRecompiler::IR
// A pixel parameter the generated stages forward from vertex export input_location.
struct RectListParameter {
uint32_t input_location = 0;
uint32_t output_location = 0;
bool flat = false;
};
struct RectListShaders {
std::vector<uint32_t> control;
std::vector<uint32_t> evaluation;
};
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info);
// Every parameter a rectangle-list pixel program reads.
std::vector<RectListParameter> RectListParameters(
const ShaderPixelInputInfo& pixel_info, const ShaderRecompiler::IR::CompiledShaderInfo& program);
// Forwards the parameters the vertex stage exports, one per output location.
RectListShaders BuildRectListShaders(std::span<const RectListParameter> parameters,
uint32_t param_export_mask);
} // namespace Libs::Graphics
+23 -29
View File
@@ -11,7 +11,6 @@
#include "graphics/guest_gpu/hardwareContext.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/Tessellation.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/shaderCompiler.h"
#include "graphics/shader/shaderVertexMetadata.h"
@@ -40,15 +39,19 @@
namespace Libs::Graphics {
// A mapped shader and its vertex metadata, parsed once when it is mapped.
struct ShaderMapData: ShaderMappedData {
ShaderVertexMetadata vertex;
const char* vertex_error = nullptr;
};
struct ShaderMapEntry {
ShaderMappedData data;
uint64_t hash;
ShaderMapData data;
uint64_t hash;
};
static std::unique_ptr<std::unordered_map<uint64_t, ShaderMapEntry>> g_shader_map;
static std::mutex g_shader_map_mutex;
static std::unordered_map<uint64_t, ShaderTessellationInputInfo> g_tess_layouts;
static std::mutex g_tess_layouts_mutex;
void ShaderInit() {
EXIT_IF(g_shader_map != nullptr);
@@ -64,8 +67,11 @@ void ShaderMapUserData(uint64_t addr, const ShaderMappedData& data) {
addr, data.code_size_bytes);
}
const auto hash = XXH3_64bits(reinterpret_cast<const void*>(addr), data.code_size_bytes);
ShaderMapData mapped {data};
mapped.vertex_error =
ShaderReadVertexMetadata(data, HW::UserSgprInfo::SGPRS_MAX, mapped.vertex);
std::scoped_lock lock(g_shader_map_mutex);
(*g_shader_map)[addr] = {data, hash};
(*g_shader_map)[addr] = {mapped, hash};
}
static ShaderMapEntry ShaderGetMappedData(uint64_t addr, const char* label) {
@@ -261,8 +267,8 @@ static uint32_t ShaderCalcPsSystemInputBase(const HW::ShaderRegisters& regs) {
static bool ShaderGetStaticVertexInputInfo(uint64_t shader_addr, const HW::UserSgprInfo& user_sgpr,
uint32_t user_sgpr_num, const HW::ShaderRegisters& sh,
const ShaderMappedData& data,
ShaderVertexInputInfo& info) {
const ShaderMapData& data,
ShaderVertexInputInfo& info) {
KYTY_PROFILER_FUNCTION();
info = {};
@@ -277,13 +283,12 @@ static bool ShaderGetStaticVertexInputInfo(uint64_t shader_addr, const HW::UserS
" user_sgpr_num=%u\n",
shader_addr, user_sgpr_num);
}
ShaderVertexMetadata metadata;
std::string metadata_error;
if (!ShaderReadVertexMetadata(data, HW::UserSgprInfo::SGPRS_MAX, metadata, &metadata_error)) {
if (data.vertex_error != nullptr) {
LOGF("ShaderGetVertexInputInfo(): invalid AGC metadata shader=0x%016" PRIx64 ": %s\n",
shader_addr, metadata_error.c_str());
shader_addr, data.vertex_error);
return false;
}
const auto& metadata = data.vertex;
if (metadata.vertex_attrib_reg >= 0) {
info.fetch_external = false;
@@ -377,7 +382,6 @@ static void ShaderGetStaticInputInfoPS(
}
static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
const HW::ShaderRegisters& /*sh*/,
const ShaderMappedData& data, ShaderComputeInputInfo& info) {
const bool async_compute = info.async_compute;
const bool dispatch_thread_dimensions = info.dispatch_thread_dimensions;
@@ -418,6 +422,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(info.lds_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(info.user_clip_planes);
key.push_back(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u));
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
if (info.clip_space.enabled) {
for (const float value: info.clip_space.scale) {
@@ -486,7 +491,9 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.object_primitive_id));
key.push_back(static_cast<uint32_t>(info.alpha_blend_source));
key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode));
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u));
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u) |
(uint32_t {info.target_uint_mask} << 16u) |
(uint32_t {info.target_sint_mask} << 24u));
const auto mask = info.target_shader_mask;
key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u);
for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) {
@@ -655,18 +662,6 @@ PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context&
};
EXIT_IF(tess.input_control_points == 0 || tess.input_control_points > 32 ||
tess.output_control_points == 0 || tess.output_control_points > 32);
// Reflection decodes both programs; the key also covers the VGT fields cached in tess.
const uint64_t key[] = {local_hash, control_hash,
uint64_t {sh.m_vgtLsHsConfig & 0xfff00u} << 32u | sh.m_vgtTfParam};
{
std::scoped_lock lock(g_tess_layouts_mutex);
auto [entry, inserted] = g_tess_layouts.try_emplace(XXH3_64bits(key, sizeof(key)), tess);
if (inserted) {
ShaderRecompiler::AnalyzeTessellationPrograms(params[0].code, params[1].code,
entry->second);
}
tess = entry->second;
}
for (auto& stage: input_info) {
stage.tess = tess;
}
@@ -684,10 +679,9 @@ ShaderParams PrepareProgram(
std::span<const uint32_t>(regs.ps_user_sgpr.value, regs.ps_regs.rsrc2.user_sgpr), data);
}
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, const HW::ShaderRegisters& sh,
ShaderComputeInputInfo& info) {
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, ShaderComputeInputInfo& info) {
const auto [data, hash] = ShaderGetMappedData(regs.cs_regs.data_addr, "ShaderGetInputInfoCS():");
ShaderGetStaticInputInfoCS(regs, sh, data, info);
ShaderGetStaticInputInfoCS(regs, data, info);
return GetShaderParams(
regs.cs_regs.data_addr, hash,
std::span<const uint32_t>(regs.cs_user_sgpr.value, regs.cs_regs.user_sgpr), data);
+20 -17
View File
@@ -15,17 +15,6 @@
namespace Libs::Graphics {
namespace ShaderError {
[[nodiscard]] inline bool Fail(std::string* error, std::string_view message) {
if (error != nullptr) {
error->assign(message.data(), message.size());
}
return false;
}
} // namespace ShaderError
namespace HW {
struct VertexShaderInfo;
struct PixelShaderInfo;
@@ -45,16 +34,14 @@ enum class ShaderType {
TessellationEvaluation
};
namespace ShaderRecompiler::IR {
struct CompiledShaderInfo;
} // namespace ShaderRecompiler::IR
struct ShaderModule;
struct ShaderStageRuntime {
const ShaderRecompiler::IR::CompiledShaderInfo* program = nullptr;
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
const ShaderModule* module = nullptr;
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
[[nodiscard]] explicit operator bool() const {
return program != nullptr && resources != nullptr;
return module != nullptr && resources != nullptr;
}
};
@@ -137,6 +124,18 @@ struct ShaderTessellationInputInfo {
uint32_t output_topology = 0;
};
// GS user-data slots an indirect draw packet writes its record's base vertex and first instance
// to. Like the hardware index VGPRs, the vertex shader's indices exclude both bases once either
// slot is set.
struct ShaderIndirectOffsets {
static constexpr uint8_t None = UINT8_MAX;
uint8_t vertex = None;
uint8_t instance = None;
bool operator==(const ShaderIndirectOffsets&) const = default;
};
struct ShaderVertexInputInfo {
static constexpr int RES_MAX = 32;
@@ -159,6 +158,7 @@ struct ShaderVertexInputInfo {
ShaderClipSpaceTransform clip_space;
ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess;
ShaderIndirectOffsets indirect_offsets;
bool fetch_external = false;
bool fetch_embedded = false;
};
@@ -194,6 +194,9 @@ struct ShaderPixelInputInfo {
uint32_t target_shader_mask = UINT32_MAX;
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
uint8_t target_packed_unorm_mask = 0;
// Exports to UINT and SINT targets, which take integer outputs of the same signedness.
uint8_t target_uint_mask = 0;
uint8_t target_sint_mask = 0;
// CB round mode per converted export: zero adds half an LSB, one truncates.
uint8_t target_round_mode = 0;
uint32_t scratch_size_dwords = 0;
+2 -2
View File
@@ -32,6 +32,7 @@ void BuildStageStaticKey(const ShaderComputeInputInfo& input_info, std::vector<u
ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context& context,
const HW::UserConfig& user_config, ShaderVertexInputInfo& input_info);
// Each stage's tess holds the VGT fields; the caller adds the layout reflected from the programs.
std::array<ShaderParams, 3>
PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context& context,
std::array<ShaderVertexInputInfo, 3>& input_info);
@@ -39,8 +40,7 @@ ShaderParams PrepareProgram(
const HW::PixelShaderInfo& regs, const HW::ShaderRegisters& sh,
std::span<const Prospero::ColorComponentMapping, 8> target_export_mapping,
ShaderPixelInputInfo& input_info);
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, const HW::ShaderRegisters& sh,
ShaderComputeInputInfo& input_info);
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, ShaderComputeInputInfo& input_info);
} // namespace Libs::Graphics
+13 -14
View File
@@ -5,10 +5,10 @@
namespace Libs::Graphics {
bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata, std::string* error) {
const char* ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata) {
if (data.user_data == nullptr) {
return ShaderError::Fail(error, "missing AGC user-data header");
return "missing AGC user-data header";
}
ShaderUserData user_data {};
@@ -17,8 +17,7 @@ bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sg
constexpr uint32_t DirectResourceCount =
static_cast<uint32_t>(AgcDirectResourceType::Last) + 1u;
if (user_data.direct_resource_count > DirectResourceCount) {
return ShaderError::Fail(error,
"AGC direct-resource count exceeds the known resource domain");
return "AGC direct-resource count exceeds the known resource domain";
}
std::array<uint16_t, DirectResourceCount> direct_offsets {};
@@ -26,7 +25,7 @@ bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sg
static_cast<uint64_t>(user_data.direct_resource_count) * sizeof(uint16_t);
if (direct_size != 0) {
if (user_data.direct_resource_offset == nullptr) {
return ShaderError::Fail(error, "missing AGC direct-resource offsets");
return "missing AGC direct-resource offsets";
}
std::memcpy(direct_offsets.data(), user_data.direct_resource_offset, direct_size);
}
@@ -47,33 +46,33 @@ bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sg
}
if (next.vertex_attrib_reg >= 0 && next.vertex_buffer_reg < 0) {
return ShaderError::Fail(error, "vertex attribute table requires a vertex buffer table");
return "vertex attribute table requires a vertex buffer table";
}
if (next.vertex_buffer_reg < 0) {
metadata = next;
return true;
return nullptr;
}
if (static_cast<uint32_t>(next.vertex_buffer_reg) + 1u >= max_user_sgprs) {
return ShaderError::Fail(error, "vertex table pointer exceeds the user-SGPR domain");
return "vertex table pointer exceeds the user-SGPR domain";
}
if (next.vertex_attrib_reg < 0) {
metadata = next;
return true;
return nullptr;
}
if (static_cast<uint32_t>(next.vertex_attrib_reg) + 1u >= max_user_sgprs) {
return ShaderError::Fail(error, "vertex table pointer exceeds the user-SGPR domain");
return "vertex table pointer exceeds the user-SGPR domain";
}
if (data.num_input_semantics == 0 ||
data.num_input_semantics > ShaderVertexInputInfo::RES_MAX) {
return ShaderError::Fail(error, "vertex semantic count is outside the supported domain");
return "vertex semantic count is outside the supported domain";
}
if (data.input_semantics == nullptr) {
return ShaderError::Fail(error, "missing vertex input semantics");
return "missing vertex input semantics";
}
next.input_semantics = {data.input_semantics, data.num_input_semantics};
metadata = next;
return true;
return nullptr;
}
} // namespace Libs::Graphics
+4 -4
View File
@@ -4,7 +4,6 @@
#include "graphics/shader/shader.h"
#include <span>
#include <string>
namespace Libs::Graphics {
@@ -14,9 +13,10 @@ struct ShaderVertexMetadata {
std::span<const ShaderSemantic> input_semantics;
};
// Validates the vertex metadata and returns a view into the shader header.
bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata, std::string* error);
// Reads the vertex metadata as a view into the shader header. Returns nullptr if it is
// valid, otherwise why it was rejected.
const char* ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata);
} // namespace Libs::Graphics
+64 -30
View File
@@ -9,6 +9,7 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "loader/systemContent.h"
#include <algorithm>
#include <array>
@@ -63,15 +64,21 @@ constexpr int PROT_GPU_WRITE = 0x20;
enum class GpuAccessMode { NoAccess, Read, Write, ReadWrite };
constexpr uint64_t PAGE_TABLE_POOL_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr int PAGE_TABLE_POOL_ENTRIES =
static_cast<int>(PAGE_TABLE_POOL_SIZE / PAGE_TABLE_GRANULARITY);
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 448ull * 1024ull * 1024ull;
// A game gets 12.5 GiB. A fixed 64 MiB cache and mapping table memory past the 64 MiB included
// for each of the CPU, GPU and AMM tables come off the top; flexible and direct memory share what
// is left. One 4 KiB table maps 2 MiB.
constexpr uint64_t GUEST_MEMORY_BUDGET = 12800ull * 1024ull * 1024ull;
constexpr uint64_t RESERVED_CACHE_SIZE = 64ull * 1024ull * 1024ull;
constexpr uint64_t INCLUDED_TABLE_MEMORY = 64ull * 1024ull * 1024ull;
constexpr uint64_t MAPPING_TABLE_SIZE = 4ull * 1024ull;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static bool g_flexible_memory_size_frozen = false;
static Graphics::RenderContext* g_gpu_resources = nullptr;
static uint64_t g_guest_memory_size = GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static uint64_t g_cpu_table_memory = INCLUDED_TABLE_MEMORY;
static uint64_t g_gpu_table_memory = INCLUDED_TABLE_MEMORY;
static Graphics::RenderContext* g_gpu_resources = nullptr;
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
static void (*g_test_gpu_range)(bool, uint64_t, size_t) = nullptr;
#endif
@@ -664,11 +671,8 @@ public:
KYTY_CLASS_NO_COPY(PhysicalMemory);
static constexpr uint64_t TotalSize() { return static_cast<uint64_t>(13824) * 1024 * 1024; }
static uint64_t Size() {
EXIT_IF(g_flexible_memory_size >= TotalSize());
return TotalSize() - g_flexible_memory_size;
}
static uint64_t TotalSize() { return g_guest_memory_size; }
static uint64_t Size() { return TotalSize() - g_flexible_memory_size; }
bool Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
uint64_t* phys_addr_out, int memory_type,
@@ -738,6 +742,8 @@ public:
static uint64_t Size() { return g_flexible_memory_size; }
uint64_t Available();
void Charge(uint64_t size);
void Release(uint64_t size);
bool Map(uint64_t vaddr, size_t len, int prot, VirtualMemory::Mode mode, GpuAccessMode gpu_mode,
const char* name);
@@ -996,7 +1002,22 @@ static bool SelfTestSub64SharedPlaceholderAlias() {
static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size);
void Initialize() {
g_flexible_memory_size_frozen = true;
const auto params = Loader::SystemContentGetMemoryParams();
if (params.flexible_size != 0) {
g_flexible_memory_size = params.flexible_size;
}
g_cpu_table_memory = std::max(params.cpu_table_memory, INCLUDED_TABLE_MEMORY);
g_gpu_table_memory = std::max(params.gpu_table_memory, INCLUDED_TABLE_MEMORY);
const auto table_overhead = g_cpu_table_memory + g_gpu_table_memory +
std::max(params.amm_table_memory, INCLUDED_TABLE_MEMORY) -
3 * INCLUDED_TABLE_MEMORY;
EXIT_IF(g_flexible_memory_size + table_overhead >= GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE);
g_guest_memory_size = GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE - table_overhead;
LOGF("\t flexible memory size = %" PRIu64 " MiB, direct memory size = %" PRIu64 " MiB\n",
g_flexible_memory_size / (1024ull * 1024ull),
(g_guest_memory_size - g_flexible_memory_size) / (1024ull * 1024ull));
VirtualMemory::Init();
g_guest_address_space = std::make_unique<GuestAddressSpace>(PhysicalMemory::TotalSize());
g_physical_memory = std::make_unique<PhysicalMemory>();
@@ -1064,15 +1085,6 @@ void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func) {
}
}
void SetFlexibleMemorySize(uint64_t size) {
constexpr uint64_t GuestPageSize = 0x4000;
EXIT_IF(g_flexible_memory_size_frozen || g_guest_address_space != nullptr);
EXIT_IF(size == 0 || (size & (GuestPageSize - 1u)) != 0 || size >= PhysicalMemory::TotalSize());
g_flexible_memory_size = size;
LOGF("\t flexible memory size = 0x%016" PRIx64 " (%" PRIu64 " MiB)\n", size,
size / (1024ull * 1024ull));
}
bool PhysicalMemory::Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
uint64_t* phys_addr_out, int memory_type, AllocationKind kind) {
if (phys_addr_out == nullptr) {
@@ -1967,6 +1979,19 @@ uint64_t FlexibleMemory::Available() {
return (Size() >= m_allocated_total ? Size() - m_allocated_total : 0);
}
void FlexibleMemory::Charge(uint64_t size) {
Common::LockGuard lock(m_mutex);
m_allocated_total += size;
}
void FlexibleMemory::Release(uint64_t size) {
Common::LockGuard lock(m_mutex);
EXIT_IF(size > m_allocated_total);
m_allocated_total -= size;
}
void PooledMemory::AddFreeUnlocked(uint64_t start, uint64_t size) {
if (size == 0) {
return;
@@ -2640,12 +2665,12 @@ int KYTY_SYSV_ABI KernelGetPageTableStats(int* cpu_total, int* cpu_available, in
const auto gpu_used =
(g_virtual_ranges != nullptr ? g_virtual_ranges->CountPageTableEntries(true) : 0);
*cpu_total = PAGE_TABLE_POOL_ENTRIES;
*gpu_total = PAGE_TABLE_POOL_ENTRIES;
*cpu_available = PAGE_TABLE_POOL_ENTRIES -
static_cast<int>(std::min<uint64_t>(cpu_used, PAGE_TABLE_POOL_ENTRIES));
*gpu_available = PAGE_TABLE_POOL_ENTRIES -
static_cast<int>(std::min<uint64_t>(gpu_used, PAGE_TABLE_POOL_ENTRIES));
const auto cpu_tables = g_cpu_table_memory / MAPPING_TABLE_SIZE;
const auto gpu_tables = g_gpu_table_memory / MAPPING_TABLE_SIZE;
*cpu_total = static_cast<int>(cpu_tables);
*gpu_total = static_cast<int>(gpu_tables);
*cpu_available = static_cast<int>(cpu_tables - std::min(cpu_used, cpu_tables));
*gpu_available = static_cast<int>(gpu_tables - std::min(gpu_used, gpu_tables));
LOGF_COLOR(Log::Color::Green,
"\t cpu_total = %d\n"
@@ -3770,6 +3795,14 @@ uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size, VirtualMemor
return AllocateGuestRuntimeMemory(search_addr, size, mode, name, VirtualRangeType::Code, false);
}
void ChargeFlexibleMemory(uint64_t size) {
g_flexible_memory->Charge(size);
}
void ReleaseFlexibleMemory(uint64_t size) {
g_flexible_memory->Release(size);
}
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
@@ -4358,7 +4391,8 @@ DebugStats GetDebugStats() {
stats.direct_total = PhysicalMemory::Size();
stats.flexible_total = FlexibleMemory::Size();
stats.pool_committed = g_memory_pool_committed.load(std::memory_order_relaxed);
stats.page_entries_total = PAGE_TABLE_POOL_ENTRIES;
stats.cpu_page_entries_total = g_cpu_table_memory / MAPPING_TABLE_SIZE;
stats.gpu_page_entries_total = g_gpu_table_memory / MAPPING_TABLE_SIZE;
if (g_physical_memory != nullptr) {
Common::LockGuard lock(g_physical_memory->GetMutex());
for (const auto& [start, block]: g_physical_memory->GetPhysicalBlocks()) {
+7 -4
View File
@@ -26,9 +26,10 @@ struct DebugStats {
uint64_t flexible_total = 0;
uint64_t flexible_used = 0;
uint64_t pool_committed = 0;
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
uint64_t gpu_page_entries = 0;
uint64_t page_entries_total = 0; // Per side.
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
uint64_t gpu_page_entries = 0;
uint64_t cpu_page_entries_total = 0;
uint64_t gpu_page_entries_total = 0;
};
[[nodiscard]] DebugStats GetDebugStats();
@@ -125,7 +126,6 @@ static_assert(sizeof(KernelMemoryPoolBlockStats) == 16,
"KernelMemoryPoolBlockStats struct size is incorrect");
void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func);
void SetFlexibleMemorySize(uint64_t size);
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t size, size_t alignment,
int memory_type, int64_t* phys_addr_out, bool automatic = false);
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
@@ -197,6 +197,9 @@ int KYTY_SYSV_ABI KernelMemoryPoolGetBlockStats(KernelMemoryPoolBlockStats* outp
uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size,
Common::VirtualMemory::Mode mode, const char* name);
// Loaded modules' segments use flexible memory without taking its backing.
void ChargeFlexibleMemory(uint64_t size);
void ReleaseFlexibleMemory(uint64_t size);
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, Common::VirtualMemory::Mode mode);
uint64_t AllocateRuntimeMemory(uint64_t search_addr, uint64_t size,
Common::VirtualMemory::Mode mode, const char* name,
+2 -2
View File
@@ -820,12 +820,12 @@ public:
return ProtectMappedUnlocked(vaddr, size, mode);
}
// Tracker protections only read m_mapped; the page tracker orders them per page.
bool ProtectTransient(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return ProtectMappedUnlocked(vaddr, size, mode);
}
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff. Show more