Compare commits

...
Author SHA1 Message Date
Stefanos Costaandnmzik 3f21e2ee0f kernel: preserve guest pthread priorities
Extracted from Stefanos Costa's pthread fix in #147.
2026-08-03 01:05:00 +02:00
nmzik 86a586025f renderer: expand rectangle lists with tessellation
Remove the legacy NGG rectangle workaround and its configuration toggle.
2026-08-03 00:35:02 +02:00
nmzik 06cbd602c1 graphics: preserve indirect instance state 2026-08-03 00:35:02 +02:00
nmzik 055d0920d7 renderer: allow array storage views at nonzero mips 2026-08-03 00:35:02 +02:00
nmzik f2ee98fe31 renderer: separate tiled and linear texture capacities 2026-08-03 00:35:02 +02:00
nmzik 207cef9602 renderer: report invalid texture upload layout 2026-08-03 00:35:02 +02:00
c4134a95e2 Use repository root as CMake source directory (#153)
* feat(project): add root cmakelists for ide auto-detect

* cmake: use repository root as source directory

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-03 00:34:21 +02:00
nmzik fb5ecec455 renderer: ignore stale stencil state for depth-only targets 2026-08-02 14:14:32 +02:00
nmzik 650d9c91a1 libNet: add missing abi 2026-08-02 13:49:07 +02:00
nmzik fc8d2a3b83 guest_gpu: remove memory-unmap submission deadlock + remove legacy agc buffering 2026-08-02 13:26:29 +02:00
nmzik 59b8fad341 graphics: support 3D color render targets 2026-08-02 09:38:57 +02:00
nmzik b877b4be9c graphics: perf - batch GPU page watcher updates at 4 MiB granularity 2026-08-02 08:39:50 +02:00
nmzik 9da7fc5dd6 renderer: minor optimizations 2026-08-02 08:34:44 +02:00
nmzik da0d33224d renderer: eliminate extra copy for small streaming buffers 2026-08-02 08:34:44 +02:00
nmzik f831e60412 agc: new abis 2026-08-02 07:59:23 +02:00
nmzik 84236d1f87 agc: new abi 2026-08-02 07:59:22 +02:00
Stefanos Costaandnmzik 302b579779 loader: zero unresolved scalar floating-point returns
Extracted from 3db2b3c5c5e1a26a861df7ebcacd9ccb8c484420 in KytyPS5/KytyPS5#147.
2026-08-02 06:35:53 +02:00
Stefanos Costaandnmzik 0b6bf01b36 kernel: preserve microsecond wall-clock resolution
Extracted from 3db2b3c5c5e1a26a861df7ebcacd9ccb8c484420 in KytyPS5/KytyPS5#147.
2026-08-02 06:35:52 +02:00
Stefanos Costaandnmzik 66f640527d audio: fix pacing and AudioOut2 port lifetime
Extracted from 6a60f1b17481a0e5e14242c0fb4dc22f963545e1 in KytyPS5/KytyPS5#147.
2026-08-02 06:35:52 +02:00
nmzik 4631b96178 perf(gpu): run dirty-page validation only in debug builds 2026-08-02 04:58:56 +02:00
43f64e4ab4 Register remaining regression tests with CTest (#24)
Register regression tests with CTest

Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-08-02 04:54:04 +02:00
IdyllizeandGitHub e63f5b7d5c cmake: preserve spaces in clang-cl linker paths (#26)
Pass linker flags as individual options so CMake keeps the PDB and lld map paths intact when the build directory contains spaces.
2026-08-02 04:45:28 +02:00
nikosszzzandnmzik fa7c3c01bf fix: guard Linux memory fixes to only Linux 2026-08-02 03:43:56 +02:00
nikosszzzandnmzik 89651f6f59 kernel/memory: reserve only available guest address ranges on Linux
Reserve only free guest address ranges
2026-08-02 03:43:56 +02:00
nmzik 44d7f2a3e8 shader cfg: handle shared early exits
Duplicate small shared exit tails so each selection gets its own merge block. This keeps overlapping early-exit ladders on structured SPIR-V and adds a regression test.
2026-08-02 03:16:08 +02:00
nmzik 2dcb90066c shader cfg: normalize loop structure
Give loops one header and one continue path before SPIR-V generation. This handles conditional headers and multiple latches without falling back to a dispatcher.
2026-08-02 03:15:26 +02:00
nmzik 51a33cc363 shader cfg: handle loop control branches
Keep simple break, continue, and repeat branches in structured control flow. Split conflicting merge blocks and add regression tests for nested loop exits.
2026-08-02 03:14:39 +02:00
nikosszzzandnmzik ed84370786 fix(libc): run thread-local destructors
Why: Thread-atexit registrations were discarded, leaving objects alive after their guest TLS storage was released.

What: Store registrations per host thread and run them in LIFO order before pthread keys and guest TLS are destroyed.

Why safe: Only callbacks registered on the exiting thread run, once, before existing teardown continues.
2026-08-02 02:48:56 +02:00
Claxtenandnmzik 43f30d3ab2 graphics: shader: ignore unused sampler border state
* Sampler dword 3 only matters when a clamp mode uses border color
  (values >= 4). When no border mode is active, dword 3 is unused
  but can still vary across loop iterations due to wave-lane spills.
  This makes resource tracking think the descriptor is dynamic and
  fail with "unsupported GPU selection".

* Fix by zeroing dword 3 when all clamp modes are non-border.

Signed-off-by: Claxten <claxten10@gmail.com>
2026-08-02 02:42:15 +02:00
Stepz97andGitHub 0838142abd macOS: anchor the guest address space in full-emulator test targets (#143)
fix(cmake): anchor the macOS guest address space for all full-emulator tests

Every target created by add_kyty_full_emulator_test links against the
full kyty_emulator sources, so it drags in the same 620 GiB .zerofill
guest address space segments as the emulator itself. Only the emulator
target and virtual_memory_allocation_tests had the linker flags that
anchor those segments; every other full-emulator test target got the
segments without the anchoring, and the kernel killed them on exec
(posix_spawn EIO / SIGKILL) before main() ever ran.

Move the configure_macos_guest_address_space() call into
add_kyty_full_emulator_test() itself so every target it creates gets
it automatically, and drop the now-redundant explicit call on
virtual_memory_allocation_tests.
2026-08-02 02:27:32 +02:00
0f550d1fd0 fix: keep hint-less guest mappings at the canonical PS5 base (fixes the #135 macOS regression) (#138)
* fix: keep hint-less guest mappings at the canonical PS5 base

FindGuestFreeRange searched the low system-managed range first for
mappings with no address hint, so the first hint-less direct-memory map
could land as low as 0x200000. The PS5 kernel never places hint-less
user mappings below 0x200000000 and guest code relies on that: Sony's
libc maps 4 MiB of direct memory for its internal heap, fails its
mspace setup when the returned address is that low, and the first
malloc then dereferences a null mspace (a read at 0x38, the mspace
magic check). On macOS this made Raiden III crash on the main guest
thread a couple of seconds after boot, 100 percent reproducible with
--printf-direction Silent.

Search from the canonical base first, fall back to the user range, and
keep the low system-managed range only as a last resort. The mmap path
already anchored hint-less searches at 0x200000000; this aligns the
shared search helper with it.

Adds two regression tests: the libc-shaped allocation must come back at
or above the canonical base and hold writes, and direct-memory content
must survive an unmap and remap of the same physical range.

* macos: make the fatal-report memory dumps fault-safe

IsReadableRange returned true for any nonzero address on macOS, so the
fatal report's guest memory dumps dereferenced whatever the crashed
thread had in its registers. A fault inside the reporter re-enters the
signal handler and wedges the reporting thread, which hid real guest
crashes whenever logging was enabled: the game kept running with a dead
thread and the report was never completed.

Walk the Mach regions covering the range and require read permission
before dumping, the same contract the Linux implementation provides.

* do not fallthrough HOST_SYSTEM_MANAGED_MIN

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-02 02:23:12 +02:00
Claxtenandnmzik c1a5927036 graphics: pm4: accept trailing PM4 type-2 packets
* A one-dword type-2 NOP is a valid packet tail. Parse it normally instead of aborting command-buffer dumps.

Signed-off-by: Claxten <claxten10@gmail.com>
2026-08-02 02:07:37 +02:00
Claxtenandnmzik bc436548a9 graphics: support packed 10-10-10-2 uint buffers
Signed-off-by: Claxten <claxten10@gmail.com>
2026-08-02 01:44:21 +02:00
nmzik a65d17a5d6 renderer: skip debug checks when stencil&depth is not active 2026-08-01 00:22:24 +02:00
nmzik c690aeea62 add HiS PM4 handler, accept Ngs2CustomMastering 2026-08-01 00:12:42 +02:00
nmzik f830d6b2e4 renderer: remove legacy code left from refactoring 2026-07-31 23:13:48 +02:00
nmzik d68a477276 renderer: broaden compatibility 2026-07-31 22:24:56 +02:00
nmzik 8977d4d2f0 shader: add descriptor log 2026-07-31 21:29:26 +02:00
nmzik 4b4e3bf3cf pm4: implement missing selectors 2026-07-31 19:45:47 +02:00
nmzik a0bb129f02 SaveData: stop escaping root directory 2026-07-31 18:53:54 +02:00
nmzik 846002c5eb shader: fix invalid texture descriptors and add missing format 2026-07-31 17:30:31 +02:00
80 changed files with 5545 additions and 4311 deletions
+9 -9
View File
@@ -73,7 +73,7 @@ jobs:
- name: Configure
shell: cmd
run: |
cmake -S src -B _Build/windows ^
cmake -S . -B _Build/windows ^
-G Ninja ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_C_COMPILER=clang-cl ^
@@ -83,12 +83,12 @@ jobs:
- name: Build
shell: cmd
run: |
cmake --build _Build/windows --target launcher virtual_memory_allocation_tests --parallel
cmake --build _Build/windows --target launcher audio_out2_port_tests virtual_memory_allocation_tests --parallel
- name: Test
shell: cmd
run: |
ctest --test-dir _Build/windows --output-on-failure -R "^virtual_memory_allocation$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -147,7 +147,7 @@ jobs:
- name: Configure
shell: bash
run: |
cmake -S src -B _Build/macos \
cmake -S . -B _Build/macos \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
@@ -159,14 +159,14 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher virtual_memory_allocation_tests \
--target launcher audio_out2_port_tests virtual_memory_allocation_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^virtual_memory_allocation$'
-R '^(audio_out2_port|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -270,7 +270,7 @@ jobs:
shell: bash
run: |
mkdir -p _Build
cmake -S src -B _Build/linux \
cmake -S . -B _Build/linux \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
@@ -297,14 +297,14 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
virtual_memory_allocation_tests \
audio_out2_port_tests virtual_memory_allocation_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(page_manager|memory_tracker|virtual_memory_allocation)$'
-R '^(audio_out2_port|page_manager|memory_tracker|virtual_memory_allocation)$'
- name: Install
shell: bash
+2 -1
View File
@@ -3,4 +3,5 @@
.idea/
build/
_Build/vscode-clang/
_Build/
_Build/
cmake-build-debug/
+1 -1
View File
@@ -1,5 +1,5 @@
{
"cmake.sourceDirectory": "${workspaceFolder}/src",
"cmake.sourceDirectory": "${workspaceFolder}",
"cmake.buildDirectory": "${workspaceFolder}/_Build/vscode-clang",
"cmake.generator": "Ninja",
"cmake.environment": {
+152 -108
View File
@@ -6,6 +6,10 @@ endif()
project(Kyty)
set(KYTY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(KYTY_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
set(KYTY_THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty")
if(CMAKE_SYSTEM_NAME MATCHES ".*Linux")
set(LINUX TRUE)
endif()
@@ -19,13 +23,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
include(utils.cmake)
include("${KYTY_SOURCE_DIR}/utils.cmake")
include(CTest)
option(KYTY_ENABLE_CLANG_TIDY "Run clang-tidy checks during builds" OFF)
set(KYTY_THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty")
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
include(TestBigEndian)
@@ -97,36 +99,36 @@ endif()
project(Kyty${KYTY_PROJECT_NAME}${CMAKE_BUILD_TYPE}${KYTY_COMPILER} VERSION 0.2.2)
include_directories(
include_directories(
${KYTY_THIRD_PARTY_DIR}/gtest/include
${KYTY_THIRD_PARTY_DIR}/gtest
${KYTY_THIRD_PARTY_DIR}/fmt/include
${KYTY_THIRD_PARTY_DIR}/magic_enum/include/magic_enum
${PROJECT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${KYTY_SOURCE_DIR}
)
set(KYTY_VERSION "${PROJECT_VERSION}")
configure_file(
${PROJECT_SOURCE_DIR}/cmake_config.h.in
${KYTY_SOURCE_DIR}/cmake_config.h.in
${PROJECT_BINARY_DIR}/cmake_config.h
)
find_package(Git)
add_custom_target( KytyGitVersion
COMMAND ${CMAKE_COMMAND}
-D INPUT_FILE=${CMAKE_CURRENT_SOURCE_DIR}/kytyGitVersion.h.in
-D OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/kytyGitVersion.h
COMMAND ${CMAKE_COMMAND}
-D INPUT_FILE=${KYTY_SOURCE_DIR}/kytyGitVersion.h.in
-D OUTPUT_FILE=${PROJECT_BINARY_DIR}/kytyGitVersion.h
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D GIT_WORKING_DIRECTORY=${CMAKE_CURRENT_SOURCE_DIR}
-P ${CMAKE_CURRENT_SOURCE_DIR}/generate_version.cmake
-P ${KYTY_SOURCE_DIR}/generate_version.cmake
COMMENT "Generate kytyGitVersion.h"
)
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0)
list(APPEND KYTY_IWYU
list(APPEND KYTY_IWYU
kyty_emulator
common
#launcher
@@ -139,55 +141,55 @@ if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0)
endif()
option(KYTY_BUILD_LAUNCHER "Build Qt launcher" ON)
config_compiler_and_linker()
config_compiler_and_linker()
add_subdirectory("${KYTY_THIRD_PARTY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/3rdparty")
add_subdirectory(common)
add_subdirectory("${KYTY_SOURCE_DIR}/common" "${CMAKE_CURRENT_BINARY_DIR}/common")
file(GLOB kyty_emulator_src CONFIGURE_DEPENDS
libs/*.cpp
libs/*.h
graphics/*.cpp
graphics/*.h
graphics/guest_gpu/*.cpp
graphics/guest_gpu/*.h
graphics/guest_gpu/command_processor/*.cpp
graphics/guest_gpu/command_processor/*.h
graphics/host_gpu/*.cpp
graphics/host_gpu/*.h
graphics/host_gpu/renderer/*.cpp
graphics/host_gpu/renderer/*.h
graphics/host_gpu/renderer/cache/*.cpp
graphics/host_gpu/renderer/cache/*.h
graphics/host_gpu/renderer/image/*.cpp
graphics/host_gpu/renderer/image/*.h
graphics/host_gpu/renderer/pipeline/*.cpp
graphics/host_gpu/renderer/pipeline/*.h
graphics/shader/*.cpp
graphics/shader/*.h
graphics/shader/recompiler/*.cpp
graphics/shader/recompiler/*.h
graphics/shader/recompiler/cfg/*.cpp
graphics/shader/recompiler/cfg/*.h
graphics/shader/recompiler/decompiler/*.cpp
graphics/shader/recompiler/decompiler/*.h
graphics/shader/recompiler/emitter/*.cpp
graphics/shader/recompiler/emitter/*.h
graphics/shader/recompiler/ir/*.cpp
graphics/shader/recompiler/ir/*.h
graphics/presentation/*.cpp
graphics/presentation/*.h
graphics/presentation/window/*.cpp
graphics/presentation/window/*.h
kernel/*.cpp
kernel/*.h
loader/*.cpp
loader/*.h
"${KYTY_SOURCE_DIR}/libs/*.cpp"
"${KYTY_SOURCE_DIR}/libs/*.h"
"${KYTY_SOURCE_DIR}/graphics/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/*.h"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/*.h"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/command_processor/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/command_processor/*.h"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/*.h"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/*.h"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/cache/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/cache/*.h"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/image/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/image/*.h"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/pipeline/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/pipeline/*.h"
"${KYTY_SOURCE_DIR}/graphics/shader/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/*.h"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/*.h"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/cfg/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/cfg/*.h"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/decompiler/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/decompiler/*.h"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/emitter/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/emitter/*.h"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/*.h"
"${KYTY_SOURCE_DIR}/graphics/presentation/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/presentation/*.h"
"${KYTY_SOURCE_DIR}/graphics/presentation/window/*.cpp"
"${KYTY_SOURCE_DIR}/graphics/presentation/window/*.h"
"${KYTY_SOURCE_DIR}/kernel/*.cpp"
"${KYTY_SOURCE_DIR}/kernel/*.h"
"${KYTY_SOURCE_DIR}/loader/*.cpp"
"${KYTY_SOURCE_DIR}/loader/*.h"
)
find_program(KYTY_GLSLANG_VALIDATOR glslangValidator REQUIRED)
set(gpu_tiler_shader_dir "${CMAKE_CURRENT_SOURCE_DIR}/graphics/host_gpu/shaders")
set(gpu_tiler_shader_dir "${KYTY_SOURCE_DIR}/graphics/host_gpu/shaders")
set(gpu_tiler_generated_dir "${PROJECT_BINARY_DIR}/gpu_tiler_shaders")
set(gpu_tiler_shader_names
standard256
@@ -215,7 +217,7 @@ foreach(shader_name IN LISTS gpu_tiler_shader_names)
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.0 -Os
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
-DSYMBOL=${shader_symbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${shader_source}" ${gpu_tiler_shader_includes}
VERBATIM
)
@@ -239,7 +241,7 @@ foreach(shader_source IN LISTS gpu_blit_shader_sources)
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.0 -Os
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
-DSYMBOL=${shader_symbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${shader_source}"
VERBATIM
)
@@ -248,8 +250,8 @@ endforeach()
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
list(APPEND kyty_emulator_src
emulator.h
emulator.cpp
"${KYTY_SOURCE_DIR}/emulator.h"
"${KYTY_SOURCE_DIR}/emulator.cpp"
)
list(REMOVE_DUPLICATES kyty_emulator_src)
@@ -280,7 +282,7 @@ if(LINUX)
endif()
set(inc_headers
${CMAKE_CURRENT_SOURCE_DIR}
${KYTY_SOURCE_DIR}
${KYTY_THIRD_PARTY_DIR}/SDL2/include
${KYTY_THIRD_PARTY_DIR}/VulkanMemoryAllocator/include
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
@@ -292,13 +294,13 @@ set(inc_headers
if (KYTY_CLANG_CL)
list(APPEND kyty_emulator_link_libraries winpthread)
list(APPEND inc_headers
list(APPEND inc_headers
${KYTY_THIRD_PARTY_DIR}/winpthread/include
)
endif()
list(APPEND check_headers
${CMAKE_CURRENT_SOURCE_DIR}
${KYTY_SOURCE_DIR}
)
function(add_kyty_full_emulator_test target source)
@@ -312,100 +314,116 @@ function(add_kyty_full_emulator_test target source)
target_link_libraries(${target} onecore)
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_THIRD_PARTY_DIR}/winpthread/bin/libwinpthread-1.dll" $<TARGET_FILE_DIR:${target}>/libwinpthread-1.dll)
endif()
# The macOS x86_64 guest address space needs its .zerofill segments anchored
# by linker flags, or the kernel kills the binary on load (posix_spawn EIO).
configure_macos_guest_address_space(${target})
endfunction()
function(configure_macos_guest_address_space target)
if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR
(NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")))
target_sources(${target} PRIVATE kernel/macosGuestAddressSpace.cpp)
target_sources(${target} PRIVATE "${KYTY_SOURCE_DIR}/kernel/macosGuestAddressSpace.cpp")
target_compile_definitions(${target} PRIVATE KYTY_LINKED_GUEST_ADDRESS_SPACE=1)
target_link_options(${target} PRIVATE
-Wl,-ld_classic,-no_pie,-no_fixup_chains,-no_huge,-pagezero_size,0x40000,-segaddr,SYSTEM_MANAGED,0x40000,-segaddr,SYSTEM_RESERVED,0x7ffffc000,-segaddr,USER_AREA,0x7000000000,-image_base,0x700000000000)
endif()
endfunction()
add_kyty_full_emulator_test(shader_cfg_tests ../tests/shaderCfgTests.cpp)
add_kyty_full_emulator_test(shader_cfg_tests "${KYTY_TESTS_DIR}/shaderCfgTests.cpp")
add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
../tests/ScalarProvenanceTests.cpp
graphics/host_gpu/hostMemory.cpp
graphics/shader/recompiler/ir/ReadLaneElimination.cpp
graphics/shader/recompiler/ir/ScalarProvenance.cpp
graphics/shader/recompiler/ir/SrtWalker.cpp
"${KYTY_TESTS_DIR}/ScalarProvenanceTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/hostMemory.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ReadLaneElimination.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ScalarProvenance.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/SrtWalker.cpp"
)
target_link_libraries(scalar_provenance_tests fmt::fmt)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(page_manager_tests EXCLUDE_FROM_ALL
../tests/PageManagerTests.cpp
graphics/host_gpu/pageManager.cpp
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
)
target_include_directories(page_manager_tests PRIVATE ${inc_headers})
add_executable(bit_array_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
)
target_include_directories(bit_array_tests PRIVATE ${inc_headers})
add_executable(memory_tracker_tests EXCLUDE_FROM_ALL
../tests/MemoryTrackerTests.cpp
graphics/host_gpu/pageManager.cpp
graphics/host_gpu/memoryTracker.cpp
"${KYTY_TESTS_DIR}/MemoryTrackerTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/memoryTracker.cpp"
)
target_link_libraries(memory_tracker_tests fmt::fmt common)
target_include_directories(memory_tracker_tests PRIVATE ${inc_headers})
add_executable(shader_vertex_metadata_tests EXCLUDE_FROM_ALL
../tests/ShaderVertexMetadataTests.cpp
graphics/host_gpu/hostMemory.cpp
graphics/shader/shaderVertexMetadata.cpp
"${KYTY_TESTS_DIR}/ShaderVertexMetadataTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/hostMemory.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/shaderVertexMetadata.cpp"
)
target_include_directories(shader_vertex_metadata_tests PRIVATE ${inc_headers})
add_executable(shader_stage_runtime_tests EXCLUDE_FROM_ALL
../tests/ShaderStageRuntimeTests.cpp
graphics/guest_gpu/gpu_format.cpp
graphics/host_gpu/hostMemory.cpp
graphics/shader/shaderStageRuntime.cpp
graphics/shader/recompiler/ir/ResourceMaterialization.cpp
graphics/shader/recompiler/ir/ScalarProvenance.cpp
graphics/shader/recompiler/ir/SrtWalker.cpp
"${KYTY_TESTS_DIR}/ShaderStageRuntimeTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/gpu_format.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/hostMemory.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/shaderStageRuntime.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ScalarProvenance.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/SrtWalker.cpp"
)
target_link_libraries(shader_stage_runtime_tests fmt::fmt)
target_include_directories(shader_stage_runtime_tests PRIVATE ${inc_headers})
add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
../tests/ResourceTrackingTests.cpp
graphics/guest_gpu/gpu_format.cpp
graphics/host_gpu/hostMemory.cpp
graphics/shader/recompiler/ir/ScalarProvenance.cpp
graphics/shader/recompiler/ir/SrtWalker.cpp
graphics/shader/recompiler/ir/SrtPatcher.cpp
graphics/shader/recompiler/ir/ResourceTracking.cpp
graphics/shader/recompiler/ir/ResourceMaterialization.cpp
graphics/shader/recompiler/ir/ShaderInfoCollection.cpp
graphics/shader/recompiler/ir/BindingLayout.cpp
"${KYTY_TESTS_DIR}/ResourceTrackingTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/gpu_format.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/hostMemory.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ScalarProvenance.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/SrtWalker.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/SrtPatcher.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ResourceTracking.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/ShaderInfoCollection.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/BindingLayout.cpp"
)
target_link_libraries(resource_tracking_tests fmt::fmt)
target_include_directories(resource_tracking_tests PRIVATE ${inc_headers})
add_executable(resource_mutex_tests EXCLUDE_FROM_ALL
../tests/ResourceMutexTests.cpp
graphics/host_gpu/renderer/cache/resourceMutex.cpp
"${KYTY_TESTS_DIR}/ResourceMutexTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/renderer/cache/resourceMutex.cpp"
)
target_link_libraries(resource_mutex_tests common)
target_include_directories(resource_mutex_tests PRIVATE ${inc_headers})
add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/AudioOut2PortTests.cpp"
"${KYTY_SOURCE_DIR}/libs/libAudio2.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(audio_out2_port_tests common fmt::fmt)
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
add_executable(event_queue_lifetime_tests EXCLUDE_FROM_ALL
../tests/EventQueueLifetimeTests.cpp
kernel/eventQueue.cpp
loader/timer.cpp
"${KYTY_TESTS_DIR}/EventQueueLifetimeTests.cpp"
"${KYTY_SOURCE_DIR}/kernel/eventQueue.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(event_queue_lifetime_tests common fmt::fmt)
target_include_directories(event_queue_lifetime_tests PRIVATE ${inc_headers})
add_executable(image_page_table_tests EXCLUDE_FROM_ALL
../tests/ImagePageTableTests.cpp
"${KYTY_TESTS_DIR}/ImagePageTableTests.cpp"
)
target_link_libraries(image_page_table_tests common fmt::fmt)
target_include_directories(image_page_table_tests PRIVATE ${inc_headers})
add_kyty_full_emulator_test(shader_recompiler_compute_tests ../tests/ShaderRecompilerComputeTests.cpp)
add_kyty_full_emulator_test(shader_recompiler_compute_tests "${KYTY_TESTS_DIR}/ShaderRecompilerComputeTests.cpp")
set(gpu_test_generated_dir "${PROJECT_BINARY_DIR}/gpu_test_shaders")
set(gpu_test_ms_depth_source
@@ -421,17 +439,16 @@ add_custom_command(
-o "${gpu_test_ms_depth_spv}" "${gpu_test_ms_depth_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${gpu_test_ms_depth_spv}
-DOUTPUT=${gpu_test_ms_depth_header} -DSYMBOL=GPU_TEST_MS_DEPTH_SPV
-P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
-P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${gpu_test_ms_depth_source}"
VERBATIM
)
target_sources(shader_recompiler_compute_tests PRIVATE
"${gpu_test_ms_depth_header}")
add_kyty_full_emulator_test(virtual_memory_allocation_tests ../tests/VirtualMemoryAllocationTests.cpp)
add_kyty_full_emulator_test(virtual_memory_allocation_tests "${KYTY_TESTS_DIR}/VirtualMemoryAllocationTests.cpp")
target_compile_definitions(virtual_memory_allocation_tests PRIVATE
KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS=1)
configure_macos_guest_address_space(virtual_memory_allocation_tests)
# These tests use exceptions.
if(NOT KYTY_CLANG_CL)
@@ -442,12 +459,18 @@ if(NOT KYTY_CLANG_CL)
endif()
if(BUILD_TESTING)
add_test(NAME shader_cfg COMMAND $<TARGET_FILE:shader_cfg_tests>)
add_test(NAME scalar_provenance COMMAND $<TARGET_FILE:scalar_provenance_tests>)
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 bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
add_test(NAME shader_stage_runtime COMMAND $<TARGET_FILE:shader_stage_runtime_tests>)
add_test(NAME resource_tracking COMMAND $<TARGET_FILE:resource_tracking_tests>)
add_test(NAME resource_mutex COMMAND $<TARGET_FILE:resource_mutex_tests>)
add_test(NAME event_queue_lifetime COMMAND $<TARGET_FILE:event_queue_lifetime_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME virtual_memory_allocation
COMMAND $<TARGET_FILE:virtual_memory_allocation_tests>)
@@ -480,10 +503,26 @@ if(BUILD_TESTING)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
endif()
add_custom_target(kyty_tests DEPENDS
shader_cfg_tests
scalar_provenance_tests
image_page_table_tests
memory_tracker_tests
page_manager_tests
bit_array_tests
shader_vertex_metadata_tests
shader_stage_runtime_tests
resource_tracking_tests
resource_mutex_tests
event_queue_lifetime_tests
shader_recompiler_compute_tests
virtual_memory_allocation_tests
)
endif()
add_executable(kyty_emulator main.cpp ${kyty_emulator_src})
add_executable(kyty_emulator "${KYTY_SOURCE_DIR}/main.cpp" ${kyty_emulator_src})
configure_macos_guest_address_space(kyty_emulator)
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
@@ -513,7 +552,12 @@ set(KYTY_EMULATOR_MAP_LINK_PATH "${CMAKE_CURRENT_BINARY_DIR}/${KYTY_EMULATOR_MAP
set(KYTY_EMULATOR_PDB_LINK_PATH "${CMAKE_CURRENT_BINARY_DIR}/kyty_emulator.pdb")
if(KYTY_CLANG_CL)
set_target_properties(kyty_emulator PROPERTIES LINK_FLAGS "/DYNAMICBASE:NO /DEBUG:FULL /PDB:${KYTY_EMULATOR_PDB_LINK_PATH} /lldmap:${KYTY_EMULATOR_MAP_LINK_PATH}")
target_link_options(kyty_emulator PRIVATE
"/DYNAMICBASE:NO"
"/DEBUG:FULL"
"/PDB:${KYTY_EMULATOR_PDB_LINK_PATH}"
"/lldmap:${KYTY_EMULATOR_MAP_LINK_PATH}"
)
add_custom_command(TARGET kyty_emulator POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_THIRD_PARTY_DIR}/winpthread/bin/libwinpthread-1.dll" $<TARGET_FILE_DIR:kyty_emulator>/libwinpthread-1.dll)
elseif(WIN32 OR LINUX)
set_target_properties(kyty_emulator PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS} -Wl,-Map=${KYTY_EMULATOR_MAP_LINK_PATH}")
@@ -528,14 +572,14 @@ if(APPLE)
# reverts to the hardened defaults and aborts when it first executes written code).
add_custom_command(TARGET kyty_emulator POST_BUILD
COMMAND codesign -s - --force --options runtime
--entitlements "${CMAKE_CURRENT_SOURCE_DIR}/macos_jit.entitlements"
--entitlements "${KYTY_SOURCE_DIR}/macos_jit.entitlements"
$<TARGET_FILE:kyty_emulator>
COMMENT "Codesign kyty_emulator with JIT entitlements (macOS)")
endif()
install(TARGETS kyty_emulator DESTINATION .)
if(KYTY_BUILD_LAUNCHER)
add_subdirectory(launcher)
add_subdirectory("${KYTY_SOURCE_DIR}/launcher" "${CMAKE_CURRENT_BINARY_DIR}/launcher")
endif()
if(KYTY_CLANG_CL)
install(FILES "${KYTY_THIRD_PARTY_DIR}/winpthread/bin/libwinpthread-1.dll" DESTINATION .)
+15 -4
View File
@@ -141,7 +141,7 @@ git submodule update --init --recursive
Configure the project. Replace the Qt path with the version installed on your system:
```powershell
cmake -S src -B _Build/windows -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvc2022_64"
cmake -S . -B _Build/windows -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvc2022_64"
```
Build the launcher and stage a runnable installation:
@@ -174,7 +174,7 @@ Qt 6 (Concurrent, Network, Widgets) is also required — either the distribution
```bash
git submodule update --init --recursive
cmake -S src -B _Build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release \
cmake -S . -B _Build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_PREFIX_PATH="$Qt6_DIR"
@@ -188,7 +188,7 @@ The install step copies the Qt libraries and plugins next to the binaries, so
As on Windows, the MSVC compiler is not used; Clang is required. `cl.exe` is rejected at configure
time.
Note that the CMake source root is `src`, not the repository root.
The CMake source root is the repository root.
### Building on macOS
@@ -207,7 +207,7 @@ Requirements:
```bash
git submodule update --init --recursive
cmake -S src -B _Build/macos -G Ninja -DCMAKE_BUILD_TYPE=Release \
cmake -S . -B _Build/macos -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_PREFIX_PATH="$Qt6_DIR"
@@ -229,6 +229,17 @@ codesign --force --sign - _Build/macos/install/libMoltenVK.dylib
Release archives already include a signed `libMoltenVK.dylib`.
### Regression tests
Build every regression executable and run the registered tests with:
```powershell
cmake --build _Build/windows --target kyty_tests
ctest --test-dir _Build/windows --output-on-failure
```
Use `_Build/linux` instead of `_Build/windows` for a Linux build.
### Visual Studio Code
A ready-made Visual Studio Code setup is included in [`.vscode`](.vscode). It configures CMake
+1 -1
View File
@@ -13,7 +13,7 @@ endif()
add_library(common STATIC ${common_src} ${common_headers})
target_include_directories(common
PUBLIC ${PROJECT_SOURCE_DIR}
PUBLIC ${KYTY_SOURCE_DIR}
PRIVATE
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
)
+260
View File
@@ -0,0 +1,260 @@
#ifndef EMULATOR_SRC_COMMON_BITARRAY_H_
#define EMULATOR_SRC_COMMON_BITARRAY_H_
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <utility>
namespace Common {
template <size_t N>
class BitArray final {
static_assert(N != 0, "BitArray size must be nonzero");
static_assert(N % 64 == 0, "BitArray size must be a multiple of 64 bits");
static constexpr size_t BITS_PER_WORD = 64;
static constexpr size_t WORD_COUNT = N / BITS_PER_WORD;
public:
using Range = std::pair<size_t, size_t>;
class Iterator final {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = Range;
using difference_type = std::ptrdiff_t;
using pointer = const Range*;
using reference = const Range&;
Iterator(const BitArray& bits, size_t start)
: m_bits(bits), m_range(bits.FirstRangeFrom(start)) {}
Iterator& operator++() {
m_range = m_bits.FirstRangeFrom(m_range.second);
return *this;
}
[[nodiscard]] bool operator==(const Iterator& other) const {
return &m_bits == &other.m_bits && m_range == other.m_range;
}
[[nodiscard]] bool operator!=(const Iterator& other) const { return !(*this == other); }
[[nodiscard]] reference operator*() const { return m_range; }
[[nodiscard]] pointer operator->() const { return &m_range; }
private:
const BitArray& m_bits;
Range m_range;
};
using const_iterator = Iterator;
constexpr BitArray() = default;
constexpr BitArray(const BitArray& other, size_t start, size_t end) {
if (start >= end || end > N) {
return;
}
const auto first_word = start / BITS_PER_WORD;
const auto last_word = (end - 1) / BITS_PER_WORD;
const auto start_bit = start % BITS_PER_WORD;
const auto end_bit = (end - 1) % BITS_PER_WORD;
const auto start_mask = ~uint64_t {0} << start_bit;
const auto end_mask =
end_bit == BITS_PER_WORD - 1 ? ~uint64_t {0} : (uint64_t {1} << (end_bit + 1)) - 1;
if (first_word == last_word) {
m_data[first_word] = other.m_data[first_word] & start_mask & end_mask;
return;
}
m_data[first_word] = other.m_data[first_word] & start_mask;
for (auto word = first_word + 1; word < last_word; word++) {
m_data[word] = other.m_data[word];
}
m_data[last_word] = other.m_data[last_word] & end_mask;
}
[[nodiscard]] constexpr bool Get(size_t index) const {
return (m_data[index / BITS_PER_WORD] & (uint64_t {1} << (index % BITS_PER_WORD))) != 0;
}
constexpr void Set(size_t index) {
m_data[index / BITS_PER_WORD] |= uint64_t {1} << (index % BITS_PER_WORD);
}
constexpr void Unset(size_t index) {
m_data[index / BITS_PER_WORD] &= ~(uint64_t {1} << (index % BITS_PER_WORD));
}
constexpr void SetRange(size_t start, size_t end) {
if (start >= end || end > N) {
return;
}
const auto first_word = start / BITS_PER_WORD;
const auto last_word = (end - 1) / BITS_PER_WORD;
const auto start_bit = start % BITS_PER_WORD;
const auto end_bit = (end - 1) % BITS_PER_WORD;
const auto start_mask = ~uint64_t {0} << start_bit;
const auto end_mask =
end_bit == BITS_PER_WORD - 1 ? ~uint64_t {0} : (uint64_t {1} << (end_bit + 1)) - 1;
if (first_word == last_word) {
m_data[first_word] |= start_mask & end_mask;
return;
}
m_data[first_word] |= start_mask;
for (auto word = first_word + 1; word < last_word; word++) {
m_data[word] = ~uint64_t {0};
}
m_data[last_word] |= end_mask;
}
constexpr void UnsetRange(size_t start, size_t end) {
if (start >= end || end > N) {
return;
}
const auto first_word = start / BITS_PER_WORD;
const auto last_word = (end - 1) / BITS_PER_WORD;
const auto start_bit = start % BITS_PER_WORD;
const auto end_bit = (end - 1) % BITS_PER_WORD;
const auto start_mask = (uint64_t {1} << start_bit) - 1;
const auto end_mask =
end_bit == BITS_PER_WORD - 1 ? uint64_t {0} : ~((uint64_t {1} << (end_bit + 1)) - 1);
if (first_word == last_word) {
m_data[first_word] &= start_mask | end_mask;
return;
}
m_data[first_word] &= start_mask;
for (auto word = first_word + 1; word < last_word; word++) {
m_data[word] = 0;
}
m_data[last_word] &= end_mask;
}
constexpr void Clear() { m_data.fill(0); }
constexpr void Fill() { m_data.fill(~uint64_t {0}); }
[[nodiscard]] constexpr bool None() const {
uint64_t combined = 0;
for (const auto word: m_data) {
combined |= word;
}
return combined == 0;
}
[[nodiscard]] constexpr bool Any() const { return !None(); }
[[nodiscard]] constexpr Range FirstRangeFrom(size_t start) const {
if (start >= N) {
return {N, N};
}
auto word_index = start / BITS_PER_WORD;
auto word = m_data[word_index] & (~uint64_t {0} << (start % BITS_PER_WORD));
while (word == 0) {
word_index++;
if (word_index == WORD_COUNT) {
return {N, N};
}
word = m_data[word_index];
}
const auto first = word_index * BITS_PER_WORD + std::countr_zero(word);
const auto first_bit = first % BITS_PER_WORD;
const auto first_ones =
static_cast<size_t>(std::countr_one(m_data[word_index] >> first_bit));
if (first_bit + first_ones < BITS_PER_WORD) {
return {first, first + first_ones};
}
for (word_index++; word_index < WORD_COUNT; word_index++) {
word = m_data[word_index];
if (word != ~uint64_t {0}) {
return {first, word_index * BITS_PER_WORD + std::countr_one(word)};
}
}
return {first, N};
}
[[nodiscard]] constexpr Range FirstRange() const { return FirstRangeFrom(0); }
[[nodiscard]] constexpr Range LastRangeFrom(size_t end) const {
if (end == 0) {
return {0, 0};
}
if (end > N) {
end = N;
}
auto word_index = (end - 1) / BITS_PER_WORD;
const auto end_bit = (end - 1) % BITS_PER_WORD;
const auto end_mask =
end_bit == BITS_PER_WORD - 1 ? ~uint64_t {0} : (uint64_t {1} << (end_bit + 1)) - 1;
auto word = m_data[word_index] & end_mask;
while (word == 0) {
if (word_index == 0) {
return {0, 0};
}
word = m_data[--word_index];
}
const auto empty_bits = static_cast<size_t>(std::countl_zero(word));
const auto ones = static_cast<size_t>(std::countl_one(word << empty_bits));
const auto last = (word_index + 1) * BITS_PER_WORD - empty_bits;
if (empty_bits + ones < BITS_PER_WORD) {
return {last - ones, last};
}
while (word_index != 0) {
word = m_data[--word_index];
if (word != ~uint64_t {0}) {
return {(word_index + 1) * BITS_PER_WORD - std::countl_one(word), last};
}
}
return {0, last};
}
[[nodiscard]] constexpr Range LastRange() const { return LastRangeFrom(N); }
[[nodiscard]] const_iterator begin() const { return Iterator(*this, 0); }
[[nodiscard]] const_iterator end() const { return Iterator(*this, N); }
constexpr BitArray& operator^=(const BitArray& other) {
for (size_t word = 0; word < WORD_COUNT; word++) {
m_data[word] ^= other.m_data[word];
}
return *this;
}
[[nodiscard]] constexpr BitArray operator^(const BitArray& other) const {
auto result = *this;
result ^= other;
return result;
}
[[nodiscard]] constexpr BitArray operator~() const {
auto result = *this;
for (auto& word: result.m_data) {
word = ~word;
}
return result;
}
private:
std::array<uint64_t, WORD_COUNT> m_data {};
};
} // namespace Common
#endif // EMULATOR_SRC_COMMON_BITARRAY_H_
-4
View File
@@ -89,10 +89,6 @@ bool RenderDocEnabled() {
return g_config->renderdoc_enabled;
}
bool NggRectlistDrawEnabled() {
return g_config->ngg_rectlist_draw_enabled;
}
bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
-2
View File
@@ -35,7 +35,6 @@ struct ConfigOptions {
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool spirv_debug_printf_enabled = false;
bool renderdoc_enabled = false;
bool ngg_rectlist_draw_enabled = true;
bool readback_linear_images = false;
};
@@ -64,7 +63,6 @@ ProfilerDirection GetProfilerDirection();
bool SpirvDebugPrintfEnabled();
bool RenderDocEnabled();
bool NggRectlistDrawEnabled();
bool ReadbackLinearImagesEnabled();
} // namespace Config
@@ -83,8 +83,7 @@ public:
uint32_t first_instance = 0);
void DrawIndexOffset(uint32_t index_offset, uint32_t index_count, uint32_t flags);
void DrawIndexAuto(uint32_t index_count, uint32_t flags,
uint32_t render_target_slice_offset = 0, uint32_t instance_count = 1,
uint32_t first_vertex = 0, uint32_t first_instance = 0);
uint32_t render_target_slice_offset = 0);
void DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed);
void DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr, uint32_t stride_in_bytes,
@@ -152,6 +151,9 @@ private:
uint32_t interrupt_context_id);
void ProcessPm4(Pm4Execution& execution, size_t stop_depth);
void SuspendPm4();
void SubmitNonIndexedDraw(uint32_t vertex_count, uint32_t flags,
uint32_t render_target_slice_offset, uint32_t first_vertex,
uint32_t first_instance);
CommandScheduler& GetScheduler() const { return m_renderer.GetCommandScheduler(); }
RenderCommandBuffer& CurrentBuffer() { return GetScheduler().Current(); }
@@ -168,7 +170,8 @@ private:
uint64_t m_index_base_addr = 0;
uint64_t m_draw_indirect_args_base_addr = 0;
uint64_t m_dispatch_indirect_args_base_addr = 0;
uint32_t m_num_instances = 1;
// Persistent draw state: indirect draws update it for subsequent draws.
uint32_t m_num_instances = 1;
uint32_t m_de_count = 0;
uint32_t m_ce_count = 0;
@@ -1917,17 +1917,23 @@ KYTY_CP_OP_PARSER(CpOpCopyData) {
EXIT_NOT_IMPLEMENTED(cmd_id != KYTY_PM4(6, Pm4::IT_COPY_DATA, 0u));
const uint32_t control = buffer[0];
const uint32_t src_sel = ((control & 0xfu) << 1u) | ((control >> 30u) & 0x1u);
const uint32_t dst_sel = ((control >> 8u) & 0xfu) << 1u;
const uint8_t src_cache = static_cast<uint8_t>((control >> 13u) & 0x3u);
const uint8_t dst_cache = static_cast<uint8_t>((control >> 25u) & 0x3u);
const uint8_t write_confirm = static_cast<uint8_t>((control >> 20u) & 0x1u);
const uint32_t num_bytes = ((control >> 16u) & 0x1u) != 0 ? 8u : 4u;
const uint64_t src = buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u);
const uint64_t dst = buffer[3] | (static_cast<uint64_t>(buffer[4]) << 32u);
if (src_sel == (9u << 1u)) {
if (dst_sel != (2u << 1u) || dst == 0 || (dst & (num_bytes - 1u)) != 0) {
const uint32_t control = buffer[0];
const uint32_t src_sel = ((control & 0xfu) << 1u) | ((control >> 30u) & 0x1u);
const uint32_t dst_sel = ((control >> 8u) & 0xfu) << 1u;
const uint8_t src_cache = static_cast<uint8_t>((control >> 13u) & 0x3u);
const uint8_t dst_cache = static_cast<uint8_t>((control >> 25u) & 0x3u);
const uint8_t write_confirm = static_cast<uint8_t>((control >> 20u) & 0x1u);
const uint32_t num_bytes = ((control >> 16u) & 0x1u) != 0 ? 8u : 4u;
const uint64_t src = buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u);
const uint64_t dst = buffer[3] | (static_cast<uint64_t>(buffer[4]) << 32u);
uint32_t reference_clock_dst = 0;
switch (src_sel) {
case 9u: reference_clock_dst = 2u; break;
case 18u: reference_clock_dst = 4u; break;
default: break;
}
if (reference_clock_dst != 0) {
if (dst_sel != reference_clock_dst || dst == 0 || (dst & (num_bytes - 1u)) != 0) {
EXIT("unsupported reference-clock copyData, src_sel=0x%02" PRIx32
" dst_sel=0x%02" PRIx32 " dst=0x%016" PRIx64 " size=%u\n",
src_sel, dst_sel, dst, num_bytes);
@@ -3390,6 +3396,12 @@ void GraphicsInitJmpTablesCxIndirect() {
g_hw_ctx_indirect_func[Pm4::DB_COUNT_CONTROL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDepthMetadataRegister(cmd_offset, value);
};
for (auto cmd_offset = Pm4::DB_SRESULTS_COMPARE_STATE0;
cmd_offset <= Pm4::DB_SRESULTS_COMPARE_STATE1; cmd_offset++) {
g_hw_ctx_indirect_func[cmd_offset] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDepthMetadataRegister(cmd_offset, value);
};
}
g_hw_ctx_indirect_func[Pm4::DB_RENDER_OVERRIDE] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDepthMetadataRegister(cmd_offset, value);
};
+1
View File
@@ -72,6 +72,7 @@ enum class ChannelLayout : uint32_t {
k32_32 = 11,
k16_16_16_16 = 12,
k32_32_32_32 = 14,
k5_6_5 = 16,
k5_5_5_1 = 17,
k4_4_4_4 = 19,
kBc1 = 35,
+1
View File
@@ -39,6 +39,7 @@ constexpr FormatInfo kFormatInfo[] = {
{GpuEnumValue(BufferFormat::k16_16Float), 4, 0, 4, true, false},
{GpuEnumValue(BufferFormat::k11_11_10Float), 4, 0, 4, true, false},
{GpuEnumValue(BufferFormat::k10_10_10_2UNorm), 4, 0, 4, true, false},
{GpuEnumValue(BufferFormat::k10_10_10_2UInt), 4, 0, 4, true, true},
{GpuEnumValue(BufferFormat::k8_8_8_8UNorm), 4, 0, 4, true, false},
{GpuEnumValue(BufferFormat::k8_8_8_8SNorm), 4, 0, 4, true, false},
{GpuEnumValue(BufferFormat::k8_8_8_8UInt), 4, 0, 4, true, true},
+28 -71
View File
@@ -32,11 +32,10 @@
namespace Libs::Graphics {
static thread_local CommandProcessor* g_current_processor = nullptr;
static thread_local Pm4Execution* g_current_execution = nullptr;
static thread_local uint32_t g_submission_pause_depth = 0;
static thread_local bool g_gpu_mutex_owned = false;
static thread_local bool g_gpu_thread = false;
static thread_local CommandProcessor* g_current_processor = nullptr;
static thread_local Pm4Execution* g_current_execution = nullptr;
static thread_local bool g_gpu_mutex_owned = false;
static thread_local bool g_gpu_thread = false;
class GpuMutexLock final {
public:
@@ -98,8 +97,6 @@ public:
bool trigger_agc_interrupt_on_done);
void SubmitFlipPreparation(uint64_t request_id);
void Done();
void PauseSubmissions();
void ResumeSubmissions();
void Shutdown();
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
@@ -410,8 +407,13 @@ void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw
const uint32_t increment = (write_control >> 16u) & 0x1u;
const uint32_t write_confirm = (write_control >> 20u) & 0x1u;
if (dst_sel != 0 && dst_sel != 2 && dst_sel != 4 && dst_sel != 5) {
EXIT("unsupported writeData destination selector 0x%02" PRIx32 "\n", dst_sel);
switch (dst_sel) {
case 0:
case 2:
case 4:
case 5:
case 6: break;
default: EXIT("unsupported writeData destination selector 0x%02" PRIx32 "\n", dst_sel);
}
EXIT_NOT_IMPLEMENTED(increment != 0);
@@ -691,26 +693,6 @@ bool GpuState::Process(Submission& submission) {
return complete;
}
void GpuState::PauseSubmissions() {
if (g_gpu_mutex_owned) {
EXIT("GPU submissions are already paused by this thread\n");
}
g_gpu_mutex_owned = true;
m_submission_mutex.Lock();
if (!IsGpuThread()) {
WaitLocked();
}
m_renderer.GetCommandScheduler().DrainPriorityOperations();
}
void GpuState::ResumeSubmissions() {
if (!g_gpu_mutex_owned) {
EXIT("GPU submissions resumed without an active pause\n");
}
m_submission_mutex.Unlock();
g_gpu_mutex_owned = false;
}
Pm4ProcessResult CommandProcessor::Process(Pm4Execution& execution, uint32_t* buffer,
uint32_t size_dw) {
KYTY_PROFILER_BLOCK("CommandProcessor::Process");
@@ -1001,8 +983,9 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato
args.start_vertex_location, args.start_instance_location);
}
}
DrawIndexAuto(args.vertex_count_per_instance, 0, 0, args.instance_count,
args.start_vertex_location, args.start_instance_location);
m_num_instances = args.instance_count;
SubmitNonIndexedDraw(args.vertex_count_per_instance, 0, 0, args.start_vertex_location,
args.start_instance_location);
return;
}
@@ -1042,6 +1025,7 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato
}
}
m_num_instances = args.instance_count;
DrawIndex(index_count, index_addr, 0, 1, args.instance_count, nullptr, 0,
static_cast<int32_t>(args.base_vertex_location), args.start_instance_location);
}
@@ -1099,8 +1083,9 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
args->start_vertex_location, args->start_instance_location);
}
}
DrawIndexAuto(args->vertex_count_per_instance, 0, 0, args->instance_count,
args->start_vertex_location, args->start_instance_location);
m_num_instances = args->instance_count;
SubmitNonIndexedDraw(args->vertex_count_per_instance, 0, 0, args->start_vertex_location,
args->start_instance_location);
continue;
}
@@ -1141,6 +1126,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
}
}
m_num_instances = args->instance_count;
DrawIndex(index_count, index_addr, 0, 1, args->instance_count, nullptr, 0,
static_cast<int32_t>(args->base_vertex_location), args->start_instance_location);
}
@@ -1232,12 +1218,17 @@ void CommandProcessor::DispatchIndirect(uint32_t data_offset, uint32_t mode) {
}
void CommandProcessor::DrawIndexAuto(uint32_t index_count, uint32_t flags,
uint32_t render_target_slice_offset, uint32_t instance_count,
uint32_t first_vertex, uint32_t first_instance) {
uint32_t render_target_slice_offset) {
SubmitNonIndexedDraw(index_count, flags, render_target_slice_offset, 0, 0);
}
void CommandProcessor::SubmitNonIndexedDraw(uint32_t vertex_count, uint32_t flags,
uint32_t render_target_slice_offset,
uint32_t first_vertex, uint32_t first_instance) {
CheckBuffer();
m_renderer.GetRenderExecutor().DrawAuto(m_submit_id, CurrentBuffer(), index_count, flags,
render_target_slice_offset, instance_count,
m_renderer.GetRenderExecutor().DrawAuto(m_submit_id, CurrentBuffer(), vertex_count, flags,
render_target_slice_offset, m_num_instances,
first_vertex, first_instance);
}
@@ -1688,32 +1679,6 @@ int Gpu::GetFrameNum() const {
return m_state->GetFrameNum();
}
void Gpu::PauseSubmissions() {
m_state->PauseSubmissions();
}
void Gpu::ResumeSubmissions() {
m_state->ResumeSubmissions();
}
Gpu::SubmissionLock::SubmissionLock(Gpu& gpu): m_gpu(gpu) {
if (g_current_processor != nullptr || g_submission_pause_depth == UINT32_MAX) {
EXIT("cannot acquire GPU submission lock in the current state\n");
}
if (g_submission_pause_depth++ == 0) {
m_gpu.PauseSubmissions();
}
}
Gpu::SubmissionLock::~SubmissionLock() {
if (g_submission_pause_depth == 0) {
EXIT("GPU submission lock released without ownership\n");
}
if (--g_submission_pause_depth == 0) {
m_gpu.ResumeSubmissions();
}
}
bool Gpu::IsCommandProcessorThread() noexcept {
return g_current_processor != nullptr;
}
@@ -1722,12 +1687,4 @@ CommandProcessor* Gpu::CurrentCommandProcessor() noexcept {
return g_current_processor;
}
bool Gpu::SubmissionLockHeld() noexcept {
return g_submission_pause_depth != 0;
}
bool Gpu::MutexHeld() noexcept {
return g_gpu_mutex_owned;
}
} // namespace Libs::Graphics
-17
View File
@@ -35,25 +35,8 @@ public:
[[nodiscard]] static bool IsCommandProcessorThread() noexcept;
[[nodiscard]] static CommandProcessor* CurrentCommandProcessor() noexcept;
[[nodiscard]] static bool SubmissionLockHeld() noexcept;
[[nodiscard]] static bool MutexHeld() noexcept;
class SubmissionLock final {
public:
explicit SubmissionLock(Gpu& gpu);
~SubmissionLock();
KYTY_CLASS_NO_COPY(SubmissionLock);
private:
Gpu& m_gpu;
};
private:
friend class SubmissionLock;
void PauseSubmissions();
void ResumeSubmissions();
std::unique_ptr<GpuState> m_state;
};
} // namespace Libs::Graphics
+3 -1
View File
@@ -110,7 +110,6 @@ void DumpPm4PacketStream(Common::File* file, uint32_t* cmd_buffer, uint32_t star
auto* cmd = cmd_buffer + start_dw;
auto dw = num_dw;
while (dw != 0) {
EXIT_NOT_IMPLEMENTED(dw < 2);
EXIT_NOT_IMPLEMENTED(dw > num_dw);
auto cmd_id = *cmd++;
@@ -120,6 +119,9 @@ void DumpPm4PacketStream(Common::File* file, uint32_t* cmd_buffer, uint32_t star
uint32_t len = 0;
const auto packet_type = static_cast<PacketType>(cmd_id >> 30u);
// Type-2 packets are header-only padding; every other packet type requires a body.
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2);
switch (packet_type) {
case PacketType::Type3: {
const bool sh_gx = (cmd_id & 0x2u) == 0;
+9 -114
View File
@@ -6,13 +6,7 @@ namespace Libs::Graphics {
static_assert(std::atomic<void*>::is_always_lock_free);
MemoryTracker::MemoryTracker(PageManager& page_manager, PageWatchMode gpu_watch_mode)
: m_page_manager(page_manager), m_gpu_watch_mode(gpu_watch_mode) {
switch (m_gpu_watch_mode) {
case PageWatchMode::Write:
case PageWatchMode::ReadWrite: break;
default: EXIT("unsupported memory tracker GPU page-watch mode\n");
}
MemoryTracker::MemoryTracker(PageManager& page_manager): m_page_manager(page_manager) {
m_regions = std::make_unique<std::atomic<RegionManager*>[]>(REGION_COUNT);
for (size_t i = 0; i < REGION_COUNT; i++) {
m_regions[i].store(nullptr, std::memory_order_relaxed);
@@ -21,6 +15,7 @@ MemoryTracker::MemoryTracker(PageManager& page_manager, PageWatchMode gpu_watch_
MemoryTracker::~MemoryTracker() = default;
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void MemoryTracker::ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
const char* operation) const noexcept {
if (vaddr == 0 || size == 0 || size > UINT64_MAX - vaddr ||
@@ -58,6 +53,7 @@ void MemoryTracker::ValidateGpuDirtyOwnership(const RangeSet& dirty, uint64_t va
}
}
}
#endif
void MemoryTracker::ValidateRange(uint64_t vaddr, uint64_t size) {
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
@@ -104,34 +100,25 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
std::lock_guard access(m_access_mutex);
Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
const auto changed =
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
manager->ApplyProtection(changed, false);
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
}
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
CheckNotInUploadCallback();
std::lock_guard access(m_access_mutex);
Iterate<true>(vaddr, size, [this](RegionManager* manager, uint64_t offset, uint64_t bytes) {
Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
const auto changed =
manager->ChangeState<DirtySource::Gpu, true>(manager->GetCpuAddr() + offset, bytes);
manager->ApplyGpuProtection(changed, true, m_gpu_watch_mode);
manager->ChangeState<DirtySource::Gpu, true>(manager->GetCpuAddr() + offset, bytes);
});
}
void MemoryTracker::UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
CheckNotInUploadCallback();
std::lock_guard access(m_access_mutex);
Iterate<true>(vaddr, size, [this](RegionManager* manager, uint64_t offset, uint64_t bytes) {
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
if (!manager->IsFullyModified<DirtySource::Gpu>(offset, bytes)) {
EXIT("cannot clear partially GPU-dirty tracking range\n");
}
const auto changed =
manager->ChangeState<DirtySource::Gpu, false>(manager->GetCpuAddr() + offset, bytes);
manager->ApplyGpuProtection(changed, false, m_gpu_watch_mode);
manager->ChangeState<DirtySource::Gpu, false>(manager->GetCpuAddr() + offset, bytes);
});
}
@@ -154,10 +141,7 @@ void MemoryTracker::UntrackMemoryLocked(uint64_t vaddr, uint64_t size) {
EXIT("cannot untrack GPU-dirty memory\n");
}
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const auto changed =
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
manager->ApplyProtection(changed, false);
manager->Untrack(manager->GetCpuAddr() + offset, bytes);
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
locks.clear();
}
@@ -168,93 +152,4 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
UntrackMemoryLocked(vaddr, size);
}
bool MemoryTracker::InvalidateRegion(uint64_t vaddr, uint64_t size, PageFaultPhase phase) noexcept {
switch (phase) {
case PageFaultPhase::Release: return true;
case PageFaultPhase::Invalidate: {
const auto action = BeginCpuFault(vaddr, size);
switch (action) {
case CpuFaultAction::Untracked: return false;
case CpuFaultAction::Continue: return true;
case CpuFaultAction::Download:
EXIT("generic region invalidation cannot download GPU-dirty memory\n");
}
}
case PageFaultPhase::Complete:
return CompleteCpuFault(vaddr, size, PageFaultAccess::Write, false);
}
EXIT("unsupported region invalidation phase\n");
}
bool MemoryTracker::InvalidateVirtualGpuWrite(PageFaultAccess access, uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept {
switch (phase) {
case PageFaultPhase::Release: return true;
case PageFaultPhase::Invalidate: {
const bool gpu_modified = Iterate<false>(
vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
return manager->IsModified<DirtySource::Gpu>(offset, bytes);
});
if (!gpu_modified) {
return false;
}
const auto action = BeginCpuFault(vaddr, size);
if (access != PageFaultAccess::Write || action != CpuFaultAction::Download) {
EXIT("virtual GPU write fault requires write access to GPU-dirty memory\n");
}
return true;
}
case PageFaultPhase::Complete: {
if (access != PageFaultAccess::Write) {
EXIT("virtual GPU write completion requires write access\n");
}
bool completed = false;
Iterate<false>(
vaddr, size, [&completed](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
if (completed) {
EXIT("virtual GPU write fault spans multiple tracked regions\n");
}
completed =
manager->CompleteVirtualGpuWrite(manager->GetCpuAddr() + offset, bytes);
});
return completed;
}
}
EXIT("unsupported virtual GPU write invalidation phase\n");
}
CpuFaultAction MemoryTracker::BeginCpuFault(uint64_t vaddr, uint64_t size,
PageFaultAccess access) noexcept {
CheckNotInUploadCallback();
CpuFaultAction action = CpuFaultAction::Untracked;
Iterate<false>(
vaddr, size, [&action, access](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
if (action != CpuFaultAction::Untracked) {
EXIT("CPU fault spans multiple tracked regions\n");
}
action = manager->BeginCpuFault(manager->GetCpuAddr() + offset, bytes, access);
});
return action;
}
bool MemoryTracker::CompleteCpuFault(uint64_t vaddr, uint64_t size, PageFaultAccess access,
bool downloaded) noexcept {
CheckNotInUploadCallback();
bool found = false;
Iterate<false>(
vaddr, size,
[&found, access, downloaded](RegionManager* manager, uint64_t offset, uint64_t bytes) {
std::scoped_lock lock(manager->lock);
if (found) {
EXIT("CPU fault completion spans multiple tracked regions\n");
}
found = manager->CompleteCpuFault(manager->GetCpuAddr() + offset, bytes, access,
downloaded);
});
return found;
}
} // namespace Libs::Graphics
+17 -31
View File
@@ -18,8 +18,7 @@ namespace Libs::Graphics {
class MemoryTracker final {
public:
explicit MemoryTracker(PageManager& page_manager,
PageWatchMode gpu_watch_mode = PageWatchMode::ReadWrite);
explicit MemoryTracker(PageManager& page_manager);
~MemoryTracker();
KYTY_CLASS_NO_COPY(MemoryTracker);
@@ -30,13 +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);
[[nodiscard]] CpuFaultAction
BeginCpuFault(uint64_t vaddr, uint64_t size,
PageFaultAccess access = PageFaultAccess::Write) noexcept;
[[nodiscard]] bool CompleteCpuFault(uint64_t vaddr, uint64_t size, PageFaultAccess access,
bool downloaded) noexcept;
[[nodiscard]] bool InvalidateRegion(uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept;
template <typename Flush>
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) {
static_assert(std::is_invocable_v<Flush&>);
@@ -63,9 +55,8 @@ public:
}
Iterate<false>(vaddr, size,
[](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const auto changed = manager->ChangeState<DirtySource::Cpu, true>(
manager->ChangeState<DirtySource::Cpu, true>(
manager->GetCpuAddr() + offset, bytes);
manager->ApplyProtection(changed, false);
});
return false;
};
@@ -78,12 +69,15 @@ public:
EXIT("memory invalidation retained GPU-owned pages\n");
}
}
[[nodiscard]] bool InvalidateVirtualGpuWrite(PageFaultAccess access, uint64_t vaddr,
uint64_t size, PageFaultPhase phase) noexcept;
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
const char* operation) const noexcept;
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
const char* operation) const noexcept;
void ValidateGpuDirtyOwnership(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
const char* operation);
#else
void ValidateGpuDirtyPages(const RangeSet&, uint64_t, uint64_t, const char*) const noexcept {}
void ValidateGpuDirtyOwnership(const RangeSet&, uint64_t, uint64_t, const char*) {}
#endif
template <bool clear, typename Preflight, typename Func>
void ForEachDownloadRange(uint64_t vaddr, uint64_t size, Preflight&& preflight, Func&& func) {
@@ -102,9 +96,6 @@ public:
}
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const auto address = manager->GetCpuAddr() + offset;
if (manager->HasPendingFault(address, bytes)) {
EXIT("GPU download synchronization raced a pending CPU fault\n");
}
manager->template ForEachModifiedRange<DirtySource::Gpu, false>(address, bytes,
preflight);
});
@@ -116,10 +107,8 @@ public:
Iterate<false>(vaddr, size,
[&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const auto address = manager->GetCpuAddr() + offset;
const auto changed =
manager->template ForEachModifiedRange<DirtySource::Gpu, true>(
address, bytes, [](uint64_t, uint64_t) noexcept {});
manager->ApplyGpuProtection(changed, false, m_gpu_watch_mode);
manager->template ForEachModifiedRange<DirtySource::Gpu, true>(
address, bytes, [](uint64_t, uint64_t) noexcept {});
});
}
}
@@ -141,7 +130,6 @@ public:
const auto* previous_upload_owner = std::exchange(s_upload_owner, this);
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->lock.lock();
manager->Track(manager->GetCpuAddr() + offset, bytes);
manager->ForEachModifiedRange<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset,
bytes, range_func);
if (!is_written) {
@@ -150,13 +138,12 @@ public:
});
upload_func();
if (is_written) {
Iterate<false>(
vaddr, size, [this](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const auto changed = manager->template ChangeState<DirtySource::Gpu, true>(
manager->GetCpuAddr() + offset, bytes);
manager->ApplyGpuProtection(changed, true, m_gpu_watch_mode);
manager->lock.unlock();
});
Iterate<false>(vaddr, size,
[](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->template ChangeState<DirtySource::Gpu, true>(
manager->GetCpuAddr() + offset, bytes);
manager->lock.unlock();
});
}
s_upload_owner = previous_upload_owner;
}
@@ -210,7 +197,6 @@ private:
std::mutex m_region_mutex;
std::mutex m_access_mutex;
PageManager& m_page_manager;
PageWatchMode m_gpu_watch_mode = PageWatchMode::ReadWrite;
};
} // namespace Libs::Graphics
+154 -489
View File
@@ -9,7 +9,6 @@
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <memory>
#include <mutex>
#include <vector>
@@ -22,13 +21,9 @@
#undef min
#undef max
#elif defined(__APPLE__)
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>
#else
#include <execinfo.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#endif
@@ -41,9 +36,8 @@ constexpr uint64_t ADDRESS_SIZE = TRACKER_ADDRESS_SIZE;
constexpr uint64_t REGION_COUNT = ADDRESS_SIZE / REGION_SIZE;
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
// The tracker reuses Win32 memory-protection tags as internal page-state values (on
// Windows they come from <windows.h> and are what VirtualQuery returns). Mirror the
// canonical Win32 numeric values so the shared state-machine logic is identical.
// The tracker reuses Win32 memory-protection tags as internal page-state values.
// Mirror their canonical numeric values so the shared state-machine logic is identical.
constexpr uint32_t PAGE_NOACCESS = 0x01;
constexpr uint32_t PAGE_READONLY = 0x02;
constexpr uint32_t PAGE_READWRITE = 0x04;
@@ -53,10 +47,6 @@ constexpr uint64_t REGION_PAGES = REGION_SIZE / PAGE_SIZE;
constexpr uint32_t NO_ACCESS_PROTECTION = PAGE_NOACCESS;
constexpr uint32_t READ_ONLY_PROTECTION = PAGE_READONLY;
constexpr uint32_t READ_WRITE_PROTECTION = PAGE_READWRITE;
// Zero is the unknown protection sentinel.
constexpr uint32_t UNKNOWN_PROTECTION = 0;
thread_local bool g_in_fault_resolution = false;
[[noreturn]] void FailFast(const char* reason = nullptr) noexcept {
std::fputs("PageManager fail-fast: ", stderr);
@@ -104,25 +94,6 @@ Common::VirtualMemory::Mode ToMemoryMode(uint32_t protection) {
}
}
uint32_t CurrentThread() noexcept {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
return GetCurrentThreadId();
#elif defined(__APPLE__)
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
#elif defined(__linux__)
static thread_local const uint32_t tid = [] {
const auto raw = static_cast<uint32_t>(::syscall(SYS_gettid));
if (raw == 0) {
FailFast("gettid returned the reserved zero owner token");
}
return raw;
}();
return tid;
#else
FailFast("page tracking thread identity is unsupported on this platform");
#endif
}
class SpinGuard final {
public:
explicit SpinGuard(std::atomic_flag& lock): m_lock(lock) {
@@ -156,47 +127,61 @@ uint64_t PageEnd(uint64_t vaddr, uint64_t size) {
struct PageManager::Impl {
struct PageState {
std::atomic_flag lock = ATOMIC_FLAG_INIT;
uint32_t write_watchers = 0;
uint32_t access_watchers = 0;
uint32_t original_protection = 0;
uint32_t backing_writer = 0;
// Shadow the protection applied through Protect().
uint32_t current_protection = UNKNOWN_PROTECTION;
bool resolving = false;
bool resolving_read_write = false;
bool late_read_pending = false;
bool late_write_pending = false;
};
uint8_t write_watchers : 7 = 0;
uint8_t access_watchers : 1 = 0;
struct Region {
std::array<PageState, REGION_PAGES> pages;
};
[[nodiscard]] uint32_t Perms() const noexcept {
if (access_watchers != 0) {
return NO_ACCESS_PROTECTION;
}
if (write_watchers != 0) {
return READ_ONLY_PROTECTION;
}
return READ_WRITE_PROTECTION;
}
class PageRangeGuard final {
public:
explicit PageRangeGuard(std::span<PageState*> pages): m_pages(pages) {
for (auto* page: m_pages) {
while (page->lock.test_and_set(std::memory_order_acquire)) {
std::atomic_signal_fence(std::memory_order_seq_cst);
template <int delta, bool is_read>
uint32_t AddDelta(uint64_t address) {
static_assert(delta >= -1 && delta <= 1);
if constexpr (is_read) {
if constexpr (delta == 1) {
if (access_watchers != 0) {
Fatal("read-watcher overflow at 0x%016" PRIx64, address);
}
return ++access_watchers;
} else if constexpr (delta == -1) {
if (access_watchers == 0) {
Fatal("read-watcher underflow at 0x%016" PRIx64, address);
}
return --access_watchers;
} else {
return access_watchers;
}
} else {
if constexpr (delta == 1) {
if (write_watchers == 0x7f) {
Fatal("write-watcher overflow at 0x%016" PRIx64, address);
}
return ++write_watchers;
} else if constexpr (delta == -1) {
if (write_watchers == 0) {
Fatal("write-watcher underflow at 0x%016" PRIx64, address);
}
return --write_watchers;
} else {
return write_watchers;
}
}
}
~PageRangeGuard() {
for (auto it = m_pages.rbegin(); it != m_pages.rend(); ++it) {
(*it)->lock.clear(std::memory_order_release);
}
}
KYTY_CLASS_NO_COPY(PageRangeGuard);
};
static_assert(sizeof(PageState) == 1);
private:
std::span<PageState*> m_pages;
struct Region {
std::atomic_flag lock = ATOMIC_FLAG_INIT;
std::array<PageState, REGION_PAGES> pages;
};
Impl(PageFaultHandler handler, void* context): fault_handler(handler), fault_context(context) {
if (fault_handler == nullptr) {
Fatal("null page-manager fault callback");
}
Impl() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
SYSTEM_INFO info {};
GetSystemInfo(&info);
@@ -223,10 +208,9 @@ struct PageManager::Impl {
~Impl() {
for (const auto& region: region_storage) {
SpinGuard lock(region->lock);
for (auto& page: region->pages) {
SpinGuard lock(page.lock);
if (page.write_watchers != 0 || page.access_watchers != 0 ||
page.backing_writer != 0 || page.resolving) {
if (page.write_watchers != 0 || page.access_watchers != 0) {
FailFast("PageManager destroyed with live page state");
}
}
@@ -254,459 +238,140 @@ struct PageManager::Impl {
return ptr;
}
PageState& GetPage(Region& region, uint64_t vaddr) const {
return region.pages[(vaddr % REGION_SIZE) / PAGE_SIZE];
}
static uint32_t WatcherProtection(const PageState& page) {
if (page.access_watchers != 0) {
return NO_ACCESS_PROTECTION;
}
if (page.write_watchers != 0) {
return READ_ONLY_PROTECTION;
}
return page.original_protection;
}
static void PublishDelayedFaults(PageState& page, uint32_t old_protection,
uint32_t new_protection) {
if (old_protection == NO_ACCESS_PROTECTION && new_protection != NO_ACCESS_PROTECTION) {
page.late_read_pending = true;
}
if ((old_protection == NO_ACCESS_PROTECTION || old_protection == READ_ONLY_PROTECTION) &&
new_protection == READ_WRITE_PROTECTION) {
page.late_write_pending = true;
}
}
static void InitializeProtection(std::span<PageState*> pages) {
for (auto* page: pages) {
page->original_protection = READ_WRITE_PROTECTION;
page->current_protection = READ_WRITE_PROTECTION;
}
}
static bool AllowsAccess(const PageState& page, [[maybe_unused]] uint64_t vaddr,
PageFaultAccess access) noexcept {
switch (access) {
case PageFaultAccess::Read:
return page.current_protection == READ_ONLY_PROTECTION ||
page.current_protection == READ_WRITE_PROTECTION;
case PageFaultAccess::Write: return page.current_protection == READ_WRITE_PROTECTION;
default: return false;
}
}
void ProtectRange(std::span<PageState*> pages, uint64_t vaddr, uint32_t protection,
std::span<const uint32_t> expected_old, bool fault_path) noexcept {
const auto size = pages.size() * PAGE_SIZE;
if (pages.size() != expected_old.size()) {
FailFast("protection range state size mismatch");
}
for (size_t i = 0; i < pages.size(); i++) {
const auto actual = pages[i]->current_protection;
if (actual != UNKNOWN_PROTECTION && actual != expected_old[i]) {
if (fault_path) {
FailFast("mprotect fault transition did not match expected protection");
}
Fatal("invalid protection transition at 0x%016" PRIx64 ", old=0x%08" PRIx32
", expected=0x%08" PRIx32 ", new=0x%08" PRIx32,
vaddr + i * PAGE_SIZE, actual, expected_old[i], protection);
}
}
void Protect(uint64_t vaddr, uint64_t size, uint32_t protection) noexcept {
if (!Libs::LibKernel::Memory::ProtectGuestHostMemory(vaddr, size,
ToMemoryMode(protection))) {
if (fault_path) {
FailFast("address-space fault protection transition failed");
}
Fatal("address-space protection failed at 0x%016" PRIx64 ", new=0x%08" PRIx32, vaddr,
protection);
}
for (auto* page: pages) {
page->current_protection = protection;
}
}
void Protect(PageState& page, uint64_t vaddr, uint32_t protection, uint32_t expected_old,
bool fault_path) noexcept {
PageState* pages[] = {&page};
uint32_t expected[] = {expected_old};
ProtectRange(pages, vaddr, protection, expected, fault_path);
template <bool track, bool is_read, bool masked>
void UpdateRegionWatchers(Region& region, uint64_t base_addr, size_t first, size_t last,
const RegionBits* mask = nullptr) {
SpinGuard lock(region.lock);
auto perms = region.pages[first].Perms();
uint64_t range_begin = 0;
uint64_t range_bytes = 0;
uint64_t potential_range_bytes = 0;
const auto release_pending = [&] {
if (range_bytes != 0) {
Protect(base_addr + range_begin * PAGE_SIZE, range_bytes, perms);
range_bytes = 0;
potential_range_bytes = 0;
}
};
for (size_t page_index = first; page_index < last; page_index++) {
auto& page = region.pages[page_index];
const auto address = base_addr + page_index * PAGE_SIZE;
const bool update = !masked || mask->Get(page_index);
const auto old_perms = page.Perms();
const auto new_count = update ? page.AddDelta<track ? 1 : -1, is_read>(address)
: page.AddDelta<0, is_read>(address);
const auto new_perms = page.Perms();
if (new_perms != perms) [[unlikely]] {
release_pending();
perms = new_perms;
} else if (range_bytes != 0) {
potential_range_bytes += PAGE_SIZE;
}
if (!update) {
continue;
}
const bool watcher_edge = (track && new_count == 1) || (!track && new_count == 0);
if (watcher_edge && old_perms != new_perms) {
if (range_bytes == 0) {
range_begin = page_index;
potential_range_bytes = PAGE_SIZE;
}
range_bytes = potential_range_bytes;
}
}
release_pending();
}
template <bool track, bool is_read>
void UpdatePageWatchers(uint64_t vaddr, uint64_t size) {
const auto begin = PageStart(vaddr);
const auto end = PageEnd(vaddr, size);
for (auto chunk_begin = begin; chunk_begin < end;) {
const auto chunk_end = std::min(end, (chunk_begin / REGION_SIZE + 1) * REGION_SIZE);
const auto region_base = chunk_begin / REGION_SIZE * REGION_SIZE;
auto* region = track ? GetOrCreateRegion(chunk_begin) : FindRegion(chunk_begin);
if (region == nullptr) {
Fatal("untracking unknown page 0x%016" PRIx64, chunk_begin);
}
const auto first = static_cast<size_t>((chunk_begin - region_base) / PAGE_SIZE);
const auto last = static_cast<size_t>((chunk_end - region_base) / PAGE_SIZE);
UpdateRegionWatchers<track, is_read, false>(*region, region_base, first, last);
chunk_begin = chunk_end;
}
}
std::unique_ptr<std::atomic<Region*>[]> regions;
std::vector<std::unique_ptr<Region>> region_storage;
std::mutex region_mutex;
PageFaultHandler fault_handler = nullptr;
void* fault_context = nullptr;
};
static_assert(std::atomic<void*>::is_always_lock_free);
PageManager::PageManager(PageFaultHandler fault_handler, void* fault_context)
: m_impl(std::make_unique<Impl>(fault_handler, fault_context)) {}
PageManager::PageManager(): m_impl(std::make_unique<Impl>()) {}
PageManager::~PageManager() = default;
uint64_t PageManager::GetPageSize() const {
if (g_in_fault_resolution) {
FailFast("nested page fault while resolving a watched page");
}
return PAGE_SIZE;
}
bool PageManager::IsTracked(uint64_t vaddr) const noexcept {
if (g_in_fault_resolution) {
FailFast("IsTracked called during fault resolution");
template <bool track>
void PageManager::UpdatePageWatchers(uint64_t vaddr, uint64_t size) {
m_impl->UpdatePageWatchers<track, false>(vaddr, size);
}
template void PageManager::UpdatePageWatchers<true>(uint64_t, uint64_t);
template void PageManager::UpdatePageWatchers<false>(uint64_t, uint64_t);
template <bool track, bool is_read>
void PageManager::UpdatePageWatchersForRegion(uint64_t base_addr, RegionBits& mask) {
if (base_addr % REGION_SIZE != 0 || base_addr >= ADDRESS_SIZE ||
REGION_SIZE > ADDRESS_SIZE - base_addr) {
Fatal("invalid tracking region base 0x%016" PRIx64, base_addr);
}
auto* region = m_impl->FindRegion(vaddr);
const auto start_range = mask.FirstRange();
const auto end_range = mask.LastRange();
if (start_range.first == REGION_PAGES) {
FailFast("empty region watcher mask");
}
const auto first = start_range.first;
const auto last = end_range.second;
if (start_range.second == end_range.second) {
m_impl->UpdatePageWatchers<track, is_read>(base_addr + first * PAGE_SIZE,
(last - first) * PAGE_SIZE);
return;
}
auto* region = track ? m_impl->GetOrCreateRegion(base_addr) : m_impl->FindRegion(base_addr);
if (region == nullptr) {
return false;
Fatal("untracking unknown region 0x%016" PRIx64, base_addr);
}
auto& page = m_impl->GetPage(*region, vaddr);
SpinGuard lock(page.lock);
return page.write_watchers != 0 || page.access_watchers != 0;
m_impl->UpdateRegionWatchers<track, is_read, true>(*region, base_addr, first, last, &mask);
}
void PageManager::UpdatePageWatchers(bool track, uint64_t vaddr, uint64_t size,
PageWatchMode mode) {
if (mode != PageWatchMode::Write && mode != PageWatchMode::ReadWrite) {
Fatal("invalid watcher mode");
}
const auto begin = PageStart(vaddr);
const auto end = PageEnd(vaddr, size);
for (auto chunk_begin = begin; chunk_begin < end;) {
const auto chunk_end = std::min(end, (chunk_begin / REGION_SIZE + 1) * REGION_SIZE);
auto* region =
track ? m_impl->GetOrCreateRegion(chunk_begin) : m_impl->FindRegion(chunk_begin);
if (region == nullptr) {
Fatal("untracking unknown page 0x%016" PRIx64, chunk_begin);
}
const auto page_count = static_cast<size_t>((chunk_end - chunk_begin) / PAGE_SIZE);
std::vector<Impl::PageState*> pages;
pages.reserve(page_count);
for (auto address = chunk_begin; address < chunk_end; address += PAGE_SIZE) {
pages.push_back(&m_impl->GetPage(*region, address));
}
Impl::PageRangeGuard lock(pages);
std::vector<uint8_t> first_watchers(page_count);
for (size_t i = 0; i < page_count; i++) {
auto& page = *pages[i];
const auto address = chunk_begin + i * PAGE_SIZE;
if (page.resolving && track) {
FailFast("new page watcher raced active fault resolution");
}
auto& watchers =
(mode == PageWatchMode::ReadWrite ? page.access_watchers : page.write_watchers);
if (track) {
if (watchers == std::numeric_limits<uint32_t>::max()) {
Fatal("watcher overflow at 0x%016" PRIx64, address);
}
first_watchers[i] = page.write_watchers == 0 && page.access_watchers == 0;
} else {
if (watchers == 0) {
Fatal("watcher underflow at 0x%016" PRIx64, address);
}
if (page.backing_writer != 0 && page.backing_writer != CurrentThread()) {
Fatal("backing write ownership changed at 0x%016" PRIx64, address);
}
}
}
if (track) {
for (size_t first = 0; first < page_count;) {
while (first < page_count && first_watchers[first] == 0) {
first++;
}
auto last = first;
while (last < page_count && first_watchers[last] != 0) {
last++;
}
if (first != last) {
Impl::InitializeProtection(std::span {pages}.subspan(first, last - first));
}
first = last;
}
}
std::vector<uint32_t> old_protections(page_count);
std::vector<uint32_t> new_protections(page_count);
std::vector<uint8_t> transitions(page_count);
for (size_t i = 0; i < page_count; i++) {
auto& page = *pages[i];
auto& watchers =
(mode == PageWatchMode::ReadWrite ? page.access_watchers : page.write_watchers);
const auto old_protection = Impl::WatcherProtection(page);
if (track) {
watchers++;
} else {
watchers--;
}
const auto new_protection = Impl::WatcherProtection(page);
old_protections[i] = old_protection;
new_protections[i] = new_protection;
if (new_protection != old_protection && (track || page.backing_writer == 0)) {
transitions[i] = 1;
}
}
for (size_t first = 0; first < page_count;) {
while (first < page_count && transitions[first] == 0) {
first++;
}
if (first == page_count) {
break;
}
const auto protection = new_protections[first];
auto current = first + 1;
auto last = current;
for (; current < page_count && new_protections[current] == protection; current++) {
if (old_protections[current] != new_protections[current] &&
transitions[current] == 0) {
break;
}
if (transitions[current] != 0) {
last = current + 1;
}
}
m_impl->ProtectRange(std::span {pages}.subspan(first, last - first),
chunk_begin + first * PAGE_SIZE, protection,
std::span {old_protections}.subspan(first, last - first), false);
first = current;
}
for (size_t i = 0; i < page_count; i++) {
auto& page = *pages[i];
const auto protection = new_protections[i];
if (track) {
switch (protection) {
case NO_ACCESS_PROTECTION:
page.late_read_pending = false;
page.late_write_pending = false;
break;
case READ_ONLY_PROTECTION: page.late_write_pending = false; break;
default: break;
}
} else if (page.backing_writer == 0) {
Impl::PublishDelayedFaults(page, old_protections[i], protection);
if (page.write_watchers == 0 && page.access_watchers == 0) {
page.original_protection = 0;
}
}
}
chunk_begin = chunk_end;
}
}
template void PageManager::UpdatePageWatchersForRegion<true, true>(uint64_t, RegionBits&);
template void PageManager::UpdatePageWatchersForRegion<true, false>(uint64_t, RegionBits&);
template void PageManager::UpdatePageWatchersForRegion<false, true>(uint64_t, RegionBits&);
template void PageManager::UpdatePageWatchersForRegion<false, false>(uint64_t, RegionBits&);
void PageManager::OnGpuMap(uint64_t, uint64_t) {}
void PageManager::OnGpuUnmap(uint64_t, uint64_t) {}
PageManager::BackingWrite::BackingWrite(PageManager& manager, uint64_t vaddr,
uint64_t size) noexcept
: m_manager(manager), m_vaddr(vaddr), m_size(size) {
m_manager.BeginBackingWrite(vaddr, size);
}
PageManager::BackingWrite::~BackingWrite() {
m_manager.EndBackingWrite(m_vaddr, m_size);
}
std::vector<std::unique_ptr<PageManager::BackingWrite>>
PageManager::ReserveBackingWrites(std::span<const RangeSet::Range> ranges) {
if (ranges.empty()) {
Fatal("cannot reserve empty backing-write ranges");
}
std::vector<std::unique_ptr<BackingWrite>> writes;
writes.reserve(ranges.size());
uint64_t begin = 0;
uint64_t end = 0;
for (const auto& range: ranges) {
if (range.address == 0 || range.size == 0 || range.size > UINT64_MAX - range.address ||
range.address + range.size > UINT64_MAX - (PAGE_SIZE - 1)) {
Fatal("invalid backing-write range");
}
const auto page_begin = PageStart(range.address);
const auto page_end = PageStart(range.address + range.size + PAGE_SIZE - 1);
if (begin != 0 && page_begin > end) {
writes.push_back(std::make_unique<BackingWrite>(*this, begin, end - begin));
begin = 0;
}
if (begin == 0) {
begin = page_begin;
end = page_end;
} else {
end = std::max(end, page_end);
}
}
writes.push_back(std::make_unique<BackingWrite>(*this, begin, end - begin));
return writes;
}
void PageManager::BeginBackingWrite(uint64_t vaddr, uint64_t size) noexcept {
if (g_in_fault_resolution) {
FailFast("backing write began during fault resolution");
}
const auto end = PageEnd(vaddr, size);
const auto writer = CurrentThread();
for (auto address = PageStart(vaddr); address < end; address += PAGE_SIZE) {
auto* region = m_impl->FindRegion(address);
if (region == nullptr) {
Fatal("backing write reserves an unknown page at 0x%016" PRIx64, address);
}
auto& page = m_impl->GetPage(*region, address);
SpinGuard lock(page.lock);
if (page.resolving || page.backing_writer != 0 || page.access_watchers == 0) {
Fatal("backing write races page resolution at 0x%016" PRIx64, address);
}
page.resolving = true;
page.resolving_read_write = true;
page.backing_writer = writer;
}
}
void PageManager::EndBackingWrite(uint64_t vaddr, uint64_t size) noexcept {
if (g_in_fault_resolution) {
FailFast("backing write ended during fault resolution");
}
const auto end = PageEnd(vaddr, size);
const auto writer = CurrentThread();
for (auto address = PageStart(vaddr); address < end; address += PAGE_SIZE) {
auto* region = m_impl->FindRegion(address);
if (region == nullptr) {
FailFast("backing write ended for an unknown page");
}
auto& page = m_impl->GetPage(*region, address);
SpinGuard lock(page.lock);
if (!page.resolving || page.backing_writer != writer) {
FailFast("backing write ended without matching owner and resolving state");
}
const auto old_protection = NO_ACCESS_PROTECTION;
const auto new_protection = Impl::WatcherProtection(page);
if (new_protection != old_protection) {
m_impl->Protect(page, address, new_protection, old_protection, false);
}
Impl::PublishDelayedFaults(page, old_protection, new_protection);
if (page.write_watchers == 0 && page.access_watchers == 0) {
page.original_protection = 0;
}
page.backing_writer = 0;
page.resolving = false;
page.resolving_read_write = false;
}
}
bool PageManager::HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noexcept {
if (g_in_fault_resolution) {
FailFast("nested HandleFault call");
}
auto* region = m_impl->FindRegion(fault_vaddr);
if (region == nullptr) {
return false;
}
auto& page = m_impl->GetPage(*region, fault_vaddr);
bool waited = false;
while (true) {
SpinGuard lock(page.lock);
if (access == PageFaultAccess::Read && page.late_read_pending &&
Impl::AllowsAccess(page, fault_vaddr, access)) {
page.late_read_pending = false;
return true;
}
if (access == PageFaultAccess::Write && page.late_write_pending &&
Impl::AllowsAccess(page, fault_vaddr, access)) {
page.late_write_pending = false;
return true;
}
if (page.resolving) {
if (page.backing_writer == CurrentThread()) {
FailFast("backing writer faulted on its own reserved page");
}
if ((!page.resolving_read_write && access != PageFaultAccess::Write) ||
(page.resolving_read_write && access != PageFaultAccess::Read &&
access != PageFaultAccess::Write)) {
FailFast("fault access is incompatible with the active resolver");
}
waited = true;
continue;
}
if (page.write_watchers == 0 && page.access_watchers == 0) {
if (access != PageFaultAccess::Read && access != PageFaultAccess::Write) {
return false;
}
bool& pending = (access == PageFaultAccess::Read ? page.late_read_pending
: page.late_write_pending);
const bool allowed = Impl::AllowsAccess(page, fault_vaddr, access);
pending = false;
if (waited && !allowed) {
FailFast("page remained inaccessible after waiting for its resolver");
}
// More than one CPU can fault before a protection transition becomes visible. The first
// delayed fault consumes the hint bit; later faults must also resume once the mapped
// page already permits the requested access. A genuinely read-only/no-access page still
// falls through to the guest exception path.
return allowed;
}
if ((access != PageFaultAccess::Read && access != PageFaultAccess::Write) ||
(access == PageFaultAccess::Read && page.access_watchers == 0)) {
FailFast("fault access is incompatible with active page watchers");
}
page.resolving = true;
page.resolving_read_write = page.access_watchers != 0;
break;
}
g_in_fault_resolution = true;
const bool handled = m_impl->fault_handler(m_impl->fault_context, access, fault_vaddr, 1,
PageFaultPhase::Invalidate);
g_in_fault_resolution = false;
{
SpinGuard lock(page.lock);
if (!handled || !page.resolving) {
FailFast("fault invalidation did not preserve the resolving state");
}
}
g_in_fault_resolution = true;
const bool completed = m_impl->fault_handler(m_impl->fault_context, access, fault_vaddr, 1,
PageFaultPhase::Complete);
g_in_fault_resolution = false;
{
SpinGuard lock(page.lock);
if (!completed || !page.resolving) {
FailFast("fault completion did not preserve the resolving state");
}
if (page.write_watchers != 0 || page.access_watchers != 0) {
const auto old_protection = Impl::WatcherProtection(page);
const bool read_only_fault = access == PageFaultAccess::Read;
if (read_only_fault && page.access_watchers == 0) {
FailFast("read fault completed without a read/write watcher");
}
page.access_watchers = 0;
if (!read_only_fault) {
page.write_watchers = 0;
}
const auto restored_protection = Impl::WatcherProtection(page);
m_impl->Protect(page, PageStart(fault_vaddr), restored_protection, old_protection,
true);
if (page.write_watchers == 0) {
page.original_protection = 0;
}
Impl::PublishDelayedFaults(page, old_protection, restored_protection);
} else if (!Impl::AllowsAccess(page, fault_vaddr, access)) {
FailFast("fault completion left the page inaccessible");
}
page.resolving = false;
page.resolving_read_write = false;
}
g_in_fault_resolution = true;
const bool released = m_impl->fault_handler(m_impl->fault_context, access, fault_vaddr, 1,
PageFaultPhase::Release);
g_in_fault_resolution = false;
if (!released) {
FailFast("fault release callback failed");
}
return true;
}
} // namespace Libs::Graphics
+6 -30
View File
@@ -2,56 +2,32 @@
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_PAGEMANAGER_H_
#include "common/common.h"
#include "graphics/host_gpu/rangeSet.h"
#include "graphics/host_gpu/regionDefinitions.h"
#include <memory>
#include <span>
#include <vector>
namespace Libs::Graphics {
enum class PageFaultAccess { Read, Write, Execute, Unknown };
enum class PageFaultPhase { Invalidate, Complete, Release };
enum class PageWatchMode { Write, ReadWrite };
using PageFaultHandler = bool (*)(void* context, PageFaultAccess access, uint64_t vaddr,
uint64_t size, PageFaultPhase phase) noexcept;
class PageManager final {
public:
class BackingWrite final {
public:
BackingWrite(PageManager& manager, uint64_t vaddr, uint64_t size) noexcept;
~BackingWrite();
KYTY_CLASS_NO_COPY(BackingWrite);
private:
PageManager& m_manager;
uint64_t m_vaddr = 0;
uint64_t m_size = 0;
};
PageManager(PageFaultHandler fault_handler, void* fault_context);
PageManager();
// The owner must stop all PageManager callers before destruction.
~PageManager();
KYTY_CLASS_NO_COPY(PageManager);
[[nodiscard]] uint64_t GetPageSize() const;
[[nodiscard]] bool IsTracked(uint64_t vaddr) const noexcept;
void UpdatePageWatchers(bool track, uint64_t vaddr, uint64_t size,
PageWatchMode mode = PageWatchMode::Write);
template <bool track>
void UpdatePageWatchers(uint64_t vaddr, uint64_t size);
template <bool track, bool is_read = false>
void UpdatePageWatchersForRegion(uint64_t base_addr, RegionBits& mask);
void OnGpuMap(uint64_t vaddr, uint64_t size);
void OnGpuUnmap(uint64_t vaddr, uint64_t size);
[[nodiscard]] bool HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noexcept;
[[nodiscard]] std::vector<std::unique_ptr<BackingWrite>>
ReserveBackingWrites(std::span<const RangeSet::Range> ranges);
private:
void BeginBackingWrite(uint64_t vaddr, uint64_t size) noexcept;
void EndBackingWrite(uint64_t vaddr, uint64_t size) noexcept;
struct Impl;
std::unique_ptr<Impl> m_impl;
};
+3 -3
View File
@@ -1,10 +1,9 @@
#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_REGIONDEFINITIONS_H_
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_REGIONDEFINITIONS_H_
#include "common/bitArray.h"
#include "common/common.h"
#include <bitset>
namespace Libs::Graphics {
constexpr uint64_t TRACKER_PAGE_SIZE = 4ull * 1024ull;
@@ -13,7 +12,8 @@ constexpr uint64_t TRACKER_ADDRESS_SIZE = 1ull << 40u;
constexpr size_t TRACKER_REGION_PAGES = TRACKER_REGION_SIZE / TRACKER_PAGE_SIZE;
enum class DirtySource { Cpu, Gpu };
using RegionBits = std::bitset<TRACKER_REGION_PAGES>;
using RegionBits = Common::BitArray<TRACKER_REGION_PAGES>;
static_assert(sizeof(RegionBits) == TRACKER_REGION_PAGES / 8);
} // namespace Libs::Graphics
+53 -202
View File
@@ -25,8 +25,6 @@
namespace Libs::Graphics {
enum class CpuFaultAction { Untracked, Continue, Download };
class TrackingSpinLock final {
public:
void lock() noexcept {
@@ -78,229 +76,93 @@ public:
if (m_cpu_addr % TRACKER_REGION_SIZE != 0) {
EXIT("invalid region tracking manager construction\n");
}
m_cpu_dirty.set();
m_writable.set();
m_cpu_dirty.Fill();
m_writable.Fill();
m_readable.Fill();
}
KYTY_CLASS_NO_COPY(RegionManager);
[[nodiscard]] uint64_t GetCpuAddr() const { return m_cpu_addr; }
void Track(uint64_t vaddr, uint64_t size) {
const auto [start, end] = GetPageRange(vaddr, size);
for (auto page = start; page < end; page++) {
m_tracked.set(page);
}
}
void Untrack(uint64_t vaddr, uint64_t size) {
const auto [start, end] = GetPageRange(vaddr, size);
for (auto page = start; page < end; page++) {
m_tracked.reset(page);
}
}
template <DirtySource source>
[[nodiscard]] bool IsModified(uint64_t offset, uint64_t size) const {
const auto [start, end] = GetPageRange(m_cpu_addr + offset, size);
const auto& bits = GetBits<source>();
for (auto page = start; page < end; page++) {
if (bits.test(page)) {
return true;
}
}
return false;
}
template <DirtySource source>
[[nodiscard]] bool IsFullyModified(uint64_t offset, uint64_t size) const {
const auto [start, end] = GetPageRange(m_cpu_addr + offset, size);
const auto& bits = GetBits<source>();
for (auto page = start; page < end; page++) {
if (!bits.test(page)) {
return false;
}
}
return true;
return RegionBits(bits, start, end).Any();
}
template <DirtySource source, bool enable>
RegionBits ChangeState(uint64_t vaddr, uint64_t size) {
void ChangeState(uint64_t vaddr, uint64_t size) {
const auto [start, end] = GetPageRange(vaddr, size);
if constexpr (source == DirtySource::Cpu && enable) {
for (auto page = start; page < end; page++) {
if (m_gpu_dirty.test(page) || m_fault_pending.test(page)) {
EXIT("CPU dirty state conflicts with GPU dirty or pending fault state\n");
}
if (RegionBits(m_gpu_dirty, start, end).Any()) {
EXIT("CPU dirty state conflicts with GPU dirty state\n");
}
}
if constexpr (source == DirtySource::Gpu && enable) {
for (auto page = start; page < end; page++) {
if (m_cpu_dirty.test(page) || m_fault_pending.test(page)) {
EXIT("GPU dirty state conflicts with CPU dirty or pending fault state\n");
}
if (RegionBits(m_cpu_dirty, start, end).Any()) {
EXIT("GPU dirty state conflicts with CPU dirty state\n");
}
}
auto& bits = GetBits<source>();
auto changed = bits;
for (auto page = start; page < end; page++) {
bits.set(page, enable);
auto& bits = GetBits<source>();
if constexpr (enable) {
bits.SetRange(start, end);
} else {
bits.UnsetRange(start, end);
}
changed ^= bits;
if constexpr (source == DirtySource::Cpu) {
changed = m_cpu_dirty ^ m_writable;
m_writable = m_cpu_dirty;
UpdateCpuProtection<!enable>();
} else {
UpdateGpuProtection<enable>();
}
return changed;
}
[[nodiscard]] CpuFaultAction BeginCpuFault(uint64_t vaddr, uint64_t size,
PageFaultAccess access = PageFaultAccess::Write) {
if (access != PageFaultAccess::Read && access != PageFaultAccess::Write) {
EXIT("unsupported CPU fault access while beginning ownership transfer\n");
}
const auto [start, end] = GetPageRange(vaddr, size);
const bool tracked = m_tracked.test(start);
for (auto page = start; page < end; page++) {
if (m_tracked.test(page) != tracked) {
EXIT("CPU fault spans mixed tracked and untracked pages\n");
}
if (m_fault_pending.test(page)) {
return CpuFaultAction::Untracked;
}
if (m_cpu_dirty.test(page) != m_writable.test(page) ||
(m_gpu_dirty.test(page) && (m_cpu_dirty.test(page) || m_writable.test(page)))) {
EXIT("inconsistent CPU fault page state\n");
}
}
if (!tracked) {
return CpuFaultAction::Untracked;
}
bool gpu_dirty = m_gpu_dirty.test(start);
bool writable = m_writable.test(start);
for (auto page = start + 1; page < end; page++) {
if (m_gpu_dirty.test(page) != gpu_dirty || m_writable.test(page) != writable) {
EXIT("CPU fault spans pages with incompatible dirty or writable state\n");
}
}
for (auto page = start; page < end; page++) {
if (!gpu_dirty && access == PageFaultAccess::Write) {
m_cpu_dirty.set(page);
m_writable.set(page);
}
m_fault_pending.set(page);
}
return gpu_dirty ? CpuFaultAction::Download : CpuFaultAction::Continue;
}
[[nodiscard]] bool CompleteCpuFault(uint64_t vaddr, uint64_t size, PageFaultAccess access,
bool downloaded) {
const auto [start, end] = GetPageRange(vaddr, size);
for (auto page = start; page < end; page++) {
if (!m_fault_pending.test(page)) {
return false;
}
}
for (auto page = start; page < end; page++) {
const bool gpu_dirty = m_gpu_dirty.test(page);
if (gpu_dirty != downloaded) {
EXIT("CPU fault download result disagrees with GPU dirty state\n");
}
if (gpu_dirty) {
m_gpu_dirty.reset(page);
switch (access) {
case PageFaultAccess::Read: break;
case PageFaultAccess::Write:
m_cpu_dirty.set(page);
m_writable.set(page);
break;
default: EXIT("unsupported CPU fault access after GPU download\n");
}
}
m_fault_pending.reset(page);
}
return true;
}
[[nodiscard]] bool HasPendingFault(uint64_t vaddr, uint64_t size) const {
const auto [start, end] = GetPageRange(vaddr, size);
for (auto page = start; page < end; page++) {
if (m_fault_pending.test(page)) {
return true;
}
}
return false;
}
[[nodiscard]] bool CompleteVirtualGpuWrite(uint64_t vaddr, uint64_t size) {
const auto [start, end] = GetPageRange(vaddr, size);
for (auto page = start; page < end; page++) {
if (!m_fault_pending.test(page)) {
return false;
}
if (!m_gpu_dirty.test(page)) {
EXIT("virtual GPU write completion found a non-GPU-dirty page\n");
}
}
for (auto page = start; page < end; page++) {
m_gpu_dirty.reset(page);
m_cpu_dirty.set(page);
m_writable.set(page);
m_fault_pending.reset(page);
}
return true;
}
template <DirtySource source, bool clear, typename Func>
RegionBits ForEachModifiedRange(uint64_t vaddr, uint64_t size, Func&& func) {
void ForEachModifiedRange(uint64_t vaddr, uint64_t size, Func&& func) {
const auto [start, end] = GetPageRange(vaddr, size);
auto mask = GetBits<source>();
if constexpr (source == DirtySource::Cpu) {
mask &= ~m_fault_pending;
}
for (auto page = 0u; page < start; page++) {
mask.reset(page);
}
for (auto page = end; page < TRACKER_REGION_PAGES; page++) {
mask.reset(page);
}
RegionBits mask(GetBits<source>(), start, end);
if constexpr (clear) {
auto& bits = GetBits<source>();
for (auto page = start; page < end; page++) {
if (mask.test(page)) {
bits.reset(page);
}
}
GetBits<source>().UnsetRange(start, end);
}
if constexpr (source == DirtySource::Cpu && clear) {
auto changed = m_cpu_dirty ^ m_writable;
m_writable = m_cpu_dirty;
ApplyProtection(changed, true);
UpdateCpuProtection<true>();
ForEachRange(mask, std::forward<Func>(func));
return changed;
return;
}
if constexpr (source == DirtySource::Gpu && clear) {
UpdateGpuProtection<false>();
}
ForEachRange(mask, std::forward<Func>(func));
if constexpr (clear) {
return mask;
}
return {};
}
void ApplyProtection(const RegionBits& changed, bool track) {
ForEachRange(changed, [this, track](uint64_t vaddr, uint64_t size) {
m_page_manager.UpdatePageWatchers(track, vaddr, size);
});
}
void ApplyGpuProtection(const RegionBits& changed, bool track, PageWatchMode mode) {
if (mode != PageWatchMode::Write && mode != PageWatchMode::ReadWrite) {
EXIT("unsupported GPU page-watch mode\n");
}
ForEachRange(changed, [this, track, mode](uint64_t vaddr, uint64_t size) {
m_page_manager.UpdatePageWatchers(track, vaddr, size, mode);
});
}
TrackingSpinLock lock;
private:
template <bool track>
void UpdateCpuProtection() {
auto mask = m_cpu_dirty ^ m_writable;
m_writable = m_cpu_dirty;
if (mask.None()) {
return;
}
m_page_manager.UpdatePageWatchersForRegion<track>(m_cpu_addr, mask);
}
template <bool track>
void UpdateGpuProtection() {
auto readable = ~m_gpu_dirty;
auto mask = readable ^ m_readable;
m_readable = readable;
if (mask.None()) {
return;
}
if constexpr (track) {
m_page_manager.UpdatePageWatchersForRegion<true, true>(m_cpu_addr, mask);
} else {
m_page_manager.UpdatePageWatchersForRegion<false, true>(m_cpu_addr, mask);
}
}
template <DirtySource source>
RegionBits& GetBits() {
if constexpr (source == DirtySource::Cpu) {
@@ -331,18 +193,8 @@ private:
template <typename Func>
void ForEachRange(const RegionBits& bits, Func&& func) const {
size_t page = 0;
while (page < TRACKER_REGION_PAGES) {
while (page < TRACKER_REGION_PAGES && !bits.test(page)) {
page++;
}
const auto start = page;
while (page < TRACKER_REGION_PAGES && bits.test(page)) {
page++;
}
if (start != page) {
func(m_cpu_addr + start * TRACKER_PAGE_SIZE, (page - start) * TRACKER_PAGE_SIZE);
}
for (const auto [start, end]: bits) {
func(m_cpu_addr + start * TRACKER_PAGE_SIZE, (end - start) * TRACKER_PAGE_SIZE);
}
}
@@ -351,8 +203,7 @@ private:
RegionBits m_cpu_dirty;
RegionBits m_gpu_dirty;
RegionBits m_writable;
RegionBits m_fault_pending;
RegionBits m_tracked;
RegionBits m_readable;
};
} // namespace Libs::Graphics
+68 -427
View File
@@ -123,30 +123,6 @@ struct BufferCache::RetiredBuffer {
std::shared_ptr<Buffer> owner;
};
struct BufferCache::FaultReadback {
PageFaultAccess access = PageFaultAccess::Unknown;
uint64_t vaddr = 0;
uint64_t size = 0;
std::vector<DownloadRange> ranges;
bool installed = false;
[[nodiscard]] bool Active() const noexcept { return !ranges.empty(); }
void Reset() {
access = PageFaultAccess::Unknown;
vaddr = 0;
size = 0;
installed = false;
ranges.clear();
}
};
struct BufferCache::PendingBackingPublication {
uint64_t address = 0;
uint64_t size = 0;
uint64_t tick = 0;
};
std::pair<uint64_t, uint64_t> BufferCache::DownloadEnvelope(const DownloadCopy& copy) {
if (copy.owner == nullptr || copy.size == 0 || copy.source_offset > copy.owner->Size() ||
copy.size > copy.owner->Size() - copy.source_offset) {
@@ -218,34 +194,26 @@ void BufferCache::QueueGarbageDownload(std::span<const DownloadCopy> copies, Ret
if (copies.empty()) {
return;
}
auto downloads = RecordDownloads(copies);
const auto tick = m_scheduler.CurrentTick();
BeginBackingPublication(retire.address, retire.size, tick);
m_scheduler.DeferOperation([this, downloads = std::move(downloads), retire = std::move(retire),
tick]() mutable {
PublishDownloads(downloads);
{
FaultSafeCacheLock lock(this, m_mutex);
if (m_memory_tracker.IsRegionGpuModified(retire.address, retire.size)) {
m_memory_tracker.ForEachDownloadRange<true>(
retire.address, retire.size,
[&](uint64_t address, uint64_t size) noexcept {
m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, size,
"asynchronous garbage retirement");
},
[](uint64_t, uint64_t) noexcept {});
}
for (const auto& range: downloads) {
m_gpu_modified_ranges.Subtract(range.address, range.size);
}
if (m_memory_tracker.IsRegionGpuModified(retire.address, retire.size) ||
!m_gpu_modified_ranges.Intersections(retire.address, retire.size).empty()) {
EXIT("BufferCache: asynchronous garbage collection retained GPU ownership\n");
}
m_memory_tracker.UntrackMemory(retire.address, retire.size);
}
CompleteBackingPublication(retire.address, retire.size, tick);
});
auto downloads = RecordDownloads(copies);
m_scheduler.DeferOperation(
[this, downloads = std::move(downloads), retire = std::move(retire)]() mutable {
PublishDownloads(downloads);
{
FaultSafeCacheLock lock(this, m_mutex);
for (const auto& range: downloads) {
m_gpu_modified_ranges.Subtract(range.address, range.size);
}
// ForEachDownloadRange reports full tracker pages, and every exact GPU-owned
// interval on those pages was downloaded and removed. Clearing the original
// query therefore cannot orphan a dirty sibling on an edge page.
m_memory_tracker.UnmarkRegionAsGpuModified(retire.address, retire.size);
if (m_memory_tracker.IsRegionGpuModified(retire.address, retire.size) ||
!m_gpu_modified_ranges.Intersections(retire.address, retire.size).empty()) {
EXIT("BufferCache: asynchronous garbage collection retained GPU ownership\n");
}
m_memory_tracker.UntrackMemory(retire.address, retire.size);
}
});
}
BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
@@ -253,13 +221,12 @@ BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
ResourceMutex& resource_mutex)
: m_graphics(graphics), m_scheduler(scheduler),
m_gds_buffer(graphics, scheduler, MemoryUsage::Stream, 0, AllFlags, GdsBufferSize),
m_fault_readback(std::make_unique<FaultReadback>()), m_memory_tracker(page_manager),
m_memory_tracker(page_manager),
m_staging_buffer(graphics, scheduler, MemoryUsage::Upload, 512 * MiB),
m_stream_buffer(graphics, scheduler, MemoryUsage::Stream, 64 * MiB),
m_download_buffer(graphics, scheduler, MemoryUsage::Download, 32 * MiB),
m_device_buffer(graphics, scheduler, MemoryUsage::DeviceLocal, 128 * MiB),
m_page_manager(page_manager), m_texture_cache(texture_cache),
m_resource_mutex(resource_mutex) {
m_texture_cache(texture_cache), m_resource_mutex(resource_mutex) {
std::memset(m_gds_buffer.Mapped().data(), 0, static_cast<size_t>(m_gds_buffer.Size()));
m_gds_buffer.Flush(0, m_gds_buffer.Size());
if (!m_graphics.CanReportMemoryUsage()) {
@@ -277,15 +244,9 @@ BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
}
BufferCache::~BufferCache() {
if (m_fault_readback->Active()) {
EXIT("BufferCache: destroyed with an active fault readback\n");
}
if (!m_gpu_modified_ranges.Empty()) {
EXIT("BufferCache: destroyed with pending GPU-modified ranges\n");
}
if (!m_pending_backing_publications.empty()) {
EXIT("BufferCache: destroyed with pending backing publications\n");
}
for (const auto& [vaddr, cached]: m_buffers) {
(void)vaddr;
if (m_memory_tracker.IsRegionGpuModified(cached->vaddr, cached->size)) {
@@ -295,68 +256,6 @@ BufferCache::~BufferCache() {
m_buffers.clear();
}
bool BufferCache::SynchronizeBacking(uint64_t vaddr, uint64_t size) {
bool waited = false;
for (;;) {
uint64_t tick = 0;
const auto page_begin = vaddr & ~(TRACKER_PAGE_SIZE - 1);
const auto page_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1);
CacheRange affected {.address = page_begin, .size = page_end - page_begin};
{
FaultSafeCacheLock lock(this, m_mutex);
bool changed = true;
while (changed) {
changed = false;
for (const auto& [address, cached]: m_buffers) {
const CacheRange previous = affected;
if (ResolveOverlap(affected, {address, cached->size}) &&
(previous.address != affected.address || previous.size != affected.size)) {
changed = true;
}
}
}
}
{
std::lock_guard lock(m_publication_mutex);
for (const auto& publication: m_pending_backing_publications) {
if (publication.address < affected.address + affected.size &&
affected.address < publication.address + publication.size) {
tick = std::max(tick, publication.tick);
}
}
}
if (tick == 0) {
return waited;
}
waited = true;
m_scheduler.Wait(tick);
m_scheduler.WaitPriorityOperations(tick);
}
}
void BufferCache::RefreshInvalidatedRanges(CommandBuffer& command, CachedBuffer& cached,
uint64_t vaddr, uint64_t size, bool upload) {
const auto invalidated = m_image_invalidated_ranges.Intersections(vaddr, size);
if (upload) {
std::array<uint8_t, 64 * 1024> bytes;
for (const auto& range: invalidated) {
for (uint64_t copied = 0; copied < range.size;) {
const auto chunk = std::min<uint64_t>(range.size - copied, bytes.size());
if (!Libs::LibKernel::Memory::TryReadBacking(range.address + copied, bytes.data(),
chunk)) {
EXIT("BufferCache: failed to refresh an invalidated image alias\n");
}
Upload(command, *cached.buffer, cached.buffer->Offset(range.address + copied),
bytes.data(), chunk);
copied += chunk;
}
}
}
if (!invalidated.empty()) {
m_image_invalidated_ranges.Subtract(vaddr, size);
}
}
StreamBuffer& BufferCache::GetUtilityBuffer(MemoryUsage usage) noexcept {
switch (usage) {
case MemoryUsage::Upload: return m_staging_buffer;
@@ -385,7 +284,6 @@ void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) {
size > TRACKER_ADDRESS_SIZE - vaddr) {
EXIT("BufferCache: invalid memory-invalidation range\n");
}
(void)SynchronizeBacking(vaddr, size);
if (!HasPageOverlap(vaddr, size)) {
return;
}
@@ -394,7 +292,6 @@ void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) {
}
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
(void)SynchronizeBacking(vaddr, size);
std::vector<DownloadCopy> copies;
{
FaultSafeCacheLock lock(this, m_mutex);
@@ -434,117 +331,21 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
PublishDownloads(downloads);
{
FaultSafeCacheLock lock(this, m_mutex);
m_memory_tracker.ForEachDownloadRange<true>(
vaddr, size,
[&](uint64_t address, uint64_t bytes) noexcept {
m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, bytes,
"memory invalidation completion");
},
[](uint64_t, uint64_t) noexcept {});
for (const auto& range: downloads) {
m_gpu_modified_ranges.Subtract(range.address, range.size);
}
// The enumeration above covered whole dirty pages and every exact interval on them.
m_memory_tracker.UnmarkRegionAsGpuModified(vaddr, size);
}
}
bool BufferCache::InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept {
const auto page = vaddr & ~(TRACKER_PAGE_SIZE - 1);
if (size == 0 || size > page + TRACKER_PAGE_SIZE - vaddr) {
EXIT("BufferCache: invalid page-fault range\n");
}
if (phase == PageFaultPhase::Complete) {
FaultSafeCacheLock lock(this, m_mutex);
auto& fault = *m_fault_readback;
if (!fault.Active()) {
return m_memory_tracker.CompleteCpuFault(vaddr, size, access, false);
}
if (fault.access != access || fault.vaddr != vaddr || fault.size != size ||
fault.installed) {
EXIT("BufferCache: mismatched fault readback completion\n");
}
PublishDownloads(fault.ranges);
if (!m_memory_tracker.CompleteCpuFault(vaddr, size, access, true)) {
EXIT("BufferCache: failed to complete downloaded CPU fault\n");
}
fault.installed = true;
return true;
}
if (phase == PageFaultPhase::Release) {
FaultSafeCacheLock lock(this, m_mutex);
auto& fault = *m_fault_readback;
if (fault.Active()) {
if (fault.access != access || fault.vaddr != vaddr || fault.size != size ||
!fault.installed) {
EXIT("BufferCache: mismatched fault readback release\n");
}
for (const auto& range: fault.ranges) {
m_gpu_modified_ranges.Subtract(range.address, range.size);
}
fault.Reset();
}
return true;
}
if (phase != PageFaultPhase::Invalidate) {
EXIT("BufferCache: unsupported page-fault phase\n");
}
const auto action = m_memory_tracker.BeginCpuFault(vaddr, size, access);
if (action != CpuFaultAction::Download) {
return action == CpuFaultAction::Continue;
}
auto& fault = *m_fault_readback;
std::vector<DownloadCopy> copies;
{
FaultSafeCacheLock lock(this, m_mutex);
if (fault.Active()) {
EXIT("BufferCache: nested fault readback\n");
}
fault.access = access;
fault.vaddr = vaddr;
fault.size = size;
m_gpu_modified_ranges.ForEachIntersection(
page, TRACKER_PAGE_SIZE, [&](RangeSet::Range range) {
auto owner = m_buffers.upper_bound(range.address);
if (owner == m_buffers.begin()) {
EXIT("BufferCache: fault readback has no buffer owner\n");
}
--owner;
auto& cached = *owner->second;
if (!cached.buffer->IsInBounds(range.address, range.size)) {
EXIT("BufferCache: fault readback is outside its buffer owner\n");
}
copies.push_back({cached.buffer, cached.buffer->Offset(range.address),
range.address, range.size});
});
if (copies.empty()) {
EXIT("BufferCache: GPU-dirty fault page has no dirty byte ranges\n");
}
}
fault.ranges = RecordDownloads(copies);
if (!fault.Active()) {
EXIT("BufferCache: GPU-dirty fault page has no dirty byte ranges\n");
}
m_scheduler.FinishCurrent();
return true;
}
void BufferCache::UnmapMemory(uint64_t vaddr, uint64_t size) {
if (vaddr == 0 || size == 0 || size > UINT64_MAX - vaddr) {
EXIT("BufferCache: invalid unmap range\n");
}
(void)SynchronizeBacking(vaddr, size);
std::vector<DownloadCopy> copies;
std::vector<RangeSet::Range> dirty_ranges;
std::vector<std::pair<uint64_t, uint64_t>> modified_buffers;
std::vector<std::unique_ptr<PageManager::BackingWrite>> backing_writes;
std::vector<std::pair<uint64_t, uint64_t>> retired_buffers;
std::vector<DownloadCopy> copies;
std::vector<std::pair<uint64_t, uint64_t>> modified_buffers;
std::vector<std::pair<uint64_t, uint64_t>> retired_buffers;
{
FaultSafeCacheLock lock(this, m_mutex);
for (const auto& [begin, cached]: m_buffers) {
@@ -561,12 +362,8 @@ void BufferCache::UnmapMemory(uint64_t vaddr, uint64_t size) {
if (dirty.empty()) {
EXIT("BufferCache: GPU-modified buffer has no dirty ranges\n");
}
dirty_ranges.insert(dirty_ranges.end(), dirty.begin(), dirty.end());
modified_buffers.emplace_back(begin, cached->size);
}
if (!dirty_ranges.empty()) {
backing_writes = m_page_manager.ReserveBackingWrites(dirty_ranges);
}
for (const auto& [begin, bytes]: modified_buffers) {
auto owner = m_buffers.find(begin);
if (owner == m_buffers.end() || owner->second->size != bytes) {
@@ -596,24 +393,11 @@ void BufferCache::UnmapMemory(uint64_t vaddr, uint64_t size) {
// command stream before removing such backing.
m_scheduler.FinishCurrent();
}
backing_writes.clear();
{
FaultSafeCacheLock lock(this, m_mutex);
for (const auto& [begin, bytes]: modified_buffers) {
if (!m_memory_tracker.IsRegionGpuModified(begin, bytes)) {
continue;
}
m_memory_tracker.ForEachDownloadRange<true>(
begin, bytes,
[&](uint64_t address, uint64_t download_size) noexcept {
m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address,
download_size, "unmap retirement");
},
[](uint64_t, uint64_t) noexcept {});
}
for (const auto& [begin, bytes]: modified_buffers) {
m_gpu_modified_ranges.Subtract(begin, bytes);
m_memory_tracker.UnmarkRegionAsGpuModified(begin, bytes);
}
for (const auto& [begin, bytes]: retired_buffers) {
m_memory_tracker.MarkRegionAsCpuModified(begin, bytes);
@@ -621,7 +405,6 @@ void BufferCache::UnmapMemory(uint64_t vaddr, uint64_t size) {
if (!m_gpu_modified_ranges.Intersections(vaddr, size).empty()) {
EXIT("BufferCache: unmap retained dirty byte ranges\n");
}
m_image_invalidated_ranges.Subtract(vaddr, size);
m_memory_tracker.UntrackMemory(vaddr, size);
for (auto it = m_buffers.begin(); it != m_buffers.end();) {
if (vaddr < it->first + it->second->size && it->first < vaddr + size) {
@@ -715,20 +498,29 @@ BufferBinding BufferCache::ObtainBuffer(CommandBuffer& command, uint64_t vaddr,
EXIT("BufferCache: buffer request requires a recording command buffer\n");
}
std::lock_guard transaction(m_resource_mutex);
(void)SynchronizeBacking(vaddr, size);
if (is_read && !is_written && size <= CACHING_PAGE_SIZE &&
!m_memory_tracker.IsRegionGpuModified(vaddr, size) &&
m_memory_tracker.IsRegionCpuModified(vaddr, size)) {
std::vector<uint8_t> data(size);
if (Libs::LibKernel::Memory::TryReadBacking(vaddr, data.data(), size)) {
return UploadTransient(data.data(), size, 16);
const auto alignment = std::max<uint64_t>(
m_graphics.physical_device_properties.limits.minUniformBufferOffsetAlignment, 1);
if (auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false);
mapped != nullptr) {
if (Libs::LibKernel::Memory::TryReadBacking(vaddr, mapped, size)) {
m_stream_buffer.Commit();
return {{}, m_stream_buffer.Handle(), offset};
}
} else {
auto owner = std::make_shared<Buffer>(m_graphics, m_scheduler, MemoryUsage::Upload, 0,
AllFlags, size);
if (Libs::LibKernel::Memory::TryReadBacking(vaddr, owner->Mapped().data(), size)) {
owner->Flush(0, size);
return {owner, owner->Handle(), 0};
}
}
}
if (is_formatted && is_read && !is_written) {
(void)m_texture_cache.SynchronizeImageToBuffer(vaddr, size);
} else if (is_formatted && is_written) {
if (is_formatted && is_written) {
(void)m_texture_cache.InvalidateMemoryFromGPU(vaddr, size, true);
}
@@ -744,10 +536,12 @@ BufferBinding BufferCache::ObtainBuffer(CommandBuffer& command, uint64_t vaddr,
reinterpret_cast<const void*>(address), bytes);
}
});
RefreshInvalidatedRanges(command, cached, vaddr, size, is_read);
if (is_written) {
m_gpu_modified_ranges.Add(vaddr, size);
}
if (is_formatted && is_read && !is_written) {
(void)SynchronizeBufferFromImage(*cached.buffer, vaddr, size);
}
return {cached.buffer, cached.buffer->Handle(), cached.buffer->Offset(vaddr)};
}
@@ -772,7 +566,6 @@ ImageBufferSource BufferCache::ObtainBufferForImage(uint64_t vaddr, uint64_t siz
size > TRACKER_ADDRESS_SIZE - vaddr) {
EXIT("BufferCache: invalid image source\n");
}
(void)SynchronizeBacking(vaddr, size);
auto find_owner = [&]() {
auto owner = m_buffers.upper_bound(vaddr);
if (owner == m_buffers.begin()) {
@@ -787,13 +580,12 @@ ImageBufferSource BufferCache::ObtainBufferForImage(uint64_t vaddr, uint64_t siz
const bool cpu_modified = m_memory_tracker.IsRegionCpuModified(vaddr, size);
const bool gpu_modified = m_memory_tracker.IsRegionGpuModified(vaddr, size);
const auto dirty = m_gpu_modified_ranges.Intersections(vaddr, size);
const bool invalidated = !m_image_invalidated_ranges.Intersections(vaddr, size).empty();
const bool requested_gpu_owned = !dirty.empty();
const bool has_dirty_buffer_source = !dirty.empty();
m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size,
"image source");
auto owner = find_owner();
if (requested_gpu_owned && owner == m_buffers.end()) {
if (has_dirty_buffer_source && owner == m_buffers.end()) {
CacheRange merged {.address = AlignDown(vaddr),
.size = AlignUp(vaddr + size) - AlignDown(vaddr)};
using Iterator = decltype(m_buffers.begin());
@@ -843,42 +635,32 @@ ImageBufferSource BufferCache::ObtainBufferForImage(uint64_t vaddr, uint64_t siz
EXIT("BufferCache: merged image source does not contain the requested range\n");
}
}
if (owner != m_buffers.end() && !cpu_modified && !invalidated &&
(!gpu_modified || requested_gpu_owned)) {
DiscardGpuDirtyBytesLocked(vaddr, size, "image source transfer");
if (owner != m_buffers.end() && !cpu_modified &&
(!gpu_modified || has_dirty_buffer_source)) {
owner->second->tick_accessed_last = m_gc_tick;
return {owner->second->buffer.get(), owner->second->buffer->Offset(vaddr),
requested_gpu_owned};
return {owner->second->buffer.get(), owner->second->buffer->Offset(vaddr)};
}
if (requested_gpu_owned && owner == m_buffers.end()) {
if (has_dirty_buffer_source && owner == m_buffers.end()) {
EXIT("BufferCache: GPU-dirty image source could not resolve its native owner\n");
}
}
// Direct-memory backing remains readable while PageManager protects the guest mapping. The
// fallback exists for plain host mappings used by standalone renderer tests and is deliberately
// performed outside the cache lock so a page fault cannot recurse into BufferCache.
const auto stage_address = vaddr & ~(TRACKER_PAGE_SIZE - 1);
const auto stage_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1);
const auto stage_size = stage_end - stage_address;
(void)SynchronizeBacking(stage_address, stage_size);
std::vector<uint8_t> bytes(stage_size);
if (!Libs::LibKernel::Memory::TryReadBacking(stage_address, bytes.data(), stage_size)) {
auto [staging, stage_offset] = m_staging_buffer.Map(size, 16);
if (staging == nullptr || !Libs::LibKernel::Memory::TryReadBacking(vaddr, staging, size)) {
EXIT("BufferCache: failed to read mapped guest image backing\n");
}
m_staging_buffer.Commit();
FaultSafeCacheLock lock(this, m_mutex);
const auto dirty = m_gpu_modified_ranges.Intersections(vaddr, size);
const bool invalidated = !m_image_invalidated_ranges.Intersections(vaddr, size).empty();
const bool requested_gpu_owned = !dirty.empty();
auto owner = find_owner();
if (requested_gpu_owned && owner == m_buffers.end()) {
const auto dirty = m_gpu_modified_ranges.Intersections(vaddr, size);
const bool has_dirty_buffer_source = !dirty.empty();
auto owner = find_owner();
if (has_dirty_buffer_source && owner == m_buffers.end()) {
EXIT("BufferCache: GPU-dirty image source lost its native owner\n");
}
const auto stage_offset = m_staging_buffer.Copy(bytes.data(), stage_size, 16);
if (owner == m_buffers.end() || invalidated ||
(m_memory_tracker.IsRegionGpuModified(vaddr, size) && !requested_gpu_owned)) {
return {&m_staging_buffer, stage_offset + vaddr - stage_address, false};
if (owner == m_buffers.end() ||
(m_memory_tracker.IsRegionGpuModified(vaddr, size) && !has_dirty_buffer_source)) {
return {&m_staging_buffer, stage_offset};
}
auto& cached = *owner->second;
@@ -892,42 +674,17 @@ ImageBufferSource BufferCache::ObtainBufferForImage(uint64_t vaddr, uint64_t siz
[&]() noexcept {
for (const auto& [address, upload_size]: uploads) {
cached.buffer->CopyFrom(
m_scheduler.Current(), m_staging_buffer, stage_offset + address - stage_address,
m_scheduler.Current(), m_staging_buffer, stage_offset + address - vaddr,
cached.buffer->Offset(address), upload_size, vk::AccessFlagBits::eHostWrite);
}
});
DiscardGpuDirtyBytesLocked(vaddr, size, "staged image source transfer");
return {cached.buffer.get(), cached.buffer->Offset(vaddr), requested_gpu_owned};
}
void BufferCache::DiscardGpuDirtyBytesLocked(uint64_t vaddr, uint64_t size, const char* operation) {
m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, operation);
m_gpu_modified_ranges.Subtract(vaddr, size);
const auto page_begin = vaddr & ~(TRACKER_PAGE_SIZE - 1);
const auto page_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1);
for (auto page = page_begin; page < page_end; page += TRACKER_PAGE_SIZE) {
if (m_gpu_modified_ranges.Intersections(page, TRACKER_PAGE_SIZE).empty() &&
m_memory_tracker.IsRegionGpuModified(page, TRACKER_PAGE_SIZE)) {
m_memory_tracker.UnmarkRegionAsGpuModified(page, TRACKER_PAGE_SIZE);
}
}
m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, operation);
}
void BufferCache::DiscardGpuDirtyBytes(uint64_t vaddr, uint64_t size) {
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
size > TRACKER_ADDRESS_SIZE - vaddr) {
EXIT("BufferCache: invalid dirty-byte discard range\n");
}
FaultSafeCacheLock lock(this, m_mutex);
DiscardGpuDirtyBytesLocked(vaddr, size, "image output supersession");
return {cached.buffer.get(), cached.buffer->Offset(vaddr)};
}
void BufferCache::WriteHostMemory(uint64_t vaddr, std::span<const uint8_t> data) {
if (vaddr == 0 || data.empty() || data.size() > UINT64_MAX - vaddr) {
EXIT("BufferCache: invalid host DMA write\n");
}
(void)SynchronizeBacking(vaddr, data.size());
Libs::LibKernel::Memory::WriteBacking(vaddr, data.data(), data.size());
FaultSafeCacheLock lock(this, m_mutex);
@@ -943,45 +700,6 @@ void BufferCache::WriteHostMemory(uint64_t vaddr, std::span<const uint8_t> data)
data.data() + begin - vaddr, range_end - begin);
cached->tick_accessed_last = m_gc_tick;
}
m_image_invalidated_ranges.Subtract(vaddr, data.size());
}
std::pair<std::shared_ptr<Buffer>, uint64_t> BufferCache::ObtainBufferForImageWrite(uint64_t vaddr,
uint64_t size) {
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
size > TRACKER_ADDRESS_SIZE - vaddr) {
EXIT("BufferCache: invalid image destination\n");
}
const auto stage_address = vaddr & ~(TRACKER_PAGE_SIZE - 1);
const auto stage_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1);
const auto stage_size = stage_end - stage_address;
(void)SynchronizeBacking(stage_address, stage_size);
std::vector<uint8_t> bytes(stage_size);
if (!Libs::LibKernel::Memory::TryReadBacking(stage_address, bytes.data(), stage_size)) {
EXIT("BufferCache: failed to preserve guest bytes around an image mirror\n");
}
FaultSafeCacheLock lock(this, m_mutex);
auto& cached = GetOrCreateBuffer(m_scheduler.Current(), vaddr, size);
m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size,
"image destination");
if (!m_gpu_modified_ranges.Intersections(vaddr, size).empty()) {
EXIT("BufferCache: image destination aliases GPU-owned buffer bytes\n");
}
const auto stage_offset = m_staging_buffer.Copy(bytes.data(), stage_size, 16);
std::vector<std::pair<uint64_t, uint64_t>> uploads;
m_memory_tracker.ForEachUploadRange(
vaddr, size, false,
[&](uint64_t address, uint64_t upload_size) noexcept {
uploads.emplace_back(address, upload_size);
},
[&]() noexcept {
for (const auto& [address, upload_size]: uploads) {
cached.buffer->CopyFrom(
m_scheduler.Current(), m_staging_buffer, stage_offset + address - stage_address,
cached.buffer->Offset(address), upload_size, vk::AccessFlagBits::eHostWrite);
}
});
return {cached.buffer, cached.buffer->Offset(vaddr)};
}
void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) {
@@ -1041,17 +759,10 @@ void BufferCache::CopyBuffer(uint64_t dst_vaddr, uint64_t src_vaddr, uint64_t si
}
if (src_memory || dst_memory) {
std::lock_guard transaction(m_resource_mutex);
if (src_memory) {
(void)SynchronizeBacking(src_vaddr, size);
}
const auto src_region =
const auto src_region =
src_memory ? m_texture_cache.QueryRegion(src_vaddr, size) : TextureCache::RegionInfo {};
const auto dst_region =
dst_memory ? m_texture_cache.QueryRegion(dst_vaddr, size) : TextureCache::RegionInfo {};
if (src_memory && src_region.gpu_image_bytes &&
!m_texture_cache.SynchronizeImageToBuffer(src_vaddr, size)) {
EXIT("BufferCache: GPU copy source image could not be synchronized\n");
}
if (src_memory && dst_memory && !HasGpuDirtyBytes(src_vaddr, size) &&
!HasGpuDirtyBytes(dst_vaddr, size) && !src_region.gpu_image_bytes &&
!dst_region.gpu_image_bytes) {
@@ -1073,7 +784,7 @@ void BufferCache::CopyBuffer(uint64_t dst_vaddr, uint64_t src_vaddr, uint64_t si
}
auto& command = m_scheduler.Current();
auto src = src_memory ? ObtainBuffer(command, src_vaddr, size, false, true)
auto src = src_memory ? ObtainBuffer(command, src_vaddr, size, false, true, true)
: BufferBinding {.buffer = m_gds_buffer.Handle(), .offset = src_vaddr};
auto dst = dst_memory ? ObtainBuffer(command, dst_vaddr, size, true, false, true)
: BufferBinding {.buffer = m_gds_buffer.Handle(), .offset = dst_vaddr};
@@ -1126,82 +837,13 @@ bool BufferCache::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
return m_memory_tracker.IsRegionCpuModified(vaddr, size);
}
void BufferCache::InvalidateImageAliases(uint64_t vaddr, uint64_t size) {
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
size > TRACKER_ADDRESS_SIZE - vaddr) {
EXIT("BufferCache: invalid image-alias invalidation\n");
}
FaultSafeCacheLock lock(this, m_mutex);
const auto end = vaddr + size;
for (const auto& [address, cached]: m_buffers) {
const auto cached_end = address + cached->size;
const auto begin = std::max(vaddr, address);
const auto range_end = std::min(end, cached_end);
if (begin >= range_end) {
continue;
}
const auto bytes = range_end - begin;
if (!m_gpu_modified_ranges.Intersections(begin, bytes).empty()) {
EXIT("BufferCache: image ownership overlaps exact dirty buffer bytes\n");
}
m_image_invalidated_ranges.Add(begin, bytes);
}
}
void BufferCache::BeginBackingPublication(uint64_t vaddr, uint64_t size, uint64_t tick) {
if (vaddr == 0 || size == 0 || tick == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
size > TRACKER_ADDRESS_SIZE - vaddr) {
EXIT("BufferCache: invalid pending backing publication\n");
}
std::lock_guard lock(m_publication_mutex);
m_pending_backing_publications.push_back({vaddr, size, tick});
}
void BufferCache::CompleteBackingPublication(uint64_t vaddr, uint64_t size, uint64_t tick) {
std::lock_guard lock(m_publication_mutex);
const auto publication =
std::ranges::find_if(m_pending_backing_publications, [&](const auto& pending) {
return pending.address == vaddr && pending.size == size && pending.tick == tick;
});
if (publication == m_pending_backing_publications.end()) {
EXIT("BufferCache: completed an unknown backing publication\n");
}
m_pending_backing_publications.erase(publication);
}
void BufferCache::PublishImageBuffer(uint64_t vaddr, uint64_t size) {
FaultSafeCacheLock lock(this, m_mutex);
auto owner = m_buffers.end();
for (auto it = m_buffers.begin(); it != m_buffers.end(); ++it) {
if (!PageOverlaps(vaddr, size, it->second->vaddr, it->second->size)) {
continue;
}
if (owner != m_buffers.end() || !it->second->buffer->IsInBounds(vaddr, size)) {
EXIT("BufferCache: image destination aliases a non-containing cached buffer\n");
}
owner = it;
}
m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size,
"image destination publication");
if (owner == m_buffers.end() || m_memory_tracker.IsRegionCpuModified(vaddr, size) ||
!m_gpu_modified_ranges.Intersections(vaddr, size).empty()) {
EXIT("BufferCache: image destination requires clean buffer ownership\n");
}
m_memory_tracker.MarkRegionAsGpuModified(vaddr, size);
m_gpu_modified_ranges.Add(vaddr, size);
m_image_invalidated_ranges.Subtract(vaddr, size);
m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size,
"published image destination");
owner->second->tick_accessed_last = m_gc_tick;
}
void BufferCache::RunGarbageCollector() {
std::lock_guard transaction(m_resource_mutex);
const auto tick = m_gc_tick++;
if (m_graphics.CanReportMemoryUsage()) {
m_total_used_memory = m_graphics.GetDeviceMemoryUsage();
}
if (m_total_used_memory < m_trigger_gc_memory || m_fault_readback->Active()) {
if (m_total_used_memory < m_trigger_gc_memory) {
return;
}
@@ -1270,7 +912,6 @@ void BufferCache::RunGarbageCollector() {
if (!m_memory_tracker.IsRegionGpuModified(retire.address, retire.size)) {
m_memory_tracker.UntrackMemory(retire.address, retire.size);
}
m_image_invalidated_ranges.Subtract(retire.address, retire.size);
if (retire.size > m_total_used_memory) {
EXIT("BufferCache: allocation accounting underflow\n");
}
+6 -27
View File
@@ -10,7 +10,6 @@
#include <map>
#include <memory>
#include <mutex>
#include <span>
#include <utility>
#include <vector>
@@ -30,9 +29,8 @@ struct BufferBinding {
};
struct ImageBufferSource {
Buffer* buffer = nullptr;
uint64_t offset = 0;
bool gpu_owned = false;
Buffer* buffer = nullptr;
uint64_t offset = 0;
};
class BufferCache {
@@ -47,11 +45,9 @@ public:
~BufferCache();
KYTY_CLASS_NO_COPY(BufferCache);
[[nodiscard]] bool InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept;
void InvalidateMemory(uint64_t vaddr, uint64_t size);
void ReadMemory(uint64_t vaddr, uint64_t size);
void UnmapMemory(uint64_t vaddr, uint64_t size);
void InvalidateMemory(uint64_t vaddr, uint64_t size);
void ReadMemory(uint64_t vaddr, uint64_t size);
void UnmapMemory(uint64_t vaddr, uint64_t size);
[[nodiscard]] BufferBinding ObtainBuffer(CommandBuffer& command, uint64_t vaddr, uint64_t size,
bool is_written = false, bool is_read = true,
bool is_formatted = false);
@@ -62,9 +58,6 @@ public:
uint64_t alignment);
[[nodiscard]] std::shared_ptr<Buffer> ObtainNullBuffer();
[[nodiscard]] ImageBufferSource ObtainBufferForImage(uint64_t vaddr, uint64_t size);
[[nodiscard]] std::pair<std::shared_ptr<Buffer>, uint64_t>
ObtainBufferForImageWrite(uint64_t vaddr, uint64_t size);
void DiscardGpuDirtyBytes(uint64_t vaddr, uint64_t size);
void FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds = false);
void CopyBuffer(uint64_t dst_vaddr, uint64_t src_vaddr, uint64_t size, bool dst_gds = false,
bool src_gds = false);
@@ -72,11 +65,6 @@ public:
[[nodiscard]] bool HasGpuDirtyBytes(uint64_t vaddr, uint64_t size);
[[nodiscard]] bool IsRegionCpuModified(uint64_t vaddr, uint64_t size);
[[nodiscard]] bool IsRegionGpuModified(uint64_t vaddr, uint64_t size);
void InvalidateImageAliases(uint64_t vaddr, uint64_t size);
void BeginBackingPublication(uint64_t vaddr, uint64_t size, uint64_t tick);
void CompleteBackingPublication(uint64_t vaddr, uint64_t size, uint64_t tick);
[[nodiscard]] bool SynchronizeBacking(uint64_t vaddr, uint64_t size);
void PublishImageBuffer(uint64_t vaddr, uint64_t size);
void RunGarbageCollector();
private:
@@ -90,8 +78,6 @@ private:
struct DownloadCopy;
struct DownloadRange;
struct RetiredBuffer;
struct FaultReadback;
struct PendingBackingPublication;
static constexpr uint64_t DOWNLOAD_ALIGNMENT = 64;
[[nodiscard]] static uint64_t AlignDown(uint64_t value) noexcept;
[[nodiscard]] static uint64_t AlignUp(uint64_t value);
@@ -106,12 +92,10 @@ private:
const void* source, uint64_t size);
[[nodiscard]] CachedBuffer& GetOrCreateBuffer(CommandBuffer& command, uint64_t vaddr,
uint64_t size);
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
[[nodiscard]] std::vector<DownloadRange> RecordDownloads(std::span<const DownloadCopy> copies);
void PublishDownloads(std::span<const DownloadRange> downloads);
void QueueGarbageDownload(std::span<const DownloadCopy> copies, RetiredBuffer retire);
void RefreshInvalidatedRanges(CommandBuffer& command, CachedBuffer& cached, uint64_t vaddr,
uint64_t size, bool upload);
void DiscardGpuDirtyBytesLocked(uint64_t vaddr, uint64_t size, const char* operation);
void WriteHostMemory(uint64_t vaddr, std::span<const uint8_t> data);
GraphicContext& m_graphics;
@@ -120,17 +104,12 @@ private:
Common::Mutex m_mutex;
std::shared_ptr<Buffer> m_null_buffer;
std::map<uint64_t, std::unique_ptr<CachedBuffer>> m_buffers;
std::unique_ptr<FaultReadback> m_fault_readback;
RangeSet m_gpu_modified_ranges;
RangeSet m_image_invalidated_ranges;
std::mutex m_publication_mutex;
std::vector<PendingBackingPublication> m_pending_backing_publications;
MemoryTracker m_memory_tracker;
StreamBuffer m_staging_buffer;
StreamBuffer m_stream_buffer;
StreamBuffer m_download_buffer;
StreamBuffer m_device_buffer;
PageManager& m_page_manager;
TextureCache& m_texture_cache;
ResourceMutex& m_resource_mutex;
uint64_t m_total_used_memory = 0;
+16 -26
View File
@@ -7,33 +7,12 @@
namespace Libs::Graphics {
GpuResourceManager::GpuResourceManager(GraphicContext& graphics, CommandScheduler& scheduler)
: m_page_manager(FaultThunk, this),
: m_scheduler(scheduler),
m_buffer_cache(graphics, scheduler, m_page_manager, m_texture_cache, m_resource_mutex),
m_texture_cache(graphics, scheduler, m_page_manager, m_buffer_cache, m_resource_mutex) {}
GpuResourceManager::~GpuResourceManager() = default;
bool GpuResourceManager::FaultThunk(void* context, PageFaultAccess access, uint64_t vaddr,
uint64_t size, PageFaultPhase phase) noexcept {
return static_cast<GpuResourceManager*>(context)->InvalidateMemory(access, vaddr, size, phase);
}
bool GpuResourceManager::InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept {
// Let the authoritative image materialize first. A clean overlapping buffer marks a write
// fault CPU-dirty when it begins ownership transfer; doing that before image preflight would
// make the image appear to race a real CPU write. Completion and release retain buffer-first
// ordering so its pending fault is gone before TextureCache publishes the downloaded backing.
if (phase == PageFaultPhase::Invalidate) {
const bool image_handled = m_texture_cache.InvalidateMemory(access, vaddr, size, phase);
const bool buffer_handled = m_buffer_cache.InvalidateMemory(access, vaddr, size, phase);
return buffer_handled || image_handled;
}
const bool buffer_handled = m_buffer_cache.InvalidateMemory(access, vaddr, size, phase);
const bool image_handled = m_texture_cache.InvalidateMemory(access, vaddr, size, phase);
return buffer_handled || image_handled;
}
bool GpuResourceManager::HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noexcept {
constexpr uint64_t fault_size = 8;
if (!IsMapped(fault_vaddr, fault_size)) {
@@ -123,7 +102,22 @@ void GpuResourceManager::MapMemory(uint64_t vaddr, uint64_t size) {
}
void GpuResourceManager::UnmapMemory(uint64_t vaddr, uint64_t size) {
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported memory unmap from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported memory unmap from a pre-owned resource transaction, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
const auto unmap = [this, vaddr, size] {
if (m_scheduler.Active()) {
const auto tick = m_scheduler.CurrentTick();
m_scheduler.FinishCurrent();
m_scheduler.WaitPriorityOperations(tick);
}
m_buffer_cache.UnmapMemory(vaddr, size);
m_texture_cache.UnmapMemory(vaddr, size);
m_page_manager.OnGpuUnmap(vaddr, size);
@@ -131,13 +125,9 @@ void GpuResourceManager::UnmapMemory(uint64_t vaddr, uint64_t size) {
m_mapped_ranges.Subtract(vaddr, size);
};
if (m_gpu == nullptr) {
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("cannot synchronously unmap from a resource transaction\n");
}
unmap();
return;
}
Gpu::SubmissionLock submissions(*m_gpu);
m_gpu->SendCommandSync(unmap);
}
+1 -5
View File
@@ -34,13 +34,9 @@ public:
void RunGarbageCollector();
private:
static bool FaultThunk(void* context, PageFaultAccess access, uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept;
[[nodiscard]] bool InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size,
PageFaultPhase phase) noexcept;
PageManager m_page_manager;
ResourceMutex m_resource_mutex;
CommandScheduler& m_scheduler;
BufferCache m_buffer_cache;
TextureCache m_texture_cache;
mutable std::shared_mutex m_mapped_ranges_mutex;
+3 -3
View File
@@ -219,7 +219,7 @@ private:
typename CoarseTable::PageRange coarse_range {};
typename TrackingTable::PageRange tracking_range {};
if (!CoarseTable::TryGetPageRange(address, size, coarse_range) ||
!TrackingTable::TryGetPageRange(address, size, tracking_range)) {
(!strict_bytes && !TrackingTable::TryGetPageRange(address, size, tracking_range))) {
return {};
}
MembershipList candidates;
@@ -230,8 +230,8 @@ private:
}
std::vector<OwnerT> result;
for (const Registration* registration: candidates) {
if ((!strict_bytes || Overlaps(registration->ranges, address, size)) &&
HasTrackingMembership(registration, tracking_range) &&
if ((strict_bytes ? Overlaps(registration->ranges, address, size)
: HasTrackingMembership(registration, tracking_range)) &&
predicate(registration->owner)) {
result.push_back(registration->owner);
}
+4 -1
View File
@@ -56,7 +56,10 @@ vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r) {
case Prospero::SamplerAnisoRatio::kFour: aniso_ratio = 4.0f; break;
case Prospero::SamplerAnisoRatio::kEight: aniso_ratio = 8.0f; break;
case Prospero::SamplerAnisoRatio::kSixteen: aniso_ratio = 16.0f; break;
default: EXIT("unknown ratio: %d\n", static_cast<int>(r.MaxAnisoRatio()));
default:
EXIT("unknown ratio: %d dwords=%08x,%08x,%08x,%08x\n",
static_cast<int>(r.MaxAnisoRatio()), r.fields[0], r.fields[1], r.fields[2],
r.fields[3]);
}
}
+155 -143
View File
@@ -323,7 +323,7 @@ void TextureCache::TrackImage(ImageId id) {
if (!image.IsTracked()) {
image.track_addr = image_begin;
image.track_addr_end = image_end;
m_page_manager.UpdatePageWatchers(true, image_begin, image.info.data.size);
m_page_manager.UpdatePageWatchers<true>(image_begin, image.info.data.size);
return;
}
if (image_begin < image.track_addr) {
@@ -348,7 +348,7 @@ void TextureCache::TrackImageHead(ImageId id) {
}
const auto size = image.track_addr - image_begin;
image.track_addr = image_begin;
m_page_manager.UpdatePageWatchers(true, image_begin, size);
m_page_manager.UpdatePageWatchers<true>(image_begin, size);
}
void TextureCache::TrackImageTail(ImageId id) {
@@ -366,7 +366,7 @@ void TextureCache::TrackImageTail(ImageId id) {
const auto address = image.track_addr_end;
const auto size = image_end - address;
image.track_addr_end = image_end;
m_page_manager.UpdatePageWatchers(true, address, size);
m_page_manager.UpdatePageWatchers<true>(address, size);
}
void TextureCache::UntrackImage(ImageId id) {
@@ -379,7 +379,7 @@ void TextureCache::UntrackImage(ImageId id) {
image.track_addr = 0;
image.track_addr_end = 0;
if (size != 0) {
m_page_manager.UpdatePageWatchers(false, address, size);
m_page_manager.UpdatePageWatchers<false>(address, size);
}
}
@@ -400,7 +400,7 @@ void TextureCache::UntrackImageHead(ImageId id) {
UntrackImage(id);
}
if (size != 0) {
m_page_manager.UpdatePageWatchers(false, begin, size);
m_page_manager.UpdatePageWatchers<false>(begin, size);
}
}
@@ -421,7 +421,7 @@ void TextureCache::UntrackImageTail(ImageId id) {
UntrackImage(id);
}
if (size != 0) {
m_page_manager.UpdatePageWatchers(false, address, size);
m_page_manager.UpdatePageWatchers<false>(address, size);
}
}
@@ -838,11 +838,20 @@ struct TextureCache::ColorTransferPlan {
TextureUploadLayout layout;
std::vector<vk::BufferImageCopy> regions;
std::vector<GpuTileInfo> tiles;
uint64_t linear_size = 0;
bool tiled = false;
bool swap_bgra16 = false;
bool valid = false;
};
static uint64_t GetLinearSize(std::span<const GpuTileInfo> tiles) {
uint64_t size = 0;
for (const auto& tile: tiles) {
size = std::max(size, tile.linear_offset + tile.linear_size);
}
return size;
}
struct TextureCache::DownloadPlan {
ColorTransferPlan color;
bool depth = false;
@@ -867,22 +876,28 @@ TextureCache::BuildColorTransfer(const Image& image, BindingType binding,
case BindingType::Texture: break;
case BindingType::Storage: owner = "StorageTextureCache"; break;
case BindingType::RenderTarget:
if (info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
info.samples != 1 || image.backing.samples != 1) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
format = ImageOps::RenderTargetTransferFormat(info.bytes_per_block);
allow_depth_tile = false;
plan.swap_bgra16 = info.bgra16;
owner = "RenderTarget";
break;
case BindingType::VideoOut:
if (info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
info.samples != 1 || image.backing.samples != 1 ||
(binding == BindingType::VideoOut &&
info.metadata.compression != VideoOutCompression::Uncompressed)) {
info.metadata.compression != VideoOutCompression::Uncompressed) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
format = binding == BindingType::RenderTarget
? ImageOps::RenderTargetTransferFormat(info.bytes_per_block)
: info.guest_format;
format = info.guest_format;
layers = info.resources.layers;
volume = false;
layered = layers > 1;
allow_depth_tile = false;
plan.swap_bgra16 = info.bgra16;
owner = binding == BindingType::RenderTarget ? "RenderTarget" : "VideoOut";
owner = "VideoOut";
break;
case BindingType::DepthTarget: return plan;
}
@@ -913,6 +928,7 @@ TextureCache::BuildColorTransfer(const Image& image, BindingType binding,
info.resources.levels, plan.tiles)) {
return plan;
}
plan.linear_size = GetLinearSize(plan.tiles);
}
plan.valid = true;
return plan;
@@ -950,11 +966,19 @@ void TextureCache::UploadImage(Image& image, const ImageDesc& desc, Buffer& sour
if (desc.type != BindingType::DepthTarget) {
auto plan = BuildColorTransfer(image, desc.type, TransferDirection::Upload);
EXIT_NOT_IMPLEMENTED(!plan.valid);
if (!plan.valid) {
EXIT("TextureCache: invalid color upload: binding=%u addr=0x%016" PRIx64
" size=0x%016" PRIx64 " format=%u tile=%u family=%u extent=%ux%ux%u "
"pitch=%u levels=%u layers=%u samples=%u\n",
static_cast<uint32_t>(desc.type), info.data.address, info.data.size,
info.guest_format, info.tile_mode, static_cast<uint32_t>(plan.layout.tile_family),
info.extent.width, info.extent.height, info.extent.depth, info.pitch,
info.resources.levels, info.resources.layers, info.samples);
}
TileManager::Result linear {source.Handle(), source_offset, info.data.size};
if (plan.tiled) {
linear = m_tiler->Detile(source.Handle(), source_offset, info.data.size, info.data.size,
plan.tiles);
linear = m_tiler->Detile(source.Handle(), source_offset, info.data.size,
plan.linear_size, plan.tiles);
}
if (plan.swap_bgra16) {
linear = m_tiler->SwapBgra16(linear);
@@ -1038,25 +1062,20 @@ void TextureCache::InitializeImage(ImageId id, const ImageDesc& desc) {
if (image.info.samples > 1) {
return;
}
bool data_gpu_owned = false;
bool data_imported = false;
const bool upload = image.IsBufferModified() || image.IsCpuDirty();
bool data_imported = false;
const bool upload = image.IsBufferModified() || image.IsCpuDirty();
if (upload) {
const auto source =
m_buffer_cache.ObtainBufferForImage(image.info.data.address, image.info.data.size);
if (source.buffer == nullptr) {
EXIT("TextureCache: failed to obtain image upload source\n");
}
data_gpu_owned |= source.gpu_owned;
data_imported = true;
UploadImage(image, desc, *source.buffer, source.offset);
}
if (data_imported) {
image.ClearBufferModified();
}
if (data_gpu_owned) {
image.MarkGpuModified();
}
if (image.IsCpuDirty()) {
image.RefreshComplete();
}
@@ -1131,8 +1150,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
}
ImageId result {};
bool replacement_buffer = false;
bool inserted_new = false;
bool inserted_new = false;
{
std::lock_guard transaction(m_resource_mutex);
CacheLock lock(*this, m_lock);
@@ -1159,8 +1177,8 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
if (owner == nullptr) {
continue;
}
const auto merged_info = result ? ResolveImage(result).info : desc.info;
const auto overlap = ResolveOverlap(merged_info, desc.type, candidate, result);
const auto& merged_info = result ? ResolveImage(result).info : desc.info;
const auto overlap = ResolveOverlap(merged_info, desc.type, candidate, result);
if (overlap.image) {
result = overlap.image;
view_mip = overlap.mip;
@@ -1174,23 +1192,15 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
if (exact_format && resolved.info.pixel_format != desc.info.pixel_format) {
result = {};
} else if (resolved.info.resources < desc.info.resources) {
ImageDesc refresh {
.info = resolved.info, .view_info = {}, .type = UploadBinding(resolved)};
RefreshImage(result, refresh);
if (resolved.IsGpuModified() && !SynchronizeImageToBuffer(result)) {
EXIT("TextureCache: cannot preserve an unsupported replacement image\n");
}
replacement_buffer = resolved.IsBufferModified();
DeleteImage(result);
result = {};
result = ExpandImage(desc.info, result);
}
}
if (!result) {
result = InsertImage(desc.info);
inserted_new = true;
auto& inserted = ResolveImage(result);
if (replacement_buffer || m_buffer_cache.HasGpuDirtyBytes(inserted.info.data.address,
inserted.info.data.size)) {
if (m_buffer_cache.HasGpuDirtyBytes(inserted.info.data.address,
inserted.info.data.size)) {
inserted.MarkBufferModified();
}
}
@@ -1360,11 +1370,6 @@ void TextureCache::CommitGpuWrite(Image& image) {
if (image.depth_id || image.backing.image == nullptr) {
EXIT("TextureCache: stencil association cannot own image contents\n");
}
const auto range = image.info.data;
if (m_buffer_cache.HasGpuDirtyBytes(range.address, range.size)) {
m_buffer_cache.DiscardGpuDirtyBytes(range.address, range.size);
}
m_buffer_cache.InvalidateImageAliases(range.address, range.size);
image.ClearBufferModified();
if (image.IsCpuDirty()) {
image.RefreshComplete();
@@ -1429,9 +1434,6 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
return false;
}
}
if (m_buffer_cache.HasGpuDirtyBytes(address, size)) {
m_buffer_cache.DiscardGpuDirtyBytes(address, size);
}
if (image.IsBufferModified() || image.IsCpuDirty()) {
ImageDesc refresh {.info = image.info, .view_info = {}, .type = UploadBinding(image)};
InitializeImage(selected, refresh);
@@ -1551,11 +1553,14 @@ void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t des
}
void TextureCache::DownloadImageData(Image& image, Buffer& destination, uint64_t destination_offset,
DownloadPlan plan) {
uint64_t destination_size, DownloadPlan plan) {
if (!plan.valid) {
EXIT("TextureCache: invalid image download plan\n");
}
if (plan.depth) {
if (destination_size != image.info.data.size) {
EXIT("TextureCache: partial depth image download is unsupported\n");
}
DownloadDepth(image, destination, destination_offset);
return;
}
@@ -1565,22 +1570,119 @@ void TextureCache::DownloadImageData(Image& image, Buffer& destination, uint64_t
: TileManager::ColorTransform::None;
if (!color.tiled) {
if (transform == TileManager::ColorTransform::SwapBgra16) {
auto linear = m_tiler->GetScratchBuffer(image.info.data.size);
auto linear = m_tiler->GetScratchBuffer(destination_size);
image.Download(color.regions, linear.buffer, 0, linear.size);
m_tiler->SwapBgra16(linear,
{destination.Handle(), destination_offset, image.info.data.size});
{destination.Handle(), destination_offset, destination_size});
return;
}
for (auto& copy: color.regions) {
copy.bufferOffset += destination_offset;
}
image.Download(color.regions, destination.Handle(), destination_offset,
image.info.data.size);
image.Download(color.regions, destination.Handle(), destination_offset, destination_size);
return;
}
m_tiler->TileImage(image, color.regions, destination.Handle(), destination_offset,
image.info.data.size, image.info.data.size, color.tiles, transform);
destination_size, color.linear_size, color.tiles, transform);
}
bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size) {
CacheLock lock(m_texture_cache, m_texture_cache.m_lock);
std::vector<ImageId> matches;
for (const auto id: m_texture_cache.FindImagesInRegion(vaddr, size, false)) {
auto owner = m_texture_cache.ResolveOwner(id);
if (owner == nullptr || owner->info.data.address != vaddr) {
continue;
}
if (owner->depth_id) {
owner = m_texture_cache.ResolveOwner(owner->depth_id);
}
if (owner != nullptr && owner->SafeToDownload()) {
matches.push_back(id);
}
}
ImageId selected {};
if (matches.size() == 1) {
selected = matches.front();
} else {
for (const auto id: matches) {
const auto& image = m_texture_cache.ResolveImage(id);
if (image.info.data.size == size) {
selected = id;
break;
}
}
}
if (!selected) {
return false;
}
if (const auto owner = m_texture_cache.ResolveOwner(selected);
owner != nullptr && owner->depth_id) {
selected = owner->depth_id;
}
auto& image = m_texture_cache.ResolveImage(selected);
if (!buffer.IsInBounds(image.info.data.address, 1)) {
return false;
}
const auto buf_offset = buffer.Offset(image.info.data.address);
const auto available = buffer.Size() - buf_offset;
uint32_t levels = 0;
uint64_t copy_size = 0;
if (image.info.IsVolume()) {
// Volume mips contain strided block slices, so a mip's linear span cannot prove that
// every retained slice fits. Keep volume synchronization whole-image only.
if (!buffer.IsInBounds(image.info.data.address, image.info.data.size)) {
return false;
}
levels = image.info.resources.levels;
copy_size = image.info.data.size;
} else {
for (; levels < image.info.resources.levels; ++levels) {
const auto& mip = image.info.mip_layout[levels];
if (mip.size == 0 || mip.offset > available || mip.size > available - mip.offset) {
break;
}
copy_size = std::max(copy_size, mip.offset + mip.size);
}
}
if (copy_size == 0) {
return false;
}
auto plan = m_texture_cache.BuildDownload(image);
if (!plan.valid) {
return false;
}
if (plan.depth && copy_size != image.info.data.size) {
return false;
}
if (!plan.depth && levels < image.info.resources.levels) {
auto& color = plan.color;
std::erase_if(color.regions, [levels](const vk::BufferImageCopy& region) {
return region.imageSubresource.mipLevel >= levels;
});
if (color.regions.empty()) {
return false;
}
if (color.tiled) {
const auto binding = m_texture_cache.UploadBinding(image);
const auto format =
binding == TextureCache::BindingType::RenderTarget
? ImageOps::RenderTargetTransferFormat(image.info.bytes_per_block)
: image.info.guest_format;
color.tiles.clear();
if (!TextureBuildGpuTileInfos(copy_size, color.regions, color.layout, format,
image.info.TransferLayers(), levels, color.tiles)) {
return false;
}
color.linear_size = GetLinearSize(color.tiles);
}
}
m_texture_cache.DownloadImageData(image, buffer, buf_offset, copy_size, std::move(plan));
m_texture_cache.RetainImage(m_scheduler.Current(), selected);
return true;
}
std::pair<uint8_t*, uint64_t> TextureCache::MapDownload(uint64_t size, uint64_t alignment) {
@@ -1612,12 +1714,9 @@ void TextureCache::QueueDownload(GuestRange range, StreamBuffer& download, uint8
m_scheduler.Current().Handle().pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
vk::PipelineStageFlagBits::eHost, {}, 0, nullptr,
1, &barrier, 0, nullptr);
const auto tick = m_scheduler.CurrentTick();
m_buffer_cache.BeginBackingPublication(range.address, range.size, tick);
m_scheduler.DeferPriorityOperation([this, &download, range, mapped, offset, tick] {
m_scheduler.DeferPriorityOperation([&download, range, mapped, offset] {
download.Invalidate(offset, range.size);
LibKernel::Memory::WriteBacking(range.address, mapped, range.size);
m_buffer_cache.CompleteBackingPublication(range.address, range.size, tick);
});
}
@@ -1638,7 +1737,7 @@ bool TextureCache::TryDownloadImage(ImageId id) {
}
download.Flush(offset, range.size);
DownloadImageData(image, download, offset, std::move(plan));
DownloadImageData(image, download, offset, range.size, std::move(plan));
QueueDownload(range, download, mapped, offset);
return true;
@@ -1652,62 +1751,6 @@ void TextureCache::DownloadImage(ImageId id) {
m_scheduler.DrainPriorityOperations();
}
bool TextureCache::SynchronizeImageToBuffer(ImageId id) {
auto& image = ResolveImage(id);
if (image.depth_id) {
return true;
}
auto plan = BuildDownload(image);
if (!plan.valid) {
return false;
}
const auto range = image.info.data;
if (image.IsCpuDirty()) {
RefreshImage(id,
ImageDesc {.info = image.info, .view_info = {}, .type = UploadBinding(image)});
}
if (!image.IsGpuModified()) {
return true;
}
if (image.IsDefinitelyCpuDirty() || image.IsBufferModified()) {
EXIT("TextureCache: image mirror source is not native-current\n");
}
auto [destination, offset] =
m_buffer_cache.ObtainBufferForImageWrite(range.address, range.size);
if (destination == nullptr) {
EXIT("TextureCache: failed to allocate image mirror\n");
}
DownloadImageData(image, *destination, offset, std::move(plan));
m_scheduler.Current().RetainResourceUntilFence(destination);
m_buffer_cache.PublishImageBuffer(range.address, range.size);
image.MarkBufferModified();
RetainImage(m_scheduler.Current(), id);
ClearGpuModified(id);
return true;
}
bool TextureCache::SynchronizeImageToBuffer(uint64_t address, uint64_t size) {
if (!GuestRange {address, size}.Valid()) {
return false;
}
CacheLock lock(*this, m_lock);
ImageId selected {};
for (const auto id: FindImagesInRegion(address, size, true)) {
auto owner = ResolveOwner(id);
if (owner == nullptr || !owner->GpuOverlaps(address, size)) {
continue;
}
if (selected) {
EXIT("TextureCache: ambiguous image-to-buffer synchronization\n");
}
selected = id;
}
if (!selected) {
return false;
}
return SynchronizeImageToBuffer(selected);
}
bool TextureCache::InvalidateMemoryFromGPU(uint64_t address, uint64_t size,
bool formatted_buffer_write) {
if (!GuestRange {address, size}.Valid()) {
@@ -1826,37 +1869,6 @@ bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) {
return true;
}
bool TextureCache::InvalidateMemory(PageFaultAccess access, uint64_t address, uint64_t size,
PageFaultPhase phase) noexcept {
if ((access != PageFaultAccess::Read && access != PageFaultAccess::Write) ||
!GuestRange {address, size}.Valid()) {
return false;
}
if (access == PageFaultAccess::Read) {
return false;
}
if (phase == PageFaultPhase::Invalidate) {
CacheLock lock(*this, m_lock);
const bool tracked =
std::ranges::any_of(FindImagesInRegion(address, size, true), [&](ImageId id) {
const auto owner = ResolveOwner(id);
return owner != nullptr && !owner->depth_id && owner->IsTracked();
});
if (tracked) {
InvalidateCpuAliases(address, size);
}
return tracked;
}
if (phase != PageFaultPhase::Complete && phase != PageFaultPhase::Release) {
return false;
}
CacheLock lock(*this, m_lock);
return std::ranges::any_of(FindImagesInRegion(address, size, true), [&](ImageId id) {
const auto owner = ResolveOwner(id);
return owner != nullptr && !owner->depth_id;
});
}
void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
if (!GuestRange {address, size}.Valid()) {
EXIT("TextureCache: invalid unmap range\n");
+5 -8
View File
@@ -66,7 +66,6 @@ public:
[[nodiscard]] bool ClearImageFromBuffer(CommandBuffer& command, uint64_t address, uint64_t size,
uint32_t packed_clear);
void InvalidateMemory(uint64_t address, uint64_t size);
[[nodiscard]] bool SynchronizeImageToBuffer(uint64_t address, uint64_t size);
[[nodiscard]] bool InvalidateMemoryFromGPU(uint64_t address, uint64_t size,
bool formatted_buffer_write = false);
[[nodiscard]] RegionInfo QueryRegion(uint64_t address, uint64_t size);
@@ -76,11 +75,9 @@ public:
[[nodiscard]] bool ClearMeta(uint64_t address);
[[nodiscard]] bool TouchMeta(uint64_t address, uint32_t slice, bool is_clear);
[[nodiscard]] bool InvalidateMemory(PageFaultAccess access, uint64_t address, uint64_t size,
PageFaultPhase phase) noexcept;
void UnmapMemory(uint64_t address, uint64_t size);
void ProcessDownloadImages();
void RunGarbageCollector();
void UnmapMemory(uint64_t address, uint64_t size);
void ProcessDownloadImages();
void RunGarbageCollector();
private:
enum class TransferDirection { Upload, Download };
@@ -142,7 +139,7 @@ private:
[[nodiscard]] DownloadPlan BuildDownload(const Image& image) const;
void UploadImage(Image& image, const ImageDesc& desc, Buffer& source, uint64_t source_offset);
void DownloadImageData(Image& image, Buffer& destination, uint64_t destination_offset,
DownloadPlan plan);
uint64_t destination_size, DownloadPlan plan);
void DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset);
void CommitGpuWrite(Image& image);
void PrepareImageCopy(Image& image);
@@ -157,7 +154,6 @@ private:
void InvalidateCpuAliases(uint64_t address, uint64_t size);
void ClearGpuModified(ImageId id);
[[nodiscard]] bool SynchronizeImageToBuffer(ImageId id);
void DownloadImage(ImageId id);
[[nodiscard]] bool TryDownloadImage(ImageId id);
[[nodiscard]] std::pair<uint8_t*, uint64_t> MapDownload(uint64_t size, uint64_t alignment);
@@ -186,6 +182,7 @@ private:
bool m_readback_linear_images = false;
friend struct TextureCacheTestAccess;
friend class BufferCache;
friend class RenderExecutor;
};
@@ -119,7 +119,18 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
uint32_t pitch = 0;
uint64_t size = 0;
bool tile = false;
const bool standard64 =
const bool volume = rt.attrib3.dimension == 2;
if (rt.attrib3.dimension != 1 && !volume) {
EXIT("unsupported render-target dimension: %u\n", rt.attrib3.dimension);
}
if (!volume && rt.attrib3.depth != 0) {
EXIT("2D render target has nonzero depth: %u\n", rt.attrib3.depth);
}
if (volume && samples != 1) {
EXIT("multisampled 3D render targets are unsupported\n");
}
const uint32_t depth = volume ? rt.attrib3.depth + 1u : 1u;
const bool standard64 =
rt.attrib3.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kStandard64KB);
switch (rt.attrib3.tile_mode) {
@@ -145,6 +156,7 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
if (bytes_per_element == 0) {
EXIT("render-target format has no valid element size\n");
}
const auto transfer_format = ImageOps::RenderTargetTransferFormat(bytes_per_element);
if (standard64 &&
(rt.attrib3.dimension != 1 || rt.attrib3.depth != 0 || levels != 1 ||
rt.view.current_mip_level != 0 || view.base_layer != 0 || view.image_layers != 1 ||
@@ -165,10 +177,14 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
if (rt.pitch.pitch_div8_minus1 != 0) {
pitch = (rt.pitch.pitch_div8_minus1 + 1u) << 3u;
} else if (tile) {
pitch = standard64
? TileGetTexturePitch(Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float),
width, levels, rt.attrib3.tile_mode)
: TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
if (volume) {
pitch = TileGetTexturePitch(transfer_format, width, levels, rt.attrib3.tile_mode);
} else if (standard64) {
pitch = TileGetTexturePitch(Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float),
width, levels, rt.attrib3.tile_mode);
} else {
pitch = TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
}
if (pitch == 0) {
EXIT("unsupported render-target pitch: width=%u bytes=%u\n", width, bytes_per_element);
}
@@ -176,9 +192,19 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
pitch = width;
}
TileSizeOffset mip_sizes[16] {};
TilePaddedSize mip_padded[16] {};
if (tile) {
TileSizeOffset mip_sizes[16] {};
TilePaddedSize mip_padded[16] {};
TileVolumeLayout volume_layout {};
uint64_t backing_size = 0;
if (volume) {
if (!tile || !TileGetTextureVolumeLayout(transfer_format, width, height, depth, levels,
rt.attrib3.tile_mode, volume_layout)) {
EXIT("unsupported 3D render-target layout: %ux%ux%u levels=%u tile=%u\n", width, height,
depth, levels, rt.attrib3.tile_mode);
}
size = volume_layout.block_slice_size;
backing_size = volume_layout.total_size;
} else if (tile) {
TileSizeAlign layout {};
bool valid_layout = false;
if (standard64) {
@@ -203,12 +229,6 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
mip_sizes[0] = {static_cast<uint32_t>(size), 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (rt.slice.slice_div64_minus1 != 0 &&
(static_cast<uint64_t>(rt.slice.slice_div64_minus1) + 1u) * 64u != size) {
EXIT("render-target slice span mismatch: encoded=0x%016" PRIx64 " derived=0x%016" PRIx64
"\n",
(static_cast<uint64_t>(rt.slice.slice_div64_minus1) + 1u) * 64u, size);
}
} else {
size = static_cast<uint64_t>(pitch) * height * bytes_per_element * samples;
if (size > UINT32_MAX) {
@@ -217,40 +237,66 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
mip_sizes[0] = {static_cast<uint32_t>(size), 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (size == 0 || size > UINT64_MAX / view.image_layers) {
if (rt.slice.slice_div64_minus1 != 0 &&
(static_cast<uint64_t>(rt.slice.slice_div64_minus1) + 1u) * 64u != size) {
EXIT("render-target slice span mismatch: encoded=0x%016" PRIx64 " derived=0x%016" PRIx64
"\n",
(static_cast<uint64_t>(rt.slice.slice_div64_minus1) + 1u) * 64u, size);
}
if (size == 0 || (!volume && size > UINT64_MAX / view.image_layers)) {
EXIT("render-target memory footprint is invalid\n");
}
const auto backing_size = size * view.image_layers;
if (!volume) {
backing_size = size * view.image_layers;
}
if (backing_size == 0) {
EXIT("render-target backing is empty\n");
}
if (backing_size > TRACKER_ADDRESS_SIZE - rt.base.addr) {
EXIT("render-target backing range is invalid\n");
}
const vk::Extent2D view_extent = {std::max(width >> rt.view.current_mip_level, 1u),
std::max(height >> rt.view.current_mip_level, 1u)};
const uint32_t view_depth = std::max(depth >> rt.view.current_mip_level, 1u);
if (volume &&
(view.base_layer >= view_depth || view.layer_count > view_depth - view.base_layer)) {
EXIT("3D render-target view exceeds mip depth: base=%u count=%u depth=%u mip=%u\n",
view.base_layer, view.layer_count, view_depth, rt.view.current_mip_level);
}
auto decision_log_id = g_render_color_log_count.fetch_add(1);
if (decision_log_id < 128) {
LOGF("RenderColorTarget: slot=%" PRIu32 " addr=0x%010" PRIx64 " size=0x%016" PRIx64
" extent=%ux%u view_mip=%u view_extent=%ux%u levels=%u pitch=%u"
" extent=%ux%ux%u view_mip=%u view_extent=%ux%u levels=%u pitch=%u"
" fmt=0x%08" PRIx32 " nfmt=0x%08" PRIx32 " order=0x%08" PRIx32 " samples=%u tile=%s\n",
rt_slot, rt.base.addr, backing_size, width, height, rt.view.current_mip_level,
rt_slot, rt.base.addr, backing_size, width, height, depth, rt.view.current_mip_level,
view_extent.width, view_extent.height, levels, pitch, rt.info.format,
rt.info.channel_type, rt.info.channel_order, samples, tile ? "tiled" : "linear");
}
TextureCache::ImageDesc desc {};
desc.type = TextureCache::BindingType::RenderTarget;
desc.info.data = {rt.base.addr, backing_size};
desc.info.pixel_format = target_format.format;
desc.info.guest_format = ImageOps::RenderTargetTransferFormat(bytes_per_element);
desc.info.type = Prospero::ImageType::kColor2D;
desc.info.extent = {width, height, 1};
desc.info.resources = {levels, view.image_layers};
desc.info.pitch = pitch;
desc.type = TextureCache::BindingType::RenderTarget;
desc.info.data = {rt.base.addr, backing_size};
desc.info.pixel_format = target_format.format;
desc.info.guest_format = transfer_format;
desc.info.type = volume ? Prospero::ImageType::kColor3D : Prospero::ImageType::kColor2D;
desc.info.extent = {width, height, depth};
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.pitch = pitch;
desc.info.bytes_per_block = bytes_per_element;
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
desc.info.mip_layout[level] = {
volume_layout.level_offsets[level],
volume_layout.level_sizes[level],
volume_layout.level_widths[level],
volume_layout.level_heights[level],
};
continue;
}
const auto level_offset =
mip_sizes[level].src_size != 0 ? mip_sizes[level].src_offset : mip_sizes[level].offset;
const auto level_size =
+10 -20
View File
@@ -359,30 +359,12 @@ static void RtCheck(const HW::RenderTarget& rt) {
logged = true;
}
}
if (rt.attrib3.depth != 0x00000000) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: ignoring PS5 color target depth_minus1=0x%08" PRIx32
"\n",
rt.attrib3.depth);
logged = true;
}
}
if (!RenderIsColorTileMode(rt.attrib3.tile_mode)) {
EXIT("unknown PS5 render-target tile mode: 0x%08" PRIx32 "\n", rt.attrib3.tile_mode);
}
if (!RenderIsColorDimension(rt.attrib3.dimension)) {
EXIT("unknown PS5 render-target dimension: 0x%08" PRIx32 "\n", rt.attrib3.dimension);
}
if (rt.attrib3.dimension != 0x00000001) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: using 2D fallback for PS5 color "
"dimension=0x%08" PRIx32 "\n",
rt.attrib3.dimension);
logged = true;
}
}
if (!rt.attrib3.cmask_pipe_aligned) {
static bool logged = false;
if (!logged) {
@@ -497,7 +479,15 @@ static void ZPrint(const char* func, const HW::DepthRenderTarget& z) {
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
static void ZCheck(const HW::DepthRenderTarget& z) {
static void ZCheck(const HW::DepthRenderTarget& z, const HW::DepthControl& dc,
const HW::RenderControl& rc) {
const bool depth_active =
dc.z_enable || dc.z_write_enable || dc.depth_bounds_enable || rc.depth_clear_enable;
const bool stencil_active = dc.stencil_enable || rc.stencil_clear_enable;
if (!depth_active && !stencil_active) {
return;
}
EXIT_NOT_IMPLEMENTED(!z.z_info.HasValidTextureCompatibility());
EXIT_NOT_IMPLEMENTED(!z.stencil_info.HasValidTextureCompatibility());
if (z.z_info.format == 0) {
@@ -1206,7 +1196,7 @@ void hw_check(const RenderCommandBuffer& buffer) {
log_phase("vp");
VpCheck(vp, smc);
log_phase("z");
ZCheck(z);
ZCheck(z, d, rc);
log_phase("clip");
ClipCheck(c);
log_phase("rc");
@@ -96,9 +96,11 @@ void RenderExecutor::ResolveRenderDepthTarget(uint64_t submit_id, RenderCommandB
const auto& dc = hw.GetDepthControl();
const auto& sc = hw.GetStencilControl();
const auto& sm = hw.GetStencilMask();
const bool depth_active =
const bool has_stencil =
z.stencil_info.format != Prospero::GpuEnumValue(Prospero::StencilFormat::kInvalid);
const bool depth_active =
dc.z_enable || dc.z_write_enable || dc.depth_bounds_enable || rc.depth_clear_enable;
const bool stencil_active = dc.stencil_enable || rc.stencil_clear_enable;
const bool stencil_active = has_stencil && (dc.stencil_enable || rc.stencil_clear_enable);
if (!depth_active && !stencil_active) {
return;
}
@@ -139,8 +141,6 @@ void RenderExecutor::ResolveRenderDepthTarget(uint64_t submit_id, RenderCommandB
}
return;
}
const bool has_stencil =
z.stencil_info.format != Prospero::GpuEnumValue(Prospero::StencilFormat::kInvalid);
const bool has_htile = z.z_info.htile_acceleration;
const auto samples = render_sample_count(z.z_info.num_samples);
if (samples == 0) {
@@ -156,8 +156,8 @@ void RenderExecutor::ResolveRenderDepthTarget(uint64_t submit_id, RenderCommandB
DepthFatal("invalid depth view: base=%u last=%u", z.depth_view.slice_start,
z.depth_view.slice_max);
}
if ((stencil_active && !has_stencil) || rc.resummarize_enable || rc.copy_centroid ||
rc.copy_sample != 0 || z.z_info.expclear_enabled || z.stencil_info.expclear_enabled ||
if (rc.resummarize_enable || rc.copy_centroid || rc.copy_sample != 0 ||
z.z_info.expclear_enabled || z.stencil_info.expclear_enabled ||
z.z_info.partially_resident || z.stencil_info.partially_resident ||
z.z_info.max_mip_level != 0 || z.depth_view.current_mip_level != 0 ||
z.depth_info.addr5_swizzle_mask != 0 || z.depth_info.array_mode != 0 ||
@@ -279,10 +279,10 @@ void RenderExecutor::ResolveRenderDepthTarget(uint64_t submit_id, RenderCommandB
r.depth_min_bounds = hw.GetDepthBoundsMin();
r.depth_max_bounds = hw.GetDepthBoundsMax();
r.stencil_clear_enable = rc.stencil_clear_enable;
r.stencil_clear_enable = has_stencil && rc.stencil_clear_enable;
r.stencil_clear_value = hw.GetStencilClearValue();
r.stencil_test_enable = dc.stencil_enable;
if (dc.stencil_enable) {
r.stencil_test_enable = has_stencil && dc.stencil_enable;
if (r.stencil_test_enable) {
if (dc.stencilfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
(dc.backface_enable &&
dc.stencilfunc_bf > static_cast<uint8_t>(vk::CompareOp::eAlways)) ||
@@ -105,6 +105,10 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout destinat
std::optional<ImageSubresourceRange> range) {
auto& state = backing.state;
auto& subresource_states = backing.subresource_states;
if (range && info.IsVolume()) {
range->base_layer = 0;
range->layer_count = 1;
}
const bool partial =
range && (range->base_level != 0 || range->level_count != info.resources.levels ||
@@ -70,6 +70,10 @@ constexpr RenderTargetFormatMapping kRenderTargetFormats[] = {
Prospero::ChannelType::kFloat,
Prospero::ChannelOrder::kStandard,
{vk::Format::eB10G11R11UfloatPack32, 4}},
{Prospero::ChannelLayout::k5_6_5,
Prospero::ChannelType::kUNorm,
Prospero::ChannelOrder::kStandard,
{vk::Format::eB5G6R5UnormPack16, 2}},
{Prospero::ChannelLayout::k16,
Prospero::ChannelType::kUNorm,
Prospero::ChannelOrder::kStandard,
@@ -478,10 +482,10 @@ static bool SetGpuTileSize(uint64_t offset, uint64_t length, uint64_t capacity,
return true;
}
bool TextureBuildGpuTileInfos(uint64_t size, const std::vector<vk::BufferImageCopy>& regions,
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t fmt, uint32_t depth,
uint64_t levels, std::vector<GpuTileInfo>& out_infos) {
if (size == 0 || levels == 0 || levels > 16 || depth == 0 ||
if (tiled_size == 0 || levels == 0 || levels > 16 || depth == 0 ||
regions.size() != GetTextureRegionCount(depth, levels, layout.volume_texture) ||
Prospero::IsFmaskTextureFormat(fmt)) {
return false;
@@ -534,8 +538,9 @@ bool TextureBuildGpuTileInfos(uint64_t size, const std::vector<vk::BufferImageCo
const uint64_t linear_span =
static_cast<uint64_t>(copy_depth - 1u) * linear_stride +
layout.level_sizes[level].size;
if (!SetGpuTileSize(info.linear_offset, linear_span, size, info.linear_size) ||
!SetGpuTileSize(info.tiled_offset, volume.level_sizes[level], size,
if (!SetGpuTileSize(info.linear_offset, linear_span, UINT64_MAX,
info.linear_size) ||
!SetGpuTileSize(info.tiled_offset, volume.level_sizes[level], tiled_size,
info.tiled_size)) {
return false;
}
@@ -584,8 +589,9 @@ bool TextureBuildGpuTileInfos(uint64_t size, const std::vector<vk::BufferImageCo
info.bytes_per_element = block.bytes_per_element;
info.linear_offset = region.bufferOffset;
info.tiled_offset = TextureUploadSliceSourceOffset(layout, level, z);
if (!SetGpuTileSize(info.linear_offset, level_size.size, size, info.linear_size) ||
!SetGpuTileSize(info.tiled_offset, GetLevelSrcSize(level_size), size,
if (!SetGpuTileSize(info.linear_offset, level_size.size, UINT64_MAX,
info.linear_size) ||
!SetGpuTileSize(info.tiled_offset, GetLevelSrcSize(level_size), tiled_size,
info.tiled_size)) {
return false;
}
@@ -44,7 +44,7 @@ std::vector<vk::BufferImageCopy> TextureBuildImageCopies(const TextureUploadLayo
uint32_t width, uint32_t height,
uint32_t depth, uint64_t levels,
bool array_texture, bool volume_texture);
bool TextureBuildGpuTileInfos(uint64_t size, const std::vector<vk::BufferImageCopy>& regions,
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t fmt, uint32_t depth,
uint64_t levels, std::vector<GpuTileInfo>& infos);
@@ -270,7 +270,7 @@ bool IsSupportedDepthTargetDescriptor(const ShaderTextureResource& descriptor, c
supported_msaa_array) &&
levels_ok && descriptor.MinLod() == 0 &&
descriptor.TileMode() == Prospero::GpuEnumValue(Prospero::TileMode::kDepth) &&
descriptor.BCSwizzle() == 0 && descriptor.MsaaDepth() == multisampled &&
descriptor.BCSwizzle() == 0 && (!descriptor.MsaaDepth() || multisampled) &&
pitch >= width && pitch == image.info.pitch;
}
@@ -391,9 +391,8 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
const bool supported_swizzle =
IsValidImageSwizzle(swizzle) &&
(swizzle == DstSel(4, 5, 6, 7) || !resource.read || resource.atomic);
const bool supported_mip_view = descriptor.BaseLevel() == 0 || is_1d || is_2d;
return (is_1d || is_1d_array || is_2d || is_2d_array || is_3d) && supported_tile &&
supported_mip_view && descriptor.BaseLevel() == descriptor.LastLevel() &&
descriptor.BaseLevel() == descriptor.LastLevel() &&
descriptor.LastLevel() <= descriptor.MaxMip() && descriptor.MinLod() == 0 &&
supported_swizzle && descriptor.BCSwizzle() == 0 && !descriptor.MsaaDepth();
}
@@ -424,10 +423,14 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const bool encoding_ok = IsSupportedStorageTextureEncoding(descriptor);
const bool uint_resource =
resource.kind == ShaderRecompiler::IR::ResourceKind::StorageImageUint;
const bool raw_sint_storage =
format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32SInt) && uint_resource &&
resource.written && !resource.read && !resource.atomic;
const bool format_ok =
Prospero::IsSupportedTextureFormat(format) &&
uint_resource == Prospero::IsUintTextureFormat(format) &&
(!resource.atomic || format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32UInt));
raw_sint_storage ||
(Prospero::IsSupportedTextureFormat(format) &&
uint_resource == Prospero::IsUintTextureFormat(format) &&
(!resource.atomic || format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32UInt)));
if (resource_ok && descriptor_ok && encoding_ok && format_ok && size != 0) {
return;
}
@@ -614,17 +617,25 @@ RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& reso
const bool multisampled = IsMultisampledTexture(type);
const auto levels = multisampled ? 1u : static_cast<uint32_t>(descriptor.MaxMip()) + 1u;
const auto tile = descriptor.TileMode();
const bool depth_tile = tile == Prospero::GpuEnumValue(Prospero::TileMode::kDepth);
const bool msaa_tile =
tile == Prospero::GpuEnumValue(descriptor.MsaaDepth() ? Prospero::TileMode::kDepth
: Prospero::TileMode::kRenderTarget);
depth_tile || tile == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget);
const bool msaa_array = type == Prospero::ImageType::kColor2DMsaaArray;
if ((!multisampled && (base_level > last_level || last_level >= levels)) ||
(multisampled &&
(base_level != 0 || last_level == 0 || last_level > 3 ||
descriptor.MaxMip() != last_level || !msaa_tile ||
(descriptor.MsaaDepth() && !depth_tile) ||
(!msaa_array && (descriptor.Depth() != 0 || descriptor.BaseArray5() != 0))))) {
EXIT("unsupported texture mip view: base=%u last=%u levels=%u\n", base_level, last_level,
levels);
EXIT("unsupported texture mip view: base=%u last=%u levels=%u max=%u type=%u tile=%u "
"kind=%u dimension=%u mip_mode=%u read=%d written=%d "
"dwords=%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x\n",
base_level, last_level, levels, descriptor.MaxMip(), descriptor.Type(), tile,
static_cast<uint32_t>(resource.kind), static_cast<uint32_t>(resource.dimension),
static_cast<uint32_t>(resource.mip_mode), resource.read, resource.written,
descriptor.fields[0], descriptor.fields[1], descriptor.fields[2], descriptor.fields[3],
descriptor.fields[4], descriptor.fields[5], descriptor.fields[6],
descriptor.fields[7]);
}
const auto samples = multisampled ? 1u << last_level : 1u;
const auto view_levels =
@@ -651,7 +662,8 @@ RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& reso
TileSizeAlign size {};
if (multisampled) {
const auto bytes = Prospero::NumBytesPerElement(format);
pitch = TileGetRenderTargetPitch(width, bytes, last_level);
pitch = depth_tile ? TileGetDepthPitch(width, bytes, last_level)
: TileGetRenderTargetPitch(width, bytes, last_level);
if (pitch == 0 || !TileGetRenderTargetSize(width, height, pitch, bytes, size, last_level) ||
size.size > UINT32_MAX / image_layers) {
EXIT("unsupported multisample texture layout\n");
@@ -668,8 +680,11 @@ RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& reso
ValidateStorageTexture(resource, descriptor, size.size);
}
const auto pixel_format = TextureGetFormat(format);
const auto storage_view_format = SrgbStorageViewFormat(pixel_format);
const auto pixel_format = TextureGetFormat(format);
const auto storage_view_format =
storage && format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32SInt)
? vk::Format::eR32Uint
: SrgbStorageViewFormat(pixel_format);
const auto view_format = storage && storage_view_format != vk::Format::eUndefined
? storage_view_format
: pixel_format;
@@ -154,8 +154,9 @@ PipelineCache::GraphicsPipeline& PipelineCache::CreateGraphicsPipeline(
for (uint32_t i = 0; i < RENDER_COLOR_ATTACHMENTS_MAX; i++) {
static_params.color_mask[i] = color_mask[i];
}
static_params.cull_back = mc.cull_back;
static_params.cull_front = mc.cull_front;
const bool rect_list = topology == vk::PrimitiveTopology::ePatchList;
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
static_params.face = mc.face;
for (uint32_t i = 0; i < color_count; i++) {
@@ -14,6 +14,7 @@
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
#include <algorithm>
@@ -463,8 +464,12 @@ void CreatePipelineInternal(
uint32_t ps_hash0, uint32_t ps_crc32, bool ps_active) {
EXIT_IF(ps_active && ps_input_info == nullptr);
vk::ShaderModule vert_shader_module = nullptr;
vk::ShaderModule frag_shader_module = nullptr;
const bool rect_list = static_params.topology == vk::PrimitiveTopology::ePatchList;
vk::ShaderModule vert_shader_module = nullptr;
vk::ShaderModule tess_control_shader_module = nullptr;
vk::ShaderModule tess_eval_shader_module = nullptr;
vk::ShaderModule frag_shader_module = nullptr;
vk::ShaderModuleCreateInfo create_info {};
@@ -491,8 +496,33 @@ void CreatePipelineInternal(
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
}
if (rect_list) {
const auto shaders =
BuildRectListShaders(vs_input_info, ps_active ? ps_input_info : nullptr);
create_info.codeSize = shaders.control.size() * 4;
create_info.pCode = shaders.control.data();
result =
graphics.device.createShaderModule(&create_info, nullptr, &tess_control_shader_module);
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done result=%s module=%p\n",
VulkanToString(result).c_str(), static_cast<void*>(tess_control_shader_module));
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
create_info.codeSize = shaders.evaluation.size() * 4;
create_info.pCode = shaders.evaluation.data();
result =
graphics.device.createShaderModule(&create_info, nullptr, &tess_eval_shader_module);
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreateShaderModule RectList TES done result=%s module=%p\n",
VulkanToString(result).c_str(), static_cast<void*>(tess_eval_shader_module));
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
}
EXIT_NOT_IMPLEMENTED(vert_shader_module == nullptr);
EXIT_NOT_IMPLEMENTED(
rect_list && (tess_control_shader_module == nullptr || tess_eval_shader_module == nullptr));
EXIT_NOT_IMPLEMENTED(ps_active && frag_shader_module == nullptr);
vk::PipelineShaderStageCreateInfo vert_shader_stage_info {};
@@ -525,9 +555,28 @@ void CreatePipelineInternal(
frag_shader_stage_info);
}
vk::PipelineShaderStageCreateInfo shader_stages[] = {vert_shader_stage_info,
frag_shader_stage_info};
const uint32_t shader_stage_count = ps_active ? 2u : 1u;
vk::PipelineShaderStageCreateInfo tess_control_shader_stage_info {};
tess_control_shader_stage_info.sType = vk::StructureType::ePipelineShaderStageCreateInfo;
tess_control_shader_stage_info.stage = vk::ShaderStageFlagBits::eTessellationControl;
tess_control_shader_stage_info.module = tess_control_shader_module;
tess_control_shader_stage_info.pName = "main";
vk::PipelineShaderStageCreateInfo tess_eval_shader_stage_info {};
tess_eval_shader_stage_info.sType = vk::StructureType::ePipelineShaderStageCreateInfo;
tess_eval_shader_stage_info.stage = vk::ShaderStageFlagBits::eTessellationEvaluation;
tess_eval_shader_stage_info.module = tess_eval_shader_module;
tess_eval_shader_stage_info.pName = "main";
vk::PipelineShaderStageCreateInfo shader_stages[4] = {};
uint32_t shader_stage_count = 0;
shader_stages[shader_stage_count++] = vert_shader_stage_info;
if (rect_list) {
shader_stages[shader_stage_count++] = tess_control_shader_stage_info;
shader_stages[shader_stage_count++] = tess_eval_shader_stage_info;
}
if (ps_active) {
shader_stages[shader_stage_count++] = frag_shader_stage_info;
}
vk::VertexInputAttributeDescription input_attr[ShaderVertexInputInfo::RES_MAX];
vk::VertexInputBindingDescription input_desc[ShaderVertexInputInfo::RES_MAX];
@@ -929,10 +978,13 @@ void CreatePipelineInternal(
pipeline_info.pStages = shader_stages;
pipeline_info.pVertexInputState = &vertex_input_info;
pipeline_info.pInputAssemblyState = &input_assembly;
pipeline_info.pTessellationState = nullptr;
pipeline_info.pViewportState = &viewport_state;
pipeline_info.pRasterizationState = &rasterizer;
pipeline_info.pMultisampleState = &multisampling;
vk::PipelineTessellationStateCreateInfo tessellation_state {};
tessellation_state.sType = vk::StructureType::ePipelineTessellationStateCreateInfo;
tessellation_state.patchControlPoints = 3;
pipeline_info.pTessellationState = (rect_list ? &tessellation_state : nullptr);
pipeline_info.pViewportState = &viewport_state;
pipeline_info.pRasterizationState = &rasterizer;
pipeline_info.pMultisampleState = &multisampling;
pipeline_info.pDepthStencilState = (static_params.with_depth ? &depth_stencil_info : nullptr);
pipeline_info.pColorBlendState = &color_blending;
pipeline_info.pDynamicState = &dynamic_state;
@@ -968,6 +1020,12 @@ void CreatePipelineInternal(
if (frag_shader_module != nullptr) {
graphics.device.destroyShaderModule(frag_shader_module, 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(vert_shader_module, nullptr);
}
+17 -61
View File
@@ -2,7 +2,6 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -650,11 +649,9 @@ static bool ConsumeMetadataColorOperation(const RenderCommandBuffer& buffer) {
}
struct DrawEmitInfo {
bool indexed = false;
bool draw_prim7_as_ngg = false;
uint32_t draw_vertex_count = 0;
int32_t vertex_offset = 0;
uint32_t first_vertex = 0;
bool indexed = false;
int32_t vertex_offset = 0;
uint32_t first_vertex = 0;
};
struct DrawIndexBufferSource {
@@ -767,7 +764,7 @@ static void SetDrawDebugPhase(RenderCommandBuffer& buffer, uint64_t submit_id,
draw.flags, draw.instance_count, draw.first_instance);
}
static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw, bool use_ngg_rectlist_draw,
static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw,
vk::PrimitiveTopology& topology) {
topology = vk::PrimitiveTopology::ePointList;
@@ -791,8 +788,7 @@ static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw, bool use
topology = vk::PrimitiveTopology::eTriangleStrip;
break;
case Prospero::PrimitiveType::kRectList:
topology = (auto_draw && use_ngg_rectlist_draw ? vk::PrimitiveTopology::eTriangleStrip
: vk::PrimitiveTopology::eTriangleList);
topology = vk::PrimitiveTopology::ePatchList;
break;
case Prospero::PrimitiveType::kRectListLegacy:
if (!auto_draw) {
@@ -991,20 +987,6 @@ static void LogDrawStateIfNeeded(const RenderCommandBuffer& buffer, const DrawCa
// LogDrawTextureState(draw.name, state.color_info[0], state.ps_input_info);
}
static bool IsHostExpandedRectListDrawSupported(const ShaderVertexInputInfo& vs_input_info,
const DrawCallInfo& draw,
const DrawEmitInfo& emit) {
if (!emit.draw_prim7_as_ngg) {
return true;
}
if (vs_input_info.buffers_num != 0) {
return false;
}
return draw.index_count == 3 || draw.index_count == emit.draw_vertex_count;
}
static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_buffer,
const ShaderVertexInputInfo& vs_input_info, const DrawCallInfo& draw,
const DrawEmitInfo& emit) {
@@ -1017,22 +999,12 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
case Prospero::PrimitiveType::kTriList:
case Prospero::PrimitiveType::kTriFan:
case Prospero::PrimitiveType::kTriStrip:
if (emit.indexed) {
vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset,
draw.first_instance);
} else {
vk_buffer.draw(draw.index_count, draw.instance_count, emit.first_vertex,
draw.first_instance);
}
break;
case Prospero::PrimitiveType::kRectList:
if (emit.indexed) {
vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset,
draw.first_instance);
} else {
EXIT_NOT_IMPLEMENTED(
!IsHostExpandedRectListDrawSupported(vs_input_info, draw, emit));
vk_buffer.draw(emit.draw_vertex_count, draw.instance_count, emit.first_vertex,
vk_buffer.draw(draw.index_count, draw.instance_count, emit.first_vertex,
draw.first_instance);
}
break;
@@ -1159,7 +1131,7 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, RenderCommandBuffer& buffer,
reinterpret_cast<uint64_t>(index_addr));
Common::LockGuard lock(m_context.GetMutex());
if (index_count == 0) {
if (index_count == 0 || instance_count == 0) {
return;
}
@@ -1201,7 +1173,7 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, RenderCommandBuffer& buffer,
hw_check(buffer);
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, false, false, topology)) {
if (!GetDrawTopology(ucfg, false, topology)) {
return;
}
@@ -1229,10 +1201,6 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, RenderCommandBuffer& buffer,
EXIT_NOT_IMPLEMENTED(flags != 0);
EXIT_NOT_IMPLEMENTED(type != 1);
if (instance_count == 0) {
instance_count = 1;
}
const DrawCallInfo draw {"DrawIndex", CommandBufferDebugOp::DrawIndex,
index_count, flags,
instance_count, first_instance};
@@ -1292,7 +1260,7 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, RenderCommandBuffer& buffer, u
index_count, flags, first_vertex, instance_count, first_instance);
Common::LockGuard lock(m_context.GetMutex());
if (index_count == 0) {
if (index_count == 0 || instance_count == 0) {
return;
}
@@ -1330,10 +1298,6 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, RenderCommandBuffer& buffer, u
hw_check(buffer);
EXIT_NOT_IMPLEMENTED(flags != 0);
if (instance_count == 0) {
instance_count = 1;
}
const DrawCallInfo draw {"DrawIndexAuto", CommandBufferDebugOp::DrawIndexAuto,
index_count, flags,
instance_count, first_instance};
@@ -1345,20 +1309,15 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, RenderCommandBuffer& buffer, u
return;
}
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
const bool use_ngg_rectlist_draw = Config::NggRectlistDrawEnabled();
if (!GetDrawTopology(ucfg, true, use_ngg_rectlist_draw, topology)) {
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, true, topology)) {
ResetBindings();
return;
}
const bool draw_prim7_as_ngg =
(use_ngg_rectlist_draw &&
ucfg.GetPrimType() == Prospero::GpuEnumValue(Prospero::PrimitiveType::kRectList));
RefreshShaders(buffer, draw, false, state);
if (draw_prim7_as_ngg && state.vs_input_info.buffers_num == 0 &&
const bool rect_list = topology == vk::PrimitiveTopology::ePatchList;
if (rect_list && state.vs_input_info.buffers_num == 0 &&
state.vs_input_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: "
@@ -1375,13 +1334,10 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, RenderCommandBuffer& buffer, u
Prospero::GpuEnumValue(Prospero::PrimitiveType::kRectListLegacy),
0, nullptr);
const uint32_t draw_vertex_count = (draw_prim7_as_ngg ? 4u : index_count);
const auto vertex_offset = ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) +
static_cast<int32_t>(first_vertex);
DrawEmitInfo emit {};
emit.draw_prim7_as_ngg = draw_prim7_as_ngg;
emit.draw_vertex_count = draw_vertex_count;
emit.first_vertex = static_cast<uint32_t>(vertex_offset);
const auto vertex_offset = ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) +
static_cast<int32_t>(first_vertex);
DrawEmitInfo emit {};
emit.first_vertex = static_cast<uint32_t>(vertex_offset);
DrawIndexBufferSource index_source {};
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false, false,
+1
View File
@@ -37,6 +37,7 @@ constexpr FormatMapping kFormatMappings[] = {
{Prospero::BufferFormat::k16_16Float, vk::Format::eR16G16Sfloat},
{Prospero::BufferFormat::k11_11_10Float, vk::Format::eB10G11R11UfloatPack32},
{Prospero::BufferFormat::k10_10_10_2UNorm, vk::Format::eA2B10G10R10UnormPack32},
{Prospero::BufferFormat::k10_10_10_2UInt, vk::Format::eA2B10G10R10UintPack32},
{Prospero::BufferFormat::k8_8_8_8UNorm, vk::Format::eR8G8B8A8Unorm},
{Prospero::BufferFormat::k8_8_8_8SNorm, vk::Format::eR8G8B8A8Snorm},
{Prospero::BufferFormat::k8_8_8_8UInt, vk::Format::eR8G8B8A8Uint},
+248 -19
View File
@@ -871,19 +871,19 @@ std::vector<uint32_t> DominatedBlocks(const Graph& graph, uint32_t header,
return blocks;
}
uint32_t AppendSyntheticMergeBlock(Graph& graph, uint32_t old_merge) {
const auto* merge = graph.FindBlock(old_merge);
uint32_t AppendSyntheticBranchBlock(Graph& graph, uint32_t target) {
const auto* target_block = graph.FindBlock(target);
BasicBlock block;
block.id = static_cast<uint32_t>(graph.blocks.size());
block.start_pc = merge != nullptr ? merge->start_pc : 0u;
block.start_pc = target_block != nullptr ? target_block->start_pc : 0u;
block.end_pc = block.start_pc;
block.inst_begin = merge != nullptr ? merge->inst_begin : 0u;
block.inst_begin = target_block != nullptr ? target_block->inst_begin : 0u;
block.inst_end = block.inst_begin;
block.successors = {old_merge};
block.successors = {target};
block.terminator.kind = TerminatorKind::Branch;
block.terminator.condition = BranchCondition::Always;
block.terminator.true_block = old_merge;
block.terminator.true_block = target;
graph.blocks.push_back(std::move(block));
return graph.blocks.back().id;
}
@@ -897,15 +897,50 @@ bool IsSyntheticMergeForwarder(const Graph& graph, uint32_t block_id, uint32_t m
block->terminator.true_block == merge;
}
bool IsInsideLoopConstruct(const Graph& graph, const NaturalLoop& loop, uint32_t block_id) {
return block_id != UINT32_MAX && block_id != loop.merge && block_id != loop.continue_block &&
graph.Dominates(loop.header, block_id) &&
(loop.merge == UINT32_MAX || !graph.Dominates(loop.merge, block_id));
const NaturalLoop* FindInnermostContainingLoop(const Graph& graph, uint32_t block_id) {
const NaturalLoop* innermost = nullptr;
for (const auto& loop: graph.natural_loops) {
if (Contains(loop.body_blocks, block_id) &&
(innermost == nullptr || loop.body_blocks.size() < innermost->body_blocks.size())) {
innermost = &loop;
}
}
return innermost;
}
bool SelectionMergeLeavesContainingLoop(const Graph& graph, uint32_t header, uint32_t merge) {
bool IsInsideLoopConstruct(const Graph& graph, const NaturalLoop& loop, uint32_t block_id) {
return block_id != UINT32_MAX && block_id != loop.merge && block_id != loop.continue_block &&
graph.Dominates(loop.header, block_id) && !graph.Dominates(loop.merge, block_id);
}
bool IsInnermostLoopControlConditional(const Graph& graph, const BasicBlock& block) {
if (block.terminator.kind != TerminatorKind::ConditionalBranch) {
return false;
}
const auto* loop = FindInnermostContainingLoop(graph, block.id);
if (loop == nullptr || loop->merge == UINT32_MAX || loop->continue_block == UINT32_MAX) {
return false;
}
const auto true_target = block.terminator.true_block;
const auto false_target = block.terminator.false_block;
if (block.id == loop->continue_block) {
const auto is_repeat_target = [&](uint32_t target) {
return target == loop->header || target == loop->merge;
};
return is_repeat_target(true_target) && is_repeat_target(false_target);
}
const auto is_control_target = [&](uint32_t target) {
return target == loop->merge || target == loop->continue_block;
};
return (is_control_target(true_target) &&
(is_control_target(false_target) ||
IsInsideLoopConstruct(graph, *loop, false_target))) ||
(is_control_target(false_target) && IsInsideLoopConstruct(graph, *loop, true_target));
}
bool MergeLeavesContainingLoop(const Graph& graph, uint32_t header, uint32_t merge) {
for (const auto& loop: graph.natural_loops) {
if (IsInsideLoopConstruct(graph, loop, header) &&
if (loop.header != header && IsInsideLoopConstruct(graph, loop, header) &&
!IsInsideLoopConstruct(graph, loop, merge)) {
return true;
}
@@ -913,6 +948,80 @@ bool SelectionMergeLeavesContainingLoop(const Graph& graph, uint32_t header, uin
return false;
}
bool CanonicalizeNaturalLoops(Graph& graph, std::string* error) {
const auto rewrite_budget = graph.blocks.size() * 2u + 16u;
for (size_t rewrite = 0; rewrite < rewrite_budget; rewrite++) {
bool changed = false;
for (const auto& loop: graph.natural_loops) {
std::vector<uint32_t> latches;
for (const auto& edge: graph.back_edges) {
if (edge.to == loop.header) {
AddUnique(latches, edge.from);
}
}
if (latches.size() <= 1u) {
continue;
}
const auto continue_block = AppendSyntheticBranchBlock(graph, loop.header);
for (auto latch: latches) {
auto* block = graph.FindBlock(latch);
if (block != nullptr) {
ReplaceValue(block->successors, loop.header, continue_block);
ReplaceTerminatorTarget(block->terminator, loop.header, continue_block);
}
}
RebuildPredecessors(graph);
RecomputeAnalyses(graph);
changed = true;
break;
}
if (changed) {
continue;
}
for (const auto& loop: graph.natural_loops) {
const auto* header = graph.FindBlock(loop.header);
const auto is_loop_control_target = [&](uint32_t target) {
return target == loop.merge || target == loop.continue_block;
};
if (header == nullptr || header->terminator.kind != TerminatorKind::ConditionalBranch ||
is_loop_control_target(header->terminator.true_block) ||
is_loop_control_target(header->terminator.false_block) ||
!Contains(loop.body_blocks, header->terminator.true_block) ||
!Contains(loop.body_blocks, header->terminator.false_block)) {
continue;
}
const auto old_header = loop.header;
const auto predecessors = header->predecessors;
const auto new_header = AppendSyntheticBranchBlock(graph, old_header);
for (auto pred: predecessors) {
auto* block = graph.FindBlock(pred);
if (block != nullptr) {
ReplaceValue(block->successors, old_header, new_header);
ReplaceTerminatorTarget(block->terminator, old_header, new_header);
}
}
if (graph.entry_block == old_header) {
graph.entry_block = new_header;
}
MoveBlockBefore(graph, new_header, old_header);
RebuildPredecessors(graph);
RecomputeAnalyses(graph);
changed = true;
break;
}
if (!changed) {
return true;
}
}
SetFailure(graph, FailureKind::StructuredControlFlow, graph.entry_block,
"CFG loop canonicalization exceeded rewrite budget", error);
return false;
}
bool SplitSharedMergeBlock(Graph& graph, uint32_t merge,
const std::vector<uint32_t>& construct_blocks,
bool force_split = false) {
@@ -948,7 +1057,7 @@ bool SplitSharedMergeBlock(Graph& graph, uint32_t merge,
return false;
}
const auto synthetic_merge = AppendSyntheticMergeBlock(graph, merge);
const auto synthetic_merge = AppendSyntheticBranchBlock(graph, merge);
auto* synthetic_block = graph.FindBlock(synthetic_merge);
if (synthetic_block != nullptr) {
synthetic_block->predecessors = predecessors_to_split;
@@ -980,14 +1089,111 @@ bool SplitSharedMergeBlock(Graph& graph, uint32_t merge,
bool SplitOneLoopMerge(Graph& graph) {
const auto& loops = graph.natural_loops;
for (const auto& loop: loops) {
if (SplitSharedMergeBlock(graph, loop.merge, loop.body_blocks)) {
const auto construct_blocks = DominatedBlocks(graph, loop.header, loop.merge);
const auto force_split = MergeLeavesContainingLoop(graph, loop.header, loop.merge);
if (SplitSharedMergeBlock(graph, loop.merge, construct_blocks, force_split)) {
return true;
}
}
return false;
}
bool SplitOneSelectionMerge(Graph& graph) {
std::vector<uint32_t> SelectionRegion(const Graph& graph, const BasicBlock& header,
uint32_t merge) {
std::vector<uint32_t> region;
std::vector<uint32_t> pending = {header.terminator.true_block,
header.terminator.false_block};
while (!pending.empty()) {
const auto block_id = pending.back();
pending.pop_back();
if (block_id == merge || Contains(region, block_id)) {
continue;
}
const auto* block = graph.FindBlock(block_id);
if (block == nullptr) {
continue;
}
AddUnique(region, block_id);
pending.insert(pending.end(), block->successors.begin(), block->successors.end());
}
SortUnique(region);
return region;
}
bool DuplicateSelectionRegion(Graph& graph, uint32_t header_id, uint32_t merge,
const std::vector<uint32_t>& region, uint32_t block_budget) {
std::vector<uint32_t> cloned_blocks;
for (auto block_id: region) {
if (!graph.Dominates(header_id, block_id)) {
cloned_blocks.push_back(block_id);
}
}
if (cloned_blocks.empty() || graph.FindBlock(header_id) == nullptr || header_id >= merge ||
graph.blocks.size() + cloned_blocks.size() + 1u > block_budget) {
return false;
}
const auto first_clone = static_cast<uint32_t>(graph.blocks.size());
std::map<uint32_t, uint32_t> clones;
for (uint32_t i = 0; i < cloned_blocks.size(); i++) {
clones.emplace(cloned_blocks[i], first_clone + i);
}
for (auto block_id: cloned_blocks) {
BasicBlock clone = *graph.FindBlock(block_id);
clone.id = clones.at(block_id);
clone.predecessors.clear();
clone.dominators.clear();
clone.post_dominators.clear();
graph.blocks.push_back(std::move(clone));
}
const auto remap_block = [&](BasicBlock& block) {
const auto remap_target = [&](uint32_t& target) {
if (const auto it = clones.find(target); it != clones.end()) {
target = it->second;
}
};
for (auto& successor: block.successors) {
remap_target(successor);
}
remap_target(block.terminator.true_block);
remap_target(block.terminator.false_block);
remap_target(block.terminator.merge_block);
remap_target(block.terminator.continue_block);
for (auto& target: block.terminator.indirect_targets) {
remap_target(target);
}
};
for (auto block_id: region) {
const auto owned_id = clones.contains(block_id) ? clones.at(block_id) : block_id;
remap_block(*graph.FindBlock(owned_id));
}
const auto private_merge = AppendSyntheticBranchBlock(graph, merge);
auto& header = *graph.FindBlock(header_id);
remap_block(header);
for (auto block_id: region) {
const auto owned_id = clones.contains(block_id) ? clones.at(block_id) : block_id;
auto* block = graph.FindBlock(owned_id);
if (block != nullptr) {
ReplaceValue(block->successors, merge, private_merge);
ReplaceTerminatorTarget(block->terminator, merge, private_merge);
}
}
ReplaceValue(header.successors, merge, private_merge);
ReplaceTerminatorTarget(header.terminator, merge, private_merge);
for (uint32_t i = 0; i <= cloned_blocks.size(); i++) {
MoveBlockBefore(graph, first_clone + i, merge + i);
}
RebuildPredecessors(graph);
RecomputeAnalyses(graph);
return true;
}
bool SplitOneSelectionMerge(Graph& graph, uint32_t block_budget) {
std::vector<uint32_t> loop_headers;
loop_headers.reserve(graph.natural_loops.size());
for (const auto& loop: graph.natural_loops) {
@@ -1001,11 +1207,26 @@ bool SplitOneSelectionMerge(Graph& graph) {
Contains(loop_headers, block_id)) {
continue;
}
if (IsInnermostLoopControlConditional(graph, *block)) {
continue;
}
const auto merge = graph.FindNearestCommonPostDominator(block->terminator.true_block,
block->terminator.false_block);
if (merge == UINT32_MAX || graph.FindBlock(merge) == nullptr) {
continue;
}
const auto region = SelectionRegion(graph, *block, merge);
if (std::any_of(region.begin(), region.end(),
[&](uint32_t member) { return !graph.Dominates(block_id, member); })) {
if (graph.natural_loops.empty() &&
DuplicateSelectionRegion(graph, block_id, merge, region, block_budget)) {
return true;
}
continue;
}
const auto construct_blocks = DominatedBlocks(graph, block_id, merge);
const auto force_split = SelectionMergeLeavesContainingLoop(graph, block_id, merge);
const auto force_split = MergeLeavesContainingLoop(graph, block_id, merge);
if (SplitSharedMergeBlock(graph, merge, construct_blocks, force_split)) {
return true;
}
@@ -1015,10 +1236,12 @@ bool SplitOneSelectionMerge(Graph& graph) {
bool SplitSharedMergeBlocks(Graph& graph, std::string* error) {
const auto original_block_count = static_cast<uint32_t>(graph.blocks.size());
const auto split_budget =
std::max<uint32_t>(16u, std::min<uint32_t>(128u, original_block_count));
const auto split_budget = std::max<uint32_t>(
16u, std::min<uint32_t>(128u, original_block_count * 4u));
const auto block_budget = std::max<uint32_t>(
32u, std::min<uint32_t>(512u, original_block_count * 8u));
for (uint32_t splits = 0; splits < split_budget; splits++) {
if (!SplitOneLoopMerge(graph) && !SplitOneSelectionMerge(graph)) {
if (!SplitOneLoopMerge(graph) && !SplitOneSelectionMerge(graph, block_budget)) {
return true;
}
RebuildPredecessors(graph);
@@ -1353,6 +1576,9 @@ bool Structurize(Graph& graph, std::string* error) {
return false;
}
if (!CanonicalizeNaturalLoops(graph, error)) {
return false;
}
if (!SplitSharedMergeBlocks(graph, error)) {
return false;
}
@@ -1395,6 +1621,9 @@ bool Structurize(Graph& graph, std::string* error) {
block.terminator.loop_header) {
continue;
}
if (IsInnermostLoopControlConditional(graph, block)) {
continue;
}
const auto merge = graph.FindNearestCommonPostDominator(block.terminator.true_block,
block.terminator.false_block);
@@ -19,7 +19,7 @@ static void AppendInstructionWords(std::vector<uint32_t>& section, const uint32_
section.insert(section.end(), words + 1, words + words_num);
}
Builder::Builder() {
Builder::Builder(uint32_t version): m_version(version) {
m_debug.reserve(InitialSpirvSectionReserve);
m_annotations.reserve(InitialSpirvSectionReserve);
m_types.reserve(InitialSpirvSectionReserve);
@@ -138,7 +138,7 @@ std::vector<uint32_t> Builder::Build() const {
m_debug.size() + m_annotations.size() + m_types.size() + m_functions.size());
module.push_back(0x07230203u);
module.push_back(0x00010300u);
module.push_back(m_version);
module.push_back(0u);
module.push_back(m_next_id);
module.push_back(0u);
@@ -10,7 +10,7 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv {
class Builder {
public:
Builder();
explicit Builder(uint32_t version = 0x00010300u);
~Builder() = default;
KYTY_CLASS_DEFAULT_COPY(Builder);
@@ -39,6 +39,7 @@ private:
static void AppendString(std::vector<uint32_t>& words, const char* text);
uint32_t m_next_id = 1;
uint32_t m_version = 0;
std::vector<uint32_t> m_capabilities;
std::vector<uint32_t> m_extensions;
std::vector<uint32_t> m_ext_inst_imports;
@@ -7,51 +7,30 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
uint32_t PixelParameterMappedLocation(const EmitterState& state, uint32_t attr) {
const auto* ps = state.pixel_input_info;
if (state.stage != ShaderType::Pixel || ps == nullptr || attr >= ps->input_num) {
if (state.stage != ShaderType::Pixel || ps == nullptr) {
return attr;
}
// VINTRP ATTR selects the PS input slot. SPI_PS_INPUT_CNTL maps that slot to a
// VS parameter export, which is the SPIR-V location we must link against.
return ps->interpolator_settings[attr] & PsInputOffsetMask;
return ShaderPixelParameterMappedLocation(*ps, attr);
}
uint32_t PixelParameterLocation(const EmitterState& state, uint32_t attr) {
bool used_locations[32] = {};
std::array<uint32_t, 32> active_inputs {};
uint32_t active_count = 0;
for (const auto& input: state.inputs) {
if (input.kind != IR::StageInputKind::Parameter) {
continue;
}
auto location = PixelParameterMappedLocation(state, input.location);
if (location < std::size(used_locations) && used_locations[location]) {
auto fallback_location = input.location;
while (fallback_location < std::size(used_locations) &&
used_locations[fallback_location]) {
fallback_location++;
}
EXIT_NOT_IMPLEMENTED(fallback_location >= std::size(used_locations));
location = fallback_location;
}
if (input.location == attr) {
return location;
}
if (location < std::size(used_locations)) {
used_locations[location] = true;
if (input.kind == IR::StageInputKind::Parameter) {
active_inputs[active_count++] = input.location;
}
}
return PixelParameterMappedLocation(state, attr);
return state.stage == ShaderType::Pixel && state.pixel_input_info != nullptr
? ShaderPixelParameterLocation(*state.pixel_input_info,
{active_inputs.data(), active_count}, attr)
: attr;
}
bool PixelParameterIsFlat(const EmitterState& state, uint32_t attr) {
const auto* ps = state.pixel_input_info;
if (state.stage != ShaderType::Pixel || ps == nullptr || attr >= ps->input_num) {
return false;
}
return (ps->interpolator_settings[attr] & PsInputFlatShade) != 0;
return state.stage == ShaderType::Pixel && ps != nullptr &&
ShaderPixelParameterIsFlat(*ps, attr);
}
void SetError(std::string* error, const char* message) {
@@ -425,9 +425,6 @@ struct EmitterState {
std::map<uint32_t, uint32_t> float_constants;
};
constexpr uint32_t PsInputOffsetMask = 0x0000001fu;
constexpr uint32_t PsInputFlatShade = 0x00000400u;
enum class VertexInputScalarKind { Float, Sint, Uint };
constexpr uint32_t NoImageComponent = 0xffffffffu;
@@ -47,8 +47,9 @@ bool NullImageDescriptor(const DescriptorValue& descriptor) {
}
bool ValidImageDescriptor(const DescriptorValue& descriptor) {
const auto type = static_cast<Prospero::ImageType>((descriptor.dwords[3] >> 28u) & 0xfu);
if (type < Prospero::ImageType::kColor1D) {
const auto type = static_cast<Prospero::ImageType>((descriptor.dwords[3] >> 28u) & 0xfu);
const auto format = static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
if (type < Prospero::ImageType::kColor1D || format == Prospero::BufferFormat::kInvalid) {
return false;
}
if (type == Prospero::ImageType::kColor2DMsaa ||
@@ -199,8 +200,12 @@ bool ValidateResourceSpecialization(const Program& program, const ResourceSnapsh
if (dimension == Decoder::ImageDimension::Unknown || dimension != image.dimension ||
DescriptorIsCube(descriptor) != image.cube) {
if (error != nullptr) {
*error =
fmt::format("image descriptor {} no longer matches specialized dimension", i);
*error = fmt::format(
"image descriptor {} no longer matches specialized dimension: "
"{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x}",
i, descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2],
descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5],
descriptor.dwords[6], descriptor.dwords[7]);
}
return false;
}
@@ -215,8 +220,12 @@ bool ValidateResourceSpecialization(const Program& program, const ResourceSnapsh
}
return false;
}
const auto uint_descriptor =
Prospero::IsUintTextureFormat((descriptor.dwords[1] >> 20u) & 0x1ffu);
const auto format = (descriptor.dwords[1] >> 20u) & 0x1ffu;
const bool raw_sint_storage =
storage && format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32SInt) &&
!image.read && !image.atomic;
const bool uint_descriptor =
Prospero::IsUintTextureFormat(format) || raw_sint_storage;
const auto uint_program = image.kind == ResourceKind::ImageUint ||
image.kind == ResourceKind::StorageImageUint;
if (uint_descriptor != uint_program && !(image.atomic && uint_program)) {
@@ -403,7 +412,14 @@ bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std
image.kind == ResourceKind::StorageImageUint) {
image.storage_swizzle = DescriptorImageSwizzle(descriptor);
}
if (Prospero::IsUintTextureFormat((descriptor.dwords[1] >> 20u) & 0x1ffu)) {
const auto format = (descriptor.dwords[1] >> 20u) & 0x1ffu;
const bool storage = image.kind == ResourceKind::StorageImage ||
image.kind == ResourceKind::StorageImageUint;
const bool raw_sint_storage =
storage && format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32SInt) &&
!image.read && !image.atomic;
const bool uint_image = Prospero::IsUintTextureFormat(format) || raw_sint_storage;
if (uint_image) {
switch (image.kind) {
case ResourceKind::Image: image.kind = ResourceKind::ImageUint; break;
case ResourceKind::StorageImage: image.kind = ResourceKind::StorageImageUint; break;
@@ -45,6 +45,8 @@ namespace {
constexpr uint32_t ScalarRegisters = 128;
constexpr uint32_t VectorRegisters = 256;
// Clamp X/Y/Z are consecutive three-bit fields; the high bit of each selects a border mode.
constexpr uint32_t SamplerBorderClampMask = (1u << 2u) | (1u << 5u) | (1u << 8u);
struct ScalarState {
std::array<uint32_t, ScalarRegisters> regs = {};
@@ -647,6 +649,24 @@ private:
return AddDescriptor(descriptor);
}
uint32_t AddSamplerDescriptor(const ScalarState& state, uint32_t base) {
if (base >= ScalarRegisters || 4u > ScalarRegisters - base) {
return ScalarProvenance::Unknown;
}
DescriptorValue descriptor;
descriptor.dword_count = 4;
for (uint32_t i = 0; i < 4; i++) {
descriptor.dwords[i] = state.regs[base + i];
}
if (auto d0 = descriptor.dwords[0];
d0 < m_graph.values.size() && m_graph.values[d0].op == ScalarValueOp::Constant &&
(m_graph.values[d0].imm & SamplerBorderClampMask) == 0) {
// Without a border clamp, the border color and table index in dword 3 are unused.
descriptor.dwords[3] = Constant(0);
}
return AddDescriptor(descriptor);
}
uint32_t AddFlatAddressDescriptor(const Instruction& inst, const ScalarState& state) {
const uint32_t first = FlatStore(inst.op) ? 1u : 0u;
if (inst.src_count < first + 2u) {
@@ -704,7 +724,7 @@ private:
inst.memory.resource_source = AddDescriptor(state, inst.memory.resource * 4u, 8);
if (inst.op == Opcode::ImageSample || inst.op == Opcode::ImageGather4 ||
inst.op == Opcode::ImageGetLod) {
inst.memory.sampler_source = AddDescriptor(state, inst.memory.sampler * 4u, 4);
inst.memory.sampler_source = AddSamplerDescriptor(state, inst.memory.sampler * 4u);
}
}
}
+397
View File
@@ -0,0 +1,397 @@
#include "graphics/shader/rectListShader.h"
#include "common/assert.h"
#include "graphics/shader/recompiler/emitter/SpirvBuilder.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/shader.h"
#include "spirv/unified1/spirv.hpp11"
#include <array>
#include <bit>
#include <cstdint>
#include <utility>
namespace Libs::Graphics {
namespace {
using ShaderRecompiler::Spirv::Builder;
constexpr uint32_t SpirvVersion15 = 0x00010500u;
template <typename T>
constexpr uint32_t Word(T value) {
return static_cast<uint32_t>(value);
}
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;
for (const auto input: active_inputs) {
const auto input_location = ShaderPixelParameterMappedLocation(*pixel_info, input);
if ((vertex_info.param_export_mask & (1u << input_location)) != 0) {
parameters.push_back({input_location,
ShaderPixelParameterLocation(*pixel_info, active_inputs, input),
ShaderPixelParameterIsFlat(*pixel_info, input)});
}
}
return parameters;
}
class RectListEmitter {
public:
RectListEmitter(const std::vector<Parameter>& parameters_, spv::ExecutionModel model)
: parameters(parameters_) {
builder.AddMemoryModel(
{Word(spv::AddressingModel::Logical), Word(spv::MemoryModel::GLSL450)});
void_type = Type(spv::Op::OpTypeVoid);
uint_type = Type(spv::Op::OpTypeInt, 32u, 0u);
int_type = Type(spv::Op::OpTypeInt, 32u, 1u);
float_type = Type(spv::Op::OpTypeFloat, 32u);
vec4_float_type = Type(spv::Op::OpTypeVector, float_type, 4u);
function_type = Type(spv::Op::OpTypeFunction, void_type);
per_vertex_type = Type(spv::Op::OpTypeStruct, vec4_float_type);
builder.AddAnnotation({Word(spv::Op::OpMemberDecorate), per_vertex_type, 0u,
Word(spv::Decoration::BuiltIn), Word(spv::BuiltIn::Position)});
builder.AddAnnotation(
{Word(spv::Op::OpDecorate), per_vertex_type, Word(spv::Decoration::Block)});
ptr_input_vec4_float = Pointer(spv::StorageClass::Input, vec4_float_type);
ptr_output_vec4_float = Pointer(spv::StorageClass::Output, vec4_float_type);
if (model == spv::ExecutionModel::TessellationControl) {
bool_type = Type(spv::Op::OpTypeBool);
vec2_bool_type = Type(spv::Op::OpTypeVector, bool_type, 2u);
vec2_float_type = Type(spv::Op::OpTypeVector, float_type, 2u);
ptr_output_float = Pointer(spv::StorageClass::Output, float_type);
} else {
vec3_float_type = Type(spv::Op::OpTypeVector, float_type, 3u);
ptr_input_float = Pointer(spv::StorageClass::Input, float_type);
}
}
std::vector<uint32_t> EmitControl() {
DefineEntry(spv::ExecutionModel::TessellationControl);
const auto float_one = Constant(float_type, std::bit_cast<uint32_t>(1.0f));
for (uint32_t i = 0; i < 4; i++) {
Store(Access(ptr_output_float, tess_outer, Int(i)), float_one);
}
for (uint32_t i = 0; i < 2; i++) {
Store(Access(ptr_output_float, tess_inner, Int(i)), float_one);
}
std::array<uint32_t, 3> positions {};
for (uint32_t i = 0; i < positions.size(); i++) {
positions[i] =
Load(vec4_float_type, Access(ptr_input_vec4_float, gl_in, Int(i), Int(0)));
}
std::array<uint32_t, 3> coordinate_equal {};
for (uint32_t i = 0; i < coordinate_equal.size(); i++) {
const auto left = Result(spv::Op::OpVectorShuffle, vec2_float_type, positions[i],
positions[i], 0u, 1u);
const auto right = Result(spv::Op::OpVectorShuffle, vec2_float_type,
positions[(i + 1u) % 3u], positions[(i + 1u) % 3u], 0u, 1u);
coordinate_equal[i] = Result(spv::Op::OpFOrdEqual, vec2_bool_type, left, right);
}
std::array<uint32_t, 3> barycentric {};
std::array<uint32_t, 3> edge_vertex {};
const auto float_minus_one = Constant(float_type, std::bit_cast<uint32_t>(-1.0f));
for (uint32_t i = 0; i < edge_vertex.size(); i++) {
const auto previous = (i + 2u) % 3u;
const auto xy = Result(
spv::Op::OpLogicalAnd, bool_type,
Result(spv::Op::OpCompositeExtract, bool_type, coordinate_equal[i], 0u),
Result(spv::Op::OpCompositeExtract, bool_type, coordinate_equal[previous], 1u));
const auto yx = Result(
spv::Op::OpLogicalAnd, bool_type,
Result(spv::Op::OpCompositeExtract, bool_type, coordinate_equal[i], 1u),
Result(spv::Op::OpCompositeExtract, bool_type, coordinate_equal[previous], 0u));
edge_vertex[i] = Result(spv::Op::OpLogicalOr, bool_type, xy, yx);
barycentric[i] =
Result(spv::Op::OpSelect, float_type, edge_vertex[i], float_minus_one, float_one);
}
auto vertex_index = Result(spv::Op::OpSelect, int_type, edge_vertex[2], Int(2), Int(0));
vertex_index = Result(spv::Op::OpSelect, int_type, edge_vertex[1], Int(1), vertex_index);
const auto invocation = Load(int_type, invocation_id);
const auto is_fourth = Result(spv::Op::OpIEqual, bool_type, invocation, Int(3));
const auto index =
Result(spv::Op::OpSMod, int_type,
Result(spv::Op::OpIAdd, int_type, vertex_index, invocation), Int(3));
const auto position3 = Interpolate(positions[0], positions[1], positions[2], barycentric);
const auto position =
Result(spv::Op::OpSelect, vec4_float_type, is_fourth, position3,
Load(vec4_float_type, Access(ptr_input_vec4_float, gl_in, index, Int(0))));
Store(Access(ptr_output_vec4_float, gl_out, invocation, Int(0)), position);
for (uint32_t i = 0; i < parameters.size(); i++) {
const auto input0 =
Load(vec4_float_type, Access(ptr_input_vec4_float, inputs[i], Int(0)));
if (parameters[i].flat) {
Store(Access(ptr_output_vec4_float, outputs[i], invocation), input0);
continue;
}
const auto input1 =
Load(vec4_float_type, Access(ptr_input_vec4_float, inputs[i], Int(1)));
const auto input2 =
Load(vec4_float_type, Access(ptr_input_vec4_float, inputs[i], Int(2)));
const auto input3 = Interpolate(input0, input1, input2, barycentric);
const auto value =
Result(spv::Op::OpSelect, vec4_float_type, is_fourth, input3,
Load(vec4_float_type, Access(ptr_input_vec4_float, inputs[i], index)));
Store(Access(ptr_output_vec4_float, outputs[i], invocation), value);
}
Emit(spv::Op::OpReturn);
Emit(spv::Op::OpFunctionEnd);
return builder.Build();
}
std::vector<uint32_t> EmitEvaluation() {
DefineEntry(spv::ExecutionModel::TessellationEvaluation);
const auto x = Load(float_type, Access(ptr_input_float, tess_coord, Int(0)));
const auto y = Load(float_type, Access(ptr_input_float, tess_coord, Int(1)));
const auto index = Result(
spv::Op::OpIAdd, int_type,
Result(spv::Op::OpIMul, int_type, Result(spv::Op::OpConvertFToS, int_type, y), Int(2)),
Result(spv::Op::OpConvertFToS, int_type, x));
const auto position =
Load(vec4_float_type, Access(ptr_input_vec4_float, gl_in, index, Int(0)));
Store(Access(ptr_output_vec4_float, gl_out, Int(0)), position);
for (uint32_t i = 0; i < parameters.size(); i++) {
Store(outputs[i],
Load(vec4_float_type, Access(ptr_input_vec4_float, inputs[i], index)));
}
Emit(spv::Op::OpReturn);
Emit(spv::Op::OpFunctionEnd);
return builder.Build();
}
private:
template <typename... Args>
uint32_t Type(spv::Op opcode, Args... operands) {
const auto id = builder.AllocateId();
builder.AddType({Word(opcode), id, Word(operands)...});
return id;
}
uint32_t Constant(uint32_t type, uint32_t value) {
const auto id = builder.AllocateId();
builder.AddType({Word(spv::Op::OpConstant), type, id, value});
return id;
}
uint32_t Pointer(spv::StorageClass storage, uint32_t type) {
return Type(spv::Op::OpTypePointer, storage, type);
}
uint32_t Array(uint32_t type, uint32_t size) {
return Type(spv::Op::OpTypeArray, type, Uint(size));
}
template <typename... Args>
uint32_t Result(spv::Op opcode, uint32_t type, Args... operands) {
const auto id = builder.AllocateId();
builder.AddFunction({Word(opcode), type, id, Word(operands)...});
return id;
}
template <typename... Args>
uint32_t ResultWithoutType(spv::Op opcode, Args... operands) {
const auto id = builder.AllocateId();
builder.AddFunction({Word(opcode), id, Word(operands)...});
return id;
}
template <typename... Args>
void Emit(spv::Op opcode, Args... operands) {
builder.AddFunction({Word(opcode), Word(operands)...});
}
template <typename... Args>
uint32_t Access(uint32_t pointer_type, uint32_t base, Args... indices) {
return Result(spv::Op::OpAccessChain, pointer_type, base, Word(indices)...);
}
uint32_t Load(uint32_t type, uint32_t pointer) {
return Result(spv::Op::OpLoad, type, pointer);
}
void Store(uint32_t pointer, uint32_t value) { Emit(spv::Op::OpStore, pointer, value); }
uint32_t Int(uint32_t value) {
auto& id = int_constants[value];
if (id == 0) {
id = Constant(int_type, value);
}
return id;
}
uint32_t Uint(uint32_t value) {
auto& id = uint_constants[value];
if (id == 0) {
id = Constant(uint_type, value);
}
return id;
}
uint32_t AddInterface(spv::StorageClass storage, uint32_t type) {
const auto variable = builder.AllocateId();
builder.AddType(
{Word(spv::Op::OpVariable), Pointer(storage, type), variable, Word(storage)});
interfaces.push_back(variable);
return variable;
}
void Decorate(uint32_t target, spv::Decoration decoration, uint32_t value) {
builder.AddAnnotation({Word(spv::Op::OpDecorate), target, Word(decoration), value});
}
void DefineEntry(spv::ExecutionModel model) {
builder.AddCapability({Word(spv::Capability::Shader)});
builder.AddCapability({Word(spv::Capability::Tessellation)});
main = Result(spv::Op::OpFunction, void_type, spv::FunctionControlMask::MaskNone,
function_type);
if (model == spv::ExecutionModel::TessellationControl) {
builder.AddExecutionMode({main, Word(spv::ExecutionMode::OutputVertices), 4u});
} else {
builder.AddExecutionMode({main, Word(spv::ExecutionMode::Quads)});
builder.AddExecutionMode({main, Word(spv::ExecutionMode::SpacingEqual)});
builder.AddExecutionMode({main, Word(spv::ExecutionMode::VertexOrderCw)});
}
DefineInputs(model);
DefineOutputs(model);
builder.AddEntryPoint(Word(model), main, "main", interfaces);
ResultWithoutType(spv::Op::OpLabel);
}
void DefineInputs(spv::ExecutionModel model) {
const auto tess_control = model == spv::ExecutionModel::TessellationControl;
if (tess_control) {
invocation_id = AddInterface(spv::StorageClass::Input, int_type);
Decorate(invocation_id, spv::Decoration::BuiltIn, Word(spv::BuiltIn::InvocationId));
} else {
tess_coord = AddInterface(spv::StorageClass::Input, vec3_float_type);
Decorate(tess_coord, spv::Decoration::BuiltIn, Word(spv::BuiltIn::TessCoord));
}
gl_in =
AddInterface(spv::StorageClass::Input, Array(per_vertex_type, tess_control ? 3u : 4u));
inputs.resize(parameters.size());
std::array<uint32_t, ShaderVertexInputInfo::RES_MAX> locations {};
for (uint32_t i = 0; i < parameters.size(); i++) {
const auto location =
tess_control ? parameters[i].input_location : parameters[i].output_location;
if (tess_control && locations[location] != 0) {
inputs[i] = locations[location];
continue;
}
inputs[i] = AddInterface(spv::StorageClass::Input,
Array(vec4_float_type, tess_control ? 3u : 4u));
Decorate(inputs[i], spv::Decoration::Location, location);
locations[location] = inputs[i];
}
}
void DefineOutputs(spv::ExecutionModel model) {
const auto tess_control = model == spv::ExecutionModel::TessellationControl;
if (tess_control) {
gl_out = AddInterface(spv::StorageClass::Output, Array(per_vertex_type, 4u));
tess_inner = AddInterface(spv::StorageClass::Output, Array(float_type, 2u));
Decorate(tess_inner, spv::Decoration::BuiltIn, Word(spv::BuiltIn::TessLevelInner));
builder.AddAnnotation(
{Word(spv::Op::OpDecorate), tess_inner, Word(spv::Decoration::Patch)});
tess_outer = AddInterface(spv::StorageClass::Output, Array(float_type, 4u));
Decorate(tess_outer, spv::Decoration::BuiltIn, Word(spv::BuiltIn::TessLevelOuter));
builder.AddAnnotation(
{Word(spv::Op::OpDecorate), tess_outer, Word(spv::Decoration::Patch)});
} else {
gl_out = AddInterface(spv::StorageClass::Output, per_vertex_type);
}
outputs.resize(parameters.size());
for (uint32_t i = 0; i < parameters.size(); i++) {
outputs[i] = AddInterface(spv::StorageClass::Output,
tess_control ? Array(vec4_float_type, 4u) : vec4_float_type);
Decorate(outputs[i], spv::Decoration::Location, parameters[i].output_location);
}
}
uint32_t Interpolate(uint32_t v0, uint32_t v1, uint32_t v2,
const std::array<uint32_t, 3>& barycentric) {
const auto p0 = Result(spv::Op::OpVectorTimesScalar, vec4_float_type, v0, barycentric[0]);
const auto p1 = Result(spv::Op::OpVectorTimesScalar, vec4_float_type, v1, barycentric[1]);
const auto p2 = Result(spv::Op::OpVectorTimesScalar, vec4_float_type, v2, barycentric[2]);
return Result(spv::Op::OpFAdd, vec4_float_type, p0,
Result(spv::Op::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;
std::array<uint32_t, 5> int_constants {};
std::array<uint32_t, 5> uint_constants {};
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::ExecutionModel::TessellationControl);
RectListEmitter evaluation(parameters, spv::ExecutionModel::TessellationEvaluation);
return {control.EmitControl(), evaluation.EmitEvaluation()};
}
} // namespace Libs::Graphics
+22
View File
@@ -0,0 +1,22 @@
#ifndef EMULATOR_SRC_GRAPHICS_SHADER_RECTLISTSHADER_H_
#define EMULATOR_SRC_GRAPHICS_SHADER_RECTLISTSHADER_H_
#include <cstdint>
#include <vector>
namespace Libs::Graphics {
struct ShaderPixelInputInfo;
struct ShaderVertexInputInfo;
struct RectListShaders {
std::vector<uint32_t> control;
std::vector<uint32_t> evaluation;
};
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info);
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_SHADER_RECTLISTSHADER_H_
+37
View File
@@ -45,6 +45,42 @@
namespace Libs::Graphics {
namespace {
constexpr uint32_t PsInputOffsetMask = 0x0000001fu;
constexpr uint32_t PsInputFlatShade = 0x00000400u;
} // namespace
uint32_t ShaderPixelParameterMappedLocation(const ShaderPixelInputInfo& info, uint32_t input) {
return input < info.input_num ? info.interpolator_settings[input] & PsInputOffsetMask : input;
}
uint32_t ShaderPixelParameterLocation(const ShaderPixelInputInfo& info,
std::span<const uint32_t> active_inputs, uint32_t input) {
std::array<bool, 32> used_locations {};
for (const auto active_input: active_inputs) {
auto location = ShaderPixelParameterMappedLocation(info, active_input);
if (location < used_locations.size() && used_locations[location]) {
location = active_input;
while (location < used_locations.size() && used_locations[location]) {
location++;
}
EXIT_NOT_IMPLEMENTED(location >= used_locations.size());
}
if (active_input == input) {
return location;
}
used_locations[location] = true;
}
return ShaderPixelParameterMappedLocation(info, input);
}
bool ShaderPixelParameterIsFlat(const ShaderPixelInputInfo& info, uint32_t input) {
return input < info.input_num && (info.interpolator_settings[input] & PsInputFlatShade) != 0;
}
struct ShaderBinaryInfo {
uint8_t signature[7];
uint8_t version;
@@ -1606,6 +1642,7 @@ ShaderId ShaderGetIdPS(const HW::PixelShaderInfo& regs, const ShaderPixelInputIn
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_pos_z));
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_pos_w));
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_front_face));
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_no_perspective));
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_pixel_kill_enable));
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_sample_mask_export_enable));
ret.ids.push_back(static_cast<uint32_t>(input_info.ps_early_z));
+5
View File
@@ -122,6 +122,11 @@ struct ShaderPixelInputInfo {
bool HasPositionInput() const { return ps_pos_x || ps_pos_y || ps_pos_z || ps_pos_w; }
};
uint32_t ShaderPixelParameterMappedLocation(const ShaderPixelInputInfo& info, uint32_t input);
uint32_t ShaderPixelParameterLocation(const ShaderPixelInputInfo& info,
std::span<const uint32_t> active_inputs, uint32_t input);
bool ShaderPixelParameterIsFlat(const ShaderPixelInputInfo& info, uint32_t input);
struct ShaderSharp {
uint16_t offset_dw : 15;
uint16_t size : 1;
+30 -26
View File
@@ -18,6 +18,7 @@
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <vector>
@@ -805,18 +806,23 @@ private:
Common::Mutex m_mutex;
};
static PhysicalMemory* g_physical_memory = nullptr;
static FlexibleMemory* g_flexible_memory = nullptr;
static PooledMemory* g_pooled_memory = nullptr;
static VirtualRanges* g_virtual_ranges = nullptr;
static GuestAddressSpace* g_guest_address_space = nullptr;
static callback_func_t g_alloc_callback = nullptr;
static callback_func_t g_free_callback = nullptr;
static std::atomic<uint64_t> g_memory_pool_committed = 0;
static void MemoryPoolSubtractCommitted(uint64_t len);
static std::unique_ptr<PhysicalMemory> g_physical_memory;
static std::unique_ptr<FlexibleMemory> g_flexible_memory;
static std::unique_ptr<PooledMemory> g_pooled_memory;
static std::unique_ptr<VirtualRanges> g_virtual_ranges;
static std::unique_ptr<GuestAddressSpace> g_guest_address_space;
static callback_func_t g_alloc_callback = nullptr;
static callback_func_t g_free_callback = nullptr;
static std::atomic<uint64_t> g_memory_pool_committed = 0;
static void MemoryPoolSubtractCommitted(uint64_t len);
// Keep host mappings, physical blocks, placeholders, and virtual ranges in step.
static std::recursive_mutex g_memory_operation_mutex;
// The base address the PS5 kernel hands out for hint-less user mappings. Guest code can
// assume mappings it did not place explicitly are at or above this (Sony's libc rejects a
// heap below it), so hint-less searches must not fall back to the low system-managed range.
static constexpr uint64_t GUEST_DEFAULT_MAP_BASE = 0x200000000ull;
static uint64_t FindGuestFreeRange(uint64_t search_addr, uint64_t size, uint64_t alignment) {
EXIT_IF(g_guest_address_space == nullptr || g_virtual_ranges == nullptr);
@@ -844,8 +850,11 @@ static uint64_t FindGuestFreeRange(uint64_t search_addr, uint64_t size, uint64_t
if (search_addr != 0) {
return find_in(search_addr, HOST_USER_MAX + 1u);
}
auto addr = find_in(HOST_SYSTEM_MANAGED_MIN, HOST_SYSTEM_MANAGED_MAX + 1u);
return addr != 0 ? addr : find_in(HOST_USER_MIN, HOST_USER_MAX + 1u);
auto addr = find_in(GUEST_DEFAULT_MAP_BASE, HOST_SYSTEM_MANAGED_MAX + 1u);
if (addr == 0) {
addr = find_in(HOST_USER_MIN, HOST_USER_MAX + 1u);
}
return addr;
}
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) {
@@ -970,11 +979,11 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size);
KYTY_SUBSYSTEM_INIT(Memory) {
g_flexible_memory_size_frozen = true;
VirtualMemory::Init();
g_guest_address_space = new GuestAddressSpace(PhysicalMemory::TotalSize());
g_physical_memory = new PhysicalMemory;
g_flexible_memory = new FlexibleMemory;
g_pooled_memory = new PooledMemory;
g_virtual_ranges = new VirtualRanges;
g_guest_address_space = std::make_unique<GuestAddressSpace>(PhysicalMemory::TotalSize());
g_physical_memory = std::make_unique<PhysicalMemory>();
g_flexible_memory = std::make_unique<FlexibleMemory>();
g_pooled_memory = std::make_unique<PooledMemory>();
g_virtual_ranges = std::make_unique<VirtualRanges>();
EXIT_IF(!g_guest_address_space->SelfTest());
EXIT_IF(!SelfTestSub64SharedPlaceholderAlias());
}
@@ -982,16 +991,11 @@ KYTY_SUBSYSTEM_INIT(Memory) {
KYTY_SUBSYSTEM_UNEXPECTED_SHUTDOWN(Memory) {}
KYTY_SUBSYSTEM_DESTROY(Memory) {
delete g_pooled_memory;
g_pooled_memory = nullptr;
delete g_flexible_memory;
g_flexible_memory = nullptr;
delete g_physical_memory;
g_physical_memory = nullptr;
delete g_virtual_ranges;
g_virtual_ranges = nullptr;
delete g_guest_address_space;
g_guest_address_space = nullptr;
g_pooled_memory.reset();
g_flexible_memory.reset();
g_physical_memory.reset();
g_virtual_ranges.reset();
g_guest_address_space.reset();
}
struct AlignedPos {
+73 -2
View File
@@ -1058,6 +1058,76 @@ private:
{HOST_SYSTEM_RESERVED_MIN, HOST_SYSTEM_RESERVED_MAX + 1u},
{HOST_USER_MIN, HOST_USER_MAX + 1u},
}};
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
std::vector<std::pair<uint64_t, uint64_t>> occupied;
FILE* maps = fopen("/proc/self/maps", "r");
EXIT_IF(maps == nullptr);
char line[512];
while (fgets(line, sizeof(line), maps) != nullptr) {
unsigned long long mapping_start = 0;
unsigned long long mapping_end = 0;
if (sscanf(line, "%llx-%llx", &mapping_start, &mapping_end) == 2) {
occupied.emplace_back(static_cast<uint64_t>(mapping_start),
static_cast<uint64_t>(mapping_end));
}
}
fclose(maps);
auto reserve_range = [this](uint64_t start, uint64_t end) {
start = AlignUp(start, PageSize());
end = AlignDown(end, PageSize());
if (start == 0 || end <= start) {
return;
}
const auto size = end - start;
int flags = MAP_PRIVATE | MAP_ANON | MAP_NORESERVE;
#if defined(KYTY_LINKED_GUEST_ADDRESS_SPACE)
flags |= MAP_FIXED;
#elif defined(MAP_FIXED_NOREPLACE)
flags |= MAP_FIXED_NOREPLACE;
#endif
void* ptr = mmap(reinterpret_cast<void*>(start), size, PROT_NONE, flags, -1, 0);
if (ptr == MAP_FAILED || reinterpret_cast<uint64_t>(ptr) != start) {
if (ptr != MAP_FAILED) {
munmap(ptr, size);
}
return;
}
AddFreeUnlocked(start, size);
m_owned.emplace_back(start, size);
};
for (const auto& [region_start, region_end]: regions) {
auto current = region_start;
for (const auto& [mapping_start, mapping_end]: occupied) {
if (mapping_end <= current) {
continue;
}
if (mapping_start >= region_end) {
break;
}
if (mapping_start > current) {
reserve_range(current, std::min(mapping_start, region_end));
}
current = std::max(current, mapping_end);
if (current >= region_end) {
break;
}
}
if (current < region_end) {
reserve_range(current, region_end);
}
}
#else
for (const auto& [start, end]: regions) {
int flags = MAP_PRIVATE | MAP_ANON | MAP_NORESERVE;
#if defined(KYTY_LINKED_GUEST_ADDRESS_SPACE)
@@ -1070,13 +1140,14 @@ private:
if (ptr != MAP_FAILED) {
munmap(ptr, end - start);
}
EXIT("failed to reserve guest address space at 0x%016" PRIx64 ", size 0x%016" PRIx64
"\n",
EXIT("failed to reserve guest address space at 0x%016" PRIx64
", size 0x%016" PRIx64 "\n",
start, end - start);
}
AddFreeUnlocked(start, end - start);
m_owned.emplace_back(start, end - start);
}
#endif
#endif
}
+52 -43
View File
@@ -65,6 +65,10 @@
namespace Libs {
namespace LibcInternalExt {
void RunThreadAtexitDestructors();
} // namespace LibcInternalExt
namespace LibKernel {
LIB_NAME("libkernel", "libkernel");
@@ -361,6 +365,7 @@ struct PthreadAttrPrivate {
uint64_t stack_map_addr;
size_t stack_map_size;
int policy;
int guest_priority;
int inherit_sched;
int solosched;
bool detached;
@@ -2124,13 +2129,8 @@ int KYTY_SYSV_ABI PthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedP
int result = pthread_attr_getschedparam(&(*attr)->p, param);
if (param->sched_priority <= -2) {
param->sched_priority = 767;
} else if (param->sched_priority >= +2) {
param->sched_priority = 256;
} else {
param->sched_priority = 700;
}
// Host priority mapping is lossy; return the exact guest value.
param->sched_priority = (*attr)->guest_priority;
if (result == 0) {
return OK;
@@ -2305,10 +2305,17 @@ int KYTY_SYSV_ABI PthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedP
int result = pthread_attr_setschedparam(&attr_value->p, &pparam);
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (result == 0) {
attr_value->guest_priority = param->sched_priority;
return OK;
}
return KERNEL_ERROR_EINVAL;
#else
attr_value->guest_priority = param->sched_priority;
(void)result;
return OK;
#endif
}
int KYTY_SYSV_ABI PthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) {
@@ -3347,6 +3354,8 @@ int PthreadGetCurrentPriorityForKernel() {
static void CleanupThread(void* arg) {
auto* thread = static_cast<Pthread>(arg);
LibcInternalExt::RunThreadAtexitDestructors();
auto thread_dtors = g_pthread_context->GetThreadDtors();
if (thread_dtors != nullptr) {
@@ -3633,26 +3642,15 @@ int KYTY_SYSV_ABI PthreadGetprio(Pthread thread, int* prio) {
EXIT_NOT_IMPLEMENTED(prio == nullptr);
sched_param param {};
int pol = 0;
int result = pthread_getschedparam(thread->p, &pol, &param);
if (result == 0) {
if (param.sched_priority <= -2) {
*prio = 767;
} else if (param.sched_priority >= +2) {
*prio = 256;
} else {
*prio = 700;
}
LOGF("\t PthreadGetprio: %d, %d\n", thread->unique_id, *prio);
return OK;
sched_param native_param {};
int native_policy = 0;
if (pthread_getschedparam(thread->p, &native_policy, &native_param) != 0) {
return KERNEL_ERROR_EINVAL;
}
return KERNEL_ERROR_EINVAL;
*prio = thread->attr->guest_priority;
LOGF("\t PthreadGetprio: %d, %d\n", thread->unique_id, *prio);
return OK;
}
int KYTY_SYSV_ABI PthreadSetprio(Pthread thread, int prio) {
@@ -3667,25 +3665,32 @@ int KYTY_SYSV_ABI PthreadSetprio(Pthread thread, int prio) {
int result = pthread_getschedparam(thread->p, &pol, &param);
if (result == 0) {
if (prio <= 478) {
param.sched_priority = +2;
} else if (prio >= 733) {
param.sched_priority = -2;
} else {
param.sched_priority = 0;
}
result = pthread_setschedparam(thread->p, pol, &param);
if (result == 0) {
LOGF("\t PthreadSetprio: %d, %d\n", thread->unique_id, prio);
return OK;
}
if (result != 0) {
return KERNEL_ERROR_EINVAL;
}
if (prio <= 478) {
param.sched_priority = +2;
} else if (prio >= 733) {
param.sched_priority = -2;
} else {
param.sched_priority = 0;
}
result = pthread_setschedparam(thread->p, pol, &param);
if (result == 0) {
thread->attr->guest_priority = prio;
LOGF("\t PthreadSetprio: %d, %d\n", thread->unique_id, prio);
return OK;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
return KERNEL_ERROR_EINVAL;
#else
thread->attr->guest_priority = prio;
return OK;
#endif
}
void KYTY_SYSV_ABI PthreadTestcancel() {
@@ -3880,8 +3885,12 @@ int KYTY_SYSV_ABI KernelGettimeofday(KernelTimeval* tp) {
tp->tv_sec = static_cast<int64_t>(ticks / 1000000);
tp->tv_usec = static_cast<int64_t>(ticks % 1000000);
#else
auto dt = Common::DateTime::FromSystemUTC();
sec_to_timeval(tp, dt.ToUnix());
struct timespec ts {};
result = ::clock_gettime(CLOCK_REALTIME, &ts);
if (result == 0) {
tp->tv_sec = static_cast<int64_t>(ts.tv_sec);
tp->tv_usec = static_cast<int64_t>(ts.tv_nsec / 1000);
}
#endif
if (result == 0) {
+4 -4
View File
@@ -141,14 +141,14 @@ elseif(LINUX)
endif()
set(tidy_dirs "${CMAKE_SOURCE_DIR}/launcher/include")
set(iwyu_maps "${CMAKE_SOURCE_DIR}/launcher/utils/qt6_16.imp")
set(tidy_dirs "${CMAKE_CURRENT_SOURCE_DIR}/include")
set(iwyu_maps "${CMAKE_CURRENT_SOURCE_DIR}/utils/qt6_16.imp")
get_property(inc_headers TARGET launcher PROPERTY INCLUDE_DIRECTORIES)
list(APPEND inc_headers
${CMAKE_SOURCE_DIR}/launcher
${CMAKE_BINARY_DIR}/launcher/launcher_autogen/include
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}/launcher_autogen/include
${Qt6Widgets_INCLUDE_DIRS}
${Qt6Core_INCLUDE_DIRS}
${Qt6Gui_INCLUDE_DIRS}
@@ -78,19 +78,6 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_ngg_rectlist_draw">
<property name="toolTip">
<string>Use the NGG 4-vertex path for rect-list DrawIndexAuto primitive 7</string>
</property>
<property name="text">
<string>Use NGG rect-list draw</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
-5
View File
@@ -94,7 +94,6 @@ public:
QString printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool renderdoc_enabled = false;
bool ngg_rectlist_draw_enabled = true;
QString elf = QStringLiteral("eboot.bin");
@@ -112,7 +111,6 @@ public:
printf_output_file = other.printf_output_file;
profiler_direction = other.profiler_direction;
renderdoc_enabled = other.renderdoc_enabled;
ngg_rectlist_draw_enabled = other.ngg_rectlist_draw_enabled;
}
void CopyFrom(const Configuration& other) {
@@ -147,7 +145,6 @@ public:
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_direction);
KYTY_CFG_SET(renderdoc_enabled);
KYTY_CFG_SET(ngg_rectlist_draw_enabled);
KYTY_CFG_SET(elf);
}
@@ -169,8 +166,6 @@ public:
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_direction);
KYTY_CFG_GET(renderdoc_enabled);
ngg_rectlist_draw_enabled =
s->value("ngg_rectlist_draw_enabled", ngg_rectlist_draw_enabled).toBool();
elf = s->value("elf", elf).toString();
}
};
@@ -124,7 +124,6 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_shader_validation->setChecked(info.shader_validation_enabled);
m_ui->checkBox_vulkan_validation->setChecked(info.vulkan_validation_enabled);
m_ui->checkBox_renderdoc_capture->setChecked(info.renderdoc_enabled);
m_ui->checkBox_ngg_rectlist_draw->setChecked(info.ngg_rectlist_draw_enabled);
ListInit(m_ui->comboBox_shader_optimization_type, info.shader_optimization_type);
ListInit(m_ui->comboBox_shader_log_direction, info.shader_log_direction);
m_ui->lineEdit_shader_log_folder->setText(info.shader_log_folder);
@@ -245,7 +244,6 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
info.shader_validation_enabled = ui.checkBox_shader_validation->isChecked();
info.renderdoc_enabled = ui.checkBox_renderdoc_capture->isChecked();
info.ngg_rectlist_draw_enabled = ui.checkBox_ngg_rectlist_draw->isChecked();
info.shader_optimization_type = TextToEnum<Configuration::ShaderOptimizationType>(
ui.comboBox_shader_optimization_type->currentText());
info.shader_log_direction = TextToEnum<Configuration::ShaderLogDirection>(
-1
View File
@@ -216,7 +216,6 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--printf-output-file" << info.printf_output_file;
args << "--profiler-direction" << EnumToText(info.profiler_direction);
args << "--spirv-debug-printf" << "false";
args << "--ngg-rectlist-draw" << BoolArg(info.ngg_rectlist_draw_enabled);
if (info.renderdoc_enabled) {
args << "--rd";
}
+121 -209
View File
@@ -221,57 +221,6 @@ static RegisterDefaults* get_internal_register_defaults(uint32_t ver) {
return get_register_defaults(g_agc_internal_reg_defaults_by_version[index], &storage[index]);
}
struct PendingGraphicsSegment {
uint32_t* start = nullptr;
uint32_t* end = nullptr;
uint32_t* range_end = nullptr;
};
static std::mutex g_pending_graphics_segment_mutex;
static PendingGraphicsSegment g_pending_graphics_segment;
static void track_pending_graphics_segment_after_submit(uint32_t* dcb, uint32_t size_in_dwords) {
if (dcb == nullptr || size_in_dwords == 0) {
return;
}
auto* segment_start = dcb + size_in_dwords;
auto* range_end = segment_start + 0xfffffu;
std::lock_guard lock(g_pending_graphics_segment_mutex);
g_pending_graphics_segment.start = segment_start;
g_pending_graphics_segment.end = segment_start;
g_pending_graphics_segment.range_end = range_end;
}
static void track_pending_graphics_allocation(uint32_t* cmd, uint32_t size_dw) {
if (cmd == nullptr || size_dw == 0) {
return;
}
std::lock_guard lock(g_pending_graphics_segment_mutex);
auto* range_start = g_pending_graphics_segment.start;
auto* range_end = g_pending_graphics_segment.range_end;
if (range_start == nullptr || range_end == nullptr || cmd < range_start || cmd >= range_end) {
return;
}
auto* cmd_end = cmd + size_dw;
if (cmd > g_pending_graphics_segment.end) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t pending graphics segment: ignoring non-contiguous allocation cmd = "
"0x%016" PRIx64 ", tracked_end = 0x%016" PRIx64 "\n",
reinterpret_cast<uint64_t>(cmd),
reinterpret_cast<uint64_t>(g_pending_graphics_segment.end));
}
return;
}
if (cmd_end > g_pending_graphics_segment.end && cmd_end <= range_end) {
g_pending_graphics_segment.end = cmd_end;
}
}
struct CommandBuffer {
using Callback = KYTY_SYSV_ABI bool (*)(CommandBuffer*, uint32_t, void*);
@@ -370,7 +319,6 @@ struct CommandBuffer {
}
auto* ret_ptr = cursor_up;
cursor_up += size_dw;
track_pending_graphics_allocation(ret_ptr, size_dw);
return ret_ptr;
}
};
@@ -834,6 +782,104 @@ int KYTY_SYSV_ABI GraphicsUnknownFuseShaderHalves(Shader* fused_result, const Sh
return OK;
}
static void merge_shader_register_max_field(ShaderRegister* dst, const ShaderRegister* src,
uint32_t shift, uint32_t mask) {
const auto dst_field = (dst->value >> shift) & mask;
const auto src_field = (src->value >> shift) & mask;
const auto field = std::max(dst_field, src_field);
dst->value &= ~(mask << shift);
dst->value |= field << shift;
}
int KYTY_SYSV_ABI GraphicsUnknownNApJjpKNBl4(Shader* fused_result, const Shader* front,
const Shader* back, void* scratch_mem) {
PRINT_NAME();
LOGF("\t fused_result = 0x%016" PRIx64 "\n"
"\t front = 0x%016" PRIx64 "\n"
"\t back = 0x%016" PRIx64 "\n"
"\t scratch_mem = 0x%016" PRIx64 "\n",
reinterpret_cast<uint64_t>(fused_result), reinterpret_cast<uint64_t>(front),
reinterpret_cast<uint64_t>(back), reinterpret_cast<uint64_t>(scratch_mem));
const auto front_type = static_cast<Prospero::ShaderBinaryType>(front->type);
const auto is_gs = front_type == Prospero::ShaderBinaryType::kGsFront;
const auto is_hs = front_type == Prospero::ShaderBinaryType::kHsFront;
if ((!is_gs && !is_hs) ||
(is_gs && back->type != static_cast<uint8_t>(Prospero::ShaderBinaryType::kGsBack)) ||
(is_hs && back->type != static_cast<uint8_t>(Prospero::ShaderBinaryType::kHsBack))) {
return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
}
*fused_result = *back;
fused_result->type = static_cast<uint8_t>(is_gs ? Prospero::ShaderBinaryType::kGs
: Prospero::ShaderBinaryType::kHs);
const auto back_stages = back->specials->vgt_shader_stages_en.value;
const auto front_stages = front->specials->vgt_shader_stages_en.value;
const auto mismatch_bit = is_gs ? (1u << 22u) : (1u << 21u);
if (((front_stages ^ back_stages) & mismatch_bit) != 0) {
return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
}
if (scratch_mem != nullptr) {
auto* sh_registers = static_cast<ShaderRegister*>(scratch_mem);
memcpy(sh_registers, back->sh_registers,
static_cast<size_t>(back->num_sh_registers) * sizeof(ShaderRegister));
fused_result->sh_registers = sh_registers;
}
auto* fused_regs = fused_result->sh_registers;
const auto fused_reg_count = static_cast<uint32_t>(fused_result->num_sh_registers);
const auto front_reg_count = static_cast<uint32_t>(front->num_sh_registers);
const auto checksum_offset =
is_gs ? Pm4::SPI_SHADER_PGM_CHKSUM_GS : Pm4::SPI_SHADER_PGM_CHKSUM_HS;
const auto* front_checksum0 =
find_shader_register(front->sh_registers, front_reg_count, checksum_offset, 0);
const auto* front_checksum1 =
find_shader_register(front->sh_registers, front_reg_count, checksum_offset, 1);
auto* fused_checksum0 = find_shader_register(fused_regs, fused_reg_count, checksum_offset, 0);
auto* fused_checksum1 = find_shader_register(fused_regs, fused_reg_count, checksum_offset, 1);
fused_checksum0->value = front_checksum0->value;
fused_checksum1->value = front_checksum1->value;
const auto rsrc1_offset = is_gs ? Pm4::SPI_SHADER_PGM_RSRC1_GS : Pm4::SPI_SHADER_PGM_RSRC1_HS;
const auto rsrc2_offset = is_gs ? Pm4::SPI_SHADER_PGM_RSRC2_GS : Pm4::SPI_SHADER_PGM_RSRC2_HS;
const auto* front_rsrc1 =
find_shader_register(front->sh_registers, front_reg_count, rsrc1_offset);
const auto* front_rsrc2 =
find_shader_register(front->sh_registers, front_reg_count, rsrc2_offset);
auto* fused_rsrc1 = find_shader_register(fused_regs, fused_reg_count, rsrc1_offset);
auto* fused_rsrc2 = find_shader_register(fused_regs, fused_reg_count, rsrc2_offset);
merge_shader_register_max_field(fused_rsrc1, front_rsrc1, 0, 0x3fu);
merge_shader_register_max_field(fused_rsrc2, front_rsrc2, 28, 0x0fu);
if (is_gs) {
merge_shader_register_max_field(fused_rsrc1, front_rsrc1, 29, 0x03u);
merge_shader_register_max_field(fused_rsrc2, front_rsrc2, 16, 0x03u);
fused_rsrc2->value =
(fused_rsrc2->value & 0xf7ffffc1u) | (front_rsrc2->value & 0x0800003eu);
fused_rsrc2->value =
(fused_rsrc2->value & 0xfffbffffu) | (front_rsrc2->value & 0x00040000u);
} else {
merge_shader_register_max_field(fused_rsrc1, front_rsrc1, 28, 0x03u);
fused_rsrc2->value =
(fused_rsrc2->value & 0xf7ffffc1u) | (front_rsrc2->value & 0x0800003eu);
}
const auto program_lo_offset = is_gs ? Pm4::SPI_SHADER_PGM_LO_ES : Pm4::SPI_SHADER_PGM_LO_LS;
auto* program_lo = find_shader_register(fused_regs, fused_reg_count, program_lo_offset);
const auto address = reinterpret_cast<uint64_t>(front->code);
program_lo->value = static_cast<uint32_t>(address >> 8u);
(program_lo + 1)->value &= 0xffffff00u;
(program_lo + 1)->value |= static_cast<uint32_t>((address >> 40u) & 0xffu);
fused_result->user_data = front->user_data;
return OK;
}
static constexpr int GRAPHICS5_ERROR_INVALID_PACKET = static_cast<int>(0x8a6c000cu);
enum class RegIndirectPacket : uint32_t {
@@ -1347,7 +1393,7 @@ int KYTY_SYSV_ABI GraphicsWriteDataPatchSetAddressOrOffset(uint32_t* cmd,
return OK;
}
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
int KYTY_SYSV_ABI GraphicsUnknownJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* target,
@@ -1820,7 +1866,6 @@ uint32_t* KYTY_SYSV_ABI GraphicsCbReleaseMem(CommandBuffer* buf, uint8_t action,
cmd[5] = static_cast<uint32_t>(packet_data & 0xffffffffu);
cmd[6] = static_cast<uint32_t>((packet_data >> 32u) & 0xffffffffu);
cmd[7] = interrupt_ctx_id & 0x07ffffffu;
return cmd;
}
@@ -2380,7 +2425,7 @@ int KYTY_SYSV_ABI GraphicsUnknownIkfdtRIqCE(uint32_t* cmd, uint64_t arg1,
auto op = (cmd[0] >> 8u) & 0xffu;
if (op != Pm4::IT_INDIRECT_BUFFER) {
return 0x8a6c000c;
return GRAPHICS5_ERROR_INVALID_PACKET;
}
auto vaddr = reinterpret_cast<uint64_t>(target);
@@ -2800,6 +2845,10 @@ uint32_t KYTY_SYSV_ABI GraphicsAcbCondExecGetSize() {
return GraphicsDcbCondExecGetSize();
}
uint32_t KYTY_SYSV_ABI GraphicsAcbJumpGetSize() {
return 0x10u;
}
uint32_t* KYTY_SYSV_ABI GraphicsAcbWaitRegMem(CommandBuffer* buf, uint8_t size,
uint8_t compare_function, uint8_t cache_policy,
const volatile void* address, uint64_t reference,
@@ -3119,7 +3168,7 @@ int KYTY_SYSV_ABI GraphicsDmaDataPatchSetDstAddressOrOffset(uint32_t* cmd,
return OK;
}
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
int KYTY_SYSV_ABI GraphicsDmaDataPatchSetSrcAddressOrOffsetOrImmediate(
@@ -3133,7 +3182,7 @@ int KYTY_SYSV_ABI GraphicsDmaDataPatchSetSrcAddressOrOffsetOrImmediate(
return OK;
}
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
uint32_t KYTY_SYSV_ABI GraphicsGetPacketSize(uint32_t* packet) {
@@ -3197,6 +3246,15 @@ int KYTY_SYSV_ABI GraphicsSetRangePredication(uint32_t* start, const volatile ui
return OK;
}
int KYTY_SYSV_ABI GraphicsRewindPatchSetRewindState(uint32_t* cmd, uint8_t state) {
if (((cmd[0] >> 8u) & 0xffu) != Pm4::IT_REWIND) {
return GRAPHICS5_ERROR_INVALID_PACKET;
}
cmd[1] = (cmd[1] & 0x7fffffffu) | (static_cast<uint32_t>(state) << 31u);
return OK;
}
int KYTY_SYSV_ABI GraphicsCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer) {
PRINT_NAME();
@@ -3205,23 +3263,23 @@ int KYTY_SYSV_ABI GraphicsCondExecPatchSetEnd(uint32_t* cmd, const volatile uint
reinterpret_cast<uint64_t>(cmd), reinterpret_cast<uint64_t>(buffer));
if (cmd == nullptr || buffer == nullptr) {
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
auto op = (cmd[0] >> 8u) & 0xffu;
if (op != Pm4::IT_COND_EXEC) {
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
auto* packet_end = cmd + 5;
auto* range_end = const_cast<uint32_t*>(reinterpret_cast<const volatile uint32_t*>(buffer));
if (range_end < packet_end) {
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
auto num_dwords = static_cast<uint64_t>(range_end - packet_end);
if (num_dwords > 0x3fffu) {
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
cmd[4] = (cmd[4] & ~0x3fffu) | static_cast<uint32_t>(num_dwords);
@@ -3237,12 +3295,12 @@ int KYTY_SYSV_ABI GraphicsCondExecPatchSetCommandAddress(uint32_t*
reinterpret_cast<uint64_t>(cmd), reinterpret_cast<uint64_t>(command));
if (cmd == nullptr || command == nullptr) {
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
auto op = (cmd[0] >> 8u) & 0xffu;
if (op != Pm4::IT_COND_EXEC || (reinterpret_cast<uintptr_t>(command) & 0x3u) != 0) {
return static_cast<int>(0x8a6c000cu);
return GRAPHICS5_ERROR_INVALID_PACKET;
}
auto addr = reinterpret_cast<uint64_t>(command);
@@ -3704,150 +3762,6 @@ static void submit_dcb(uint32_t* dcb, uint32_t size_in_dwords) {
EXIT_IF(g_renderer == nullptr);
g_renderer->GetGpu().Submit(dcb, size_in_dwords, nullptr, 0,
!dcb_has_queued_interrupt(dcb, size_in_dwords));
Gen5::track_pending_graphics_segment_after_submit(dcb, size_in_dwords);
}
static std::vector<uint64_t> collect_acb_wait_addresses(const uint32_t* acb,
uint32_t size_in_dwords) {
std::vector<uint64_t> addresses;
for (uint32_t offset = 0; offset < size_in_dwords;) {
auto cmd_id = acb[offset];
auto len = KYTY_PM4_LEN(cmd_id);
if (len == 0 || len > size_in_dwords - offset) {
return addresses;
}
auto op = (cmd_id >> 8u) & 0xffu;
if (op == Pm4::IT_NOP && KYTY_PM4_R(cmd_id) == Pm4::R_WAIT_MEM_32 && len >= 7) {
auto address = static_cast<uint64_t>(acb[offset + 1]) |
(static_cast<uint64_t>(acb[offset + 2]) << 32u);
if (address != 0) {
addresses.push_back(address);
}
} else if (op == Pm4::IT_NOP && KYTY_PM4_R(cmd_id) == Pm4::R_WAIT_MEM_64 && len >= 9) {
auto address = static_cast<uint64_t>(acb[offset + 1]) |
(static_cast<uint64_t>(acb[offset + 2]) << 32u);
if (address != 0) {
addresses.push_back(address);
}
}
offset += len;
}
return addresses;
}
static bool acb_waits_for_address(const std::vector<uint64_t>& wait_addresses,
uint64_t release_address) {
for (auto address: wait_addresses) {
if (address == release_address) {
return true;
}
}
return false;
}
static void flush_pending_graphics_segment_before_acb(const uint32_t* acb,
uint32_t acb_size_in_dwords) {
uint32_t* dcb = nullptr;
uint32_t size_in_dwords = 0;
auto wait_addresses = collect_acb_wait_addresses(acb, acb_size_in_dwords);
{
std::lock_guard lock(Gen5::g_pending_graphics_segment_mutex);
if (!wait_addresses.empty() && Gen5::g_pending_graphics_segment.start != nullptr) {
auto* scan = Gen5::g_pending_graphics_segment.start;
auto* matched_end = Gen5::g_pending_graphics_segment.start;
while (scan < Gen5::g_pending_graphics_segment.end) {
auto cmd_id = *scan;
if (cmd_id == 0x80000000u) {
scan++;
continue;
}
if ((cmd_id & 0xC0000000u) != 0xC0000000u) {
break;
}
auto len = KYTY_PM4_LEN(cmd_id);
if (len == 0 ||
len > static_cast<uint32_t>(Gen5::g_pending_graphics_segment.end - scan)) {
break;
}
if (((cmd_id >> 8u) & 0xffu) == Pm4::IT_NOP &&
KYTY_PM4_R(cmd_id) == Pm4::R_RELEASE_MEM && len >= 7) {
auto release_addr =
static_cast<uint64_t>(scan[3]) | (static_cast<uint64_t>(scan[4]) << 32u);
if (acb_waits_for_address(wait_addresses, release_addr)) {
matched_end = scan + len;
}
}
scan += len;
}
if (matched_end > Gen5::g_pending_graphics_segment.start) {
Gen5::g_pending_graphics_segment.end = matched_end;
}
}
if (Gen5::g_pending_graphics_segment.start != nullptr &&
Gen5::g_pending_graphics_segment.end > Gen5::g_pending_graphics_segment.start) {
auto* scan = Gen5::g_pending_graphics_segment.start;
auto* valid_end = Gen5::g_pending_graphics_segment.start;
while (scan < Gen5::g_pending_graphics_segment.end) {
auto cmd_id = *scan;
if (cmd_id == 0x80000000u) {
scan++;
valid_end = scan;
continue;
}
if ((cmd_id & 0xC0000000u) != 0xC0000000u) {
break;
}
auto len = KYTY_PM4_LEN(cmd_id);
if (len == 0 ||
len > static_cast<uint32_t>(Gen5::g_pending_graphics_segment.end - scan)) {
break;
}
scan += len;
valid_end = scan;
}
if (valid_end < Gen5::g_pending_graphics_segment.end) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t trimming pending graphics segment: addr = 0x%016" PRIx64
", old_dw = 0x%08" PRIx32 ", new_dw = 0x%08" PRIx32 "\n",
reinterpret_cast<uint64_t>(Gen5::g_pending_graphics_segment.start),
static_cast<uint32_t>(Gen5::g_pending_graphics_segment.end -
Gen5::g_pending_graphics_segment.start),
static_cast<uint32_t>(valid_end - Gen5::g_pending_graphics_segment.start));
}
Gen5::g_pending_graphics_segment.end = valid_end;
}
}
if (Gen5::g_pending_graphics_segment.start == nullptr ||
Gen5::g_pending_graphics_segment.end <= Gen5::g_pending_graphics_segment.start) {
return;
}
dcb = Gen5::g_pending_graphics_segment.start;
size_in_dwords = static_cast<uint32_t>(Gen5::g_pending_graphics_segment.end -
Gen5::g_pending_graphics_segment.start);
}
LOGF("\t flushing pending graphics segment before ACB: addr = 0x%016" PRIx64
", dw_num = 0x%08" PRIx32 "\n",
reinterpret_cast<uint64_t>(dcb), size_in_dwords);
submit_dcb(dcb, size_in_dwords);
}
int KYTY_SYSV_ABI GraphicsDriverSubmitDcb(const Packet* packet) {
@@ -3963,8 +3877,6 @@ static void submit_acb(uint32_t queue, uint32_t* acb, uint32_t size_in_dwords) {
LOGF("\t acb[%u] = 0x%08" PRIx32 "\n", i, acb[i]);
}
flush_pending_graphics_segment_before_acb(acb, size_in_dwords);
GraphicsDbgDumpDcb("a", size_in_dwords, acb);
const bool trigger_interrupt_on_done = !dcb_has_queued_interrupt(acb, size_in_dwords);
+4
View File
@@ -38,6 +38,8 @@ int KYTY_SYSV_ABI GraphicsUnknownGetFusedShaderSize(SizeAlign* dst, const Shad
const Shader* back);
int KYTY_SYSV_ABI GraphicsUnknownFuseShaderHalves(Shader* fused_result, const Shader* front,
const Shader* back, void* scratch_mem);
int KYTY_SYSV_ABI GraphicsUnknownNApJjpKNBl4(Shader* fused_result, const Shader* front,
const Shader* back, void* scratch_mem);
int KYTY_SYSV_ABI GraphicsSetCxRegIndirectPatchSetAddress(uint32_t* cmd,
const volatile ShaderRegister* regs);
int KYTY_SYSV_ABI GraphicsSetShRegIndirectPatchSetAddress(uint32_t* cmd,
@@ -183,6 +185,7 @@ uint32_t KYTY_SYSV_ABI GraphicsAcbAcquireMemGetSize();
uint32_t* KYTY_SYSV_ABI GraphicsAcbCondExec(CommandBuffer* buf, const volatile uint32_t* address,
uint32_t num_dwords);
uint32_t KYTY_SYSV_ABI GraphicsAcbCondExecGetSize();
uint32_t KYTY_SYSV_ABI GraphicsAcbJumpGetSize();
uint32_t* KYTY_SYSV_ABI GraphicsAcbWaitRegMem(CommandBuffer* buf, uint8_t size,
uint8_t compare_function, uint8_t cache_policy,
const volatile void* address, uint64_t reference,
@@ -241,6 +244,7 @@ uint32_t KYTY_SYSV_ABI GraphicsGetPacketSize(uint32_t* packet);
int KYTY_SYSV_ABI GraphicsSetPacketPredication(uint32_t* packet, uint32_t predication);
int KYTY_SYSV_ABI GraphicsSetRangePredication(uint32_t* start, const volatile uint32_t* end,
uint32_t predication);
int KYTY_SYSV_ABI GraphicsRewindPatchSetRewindState(uint32_t* cmd, uint8_t state);
int KYTY_SYSV_ABI GraphicsCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer);
int KYTY_SYSV_ABI GraphicsCondExecPatchSetCommandAddress(uint32_t* cmd,
const volatile uint32_t* command);
+71 -2
View File
@@ -364,7 +364,12 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
}
if (blocking) {
const auto min_queued_size = queue_size * 2u;
constexpr uint64_t target_latency_us = 40000;
const auto buffer_us = port->freq != 0 ? (1000000ULL * port->samples_num) / port->freq : 0;
const auto buffers =
buffer_us != 0 ? static_cast<uint32_t>((target_latency_us + buffer_us - 1) / buffer_us)
: 2u;
const auto min_queued_size = queue_size * std::clamp(buffers, 2u, 16u);
const auto wait_start = LibKernel::KernelGetProcessTime();
while (SDL_GetQueuedAudioSize(port->audio_device) > min_queued_size) {
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
@@ -511,7 +516,16 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
}
if (blocking && max_wait_time != 0) {
bool all_ports_have_device = true;
for (uint32_t i = 0; i < num; i++) {
if (m_out_ports[params[i].handle.GetId()].audio_device == 0) {
all_ports_have_device = false;
break;
}
}
// Device-backed ports are paced by the SDL queue above.
if (blocking && max_wait_time != 0 && !all_ports_have_device) {
Common::Thread::SleepMicro(max_wait_time);
}
@@ -1416,6 +1430,12 @@ struct Ngs2CustomSubmixerRackOption {
uint32_t max_inputs = 0;
};
struct Ngs2CustomMasteringRackOption {
Ngs2CustomRackOption custom_rack_option;
uint32_t max_channels = 0;
uint32_t max_inputs = 0;
};
struct Ngs2CustomSamplerRackOption {
Ngs2CustomRackOption custom_rack_option;
uint32_t max_channel_works = 0;
@@ -1433,6 +1453,7 @@ union Ngs2RackOptionUnion {
Ngs2SubmixerRackOption submixer;
Ngs2ReverbRackOption reverb;
Ngs2CustomSubmixerRackOption custom_submixer;
Ngs2CustomMasteringRackOption custom_mastering;
Ngs2CustomSamplerRackOption custom_sampler;
};
@@ -1588,6 +1609,7 @@ enum class Ngs2RackType {
Mastering,
Reverb,
CustomSubmixer,
CustomMastering,
CustomSampler,
};
@@ -1665,6 +1687,20 @@ struct Ngs2VoiceCallbackParam {
struct Ngs2VoiceState {
uint32_t state_flags;
int32_t error_code;
};
struct Ngs2SubmixerVoiceState {
Ngs2VoiceState voice_state;
float envelope_height;
float peak_height;
float compressor_height;
};
struct Ngs2CustomMasteringVoiceState {
Ngs2VoiceState voice_state;
uint32_t reserved;
uint32_t reserved2;
};
struct Ngs2SamplerVoiceState {
@@ -1683,6 +1719,10 @@ static Ngs2RackInternal* g_racks_list = nullptr;
static_assert(sizeof(Ngs2SystemOption) == 144);
static_assert(sizeof(Ngs2RackOption) == 176);
static_assert(sizeof(Ngs2VoiceState) == 8);
static_assert(sizeof(Ngs2SubmixerVoiceState) == 20);
static_assert(sizeof(Ngs2CustomMasteringVoiceState) == 16);
static_assert(sizeof(Ngs2SamplerVoiceState) == 56);
static uint32_t Ngs2GetStateFlags(const Ngs2VoiceInternal* voice) {
switch (voice->state) {
@@ -1726,6 +1766,7 @@ static Ngs2Internal* Ngs2CreateSystemInternal(const Ngs2SystemOption* option, vo
static bool Ngs2RackIsCustom(Ngs2RackType type) {
switch (type) {
case Ngs2RackType::CustomSubmixer:
case Ngs2RackType::CustomMastering:
case Ngs2RackType::CustomSampler: return true;
default: return false;
}
@@ -2037,6 +2078,12 @@ int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
*reinterpret_cast<const Ngs2CustomSubmixerRackOption*>(option);
rack->type = Ngs2RackType::CustomSubmixer;
break;
case 0x4003:
EXIT_NOT_IMPLEMENTED(option->size != sizeof(Ngs2CustomMasteringRackOption));
rack->option.custom_mastering =
*reinterpret_cast<const Ngs2CustomMasteringRackOption*>(option);
rack->type = Ngs2RackType::CustomMastering;
break;
case 0x4001:
EXIT_NOT_IMPLEMENTED(option->size != sizeof(Ngs2CustomSamplerRackOption));
rack->option.custom_sampler =
@@ -2550,6 +2597,9 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x4002:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSubmixer);
break;
case 0x4003:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering);
break;
default: EXIT("unknown rack_id: 0x%" PRIx32 "\n", rack_id);
}
@@ -2587,6 +2637,25 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
Common::LockGuard lock(voice->rack->ngs->mutex);
switch (voice->rack->type) {
case Ngs2RackType::Submixer: {
EXIT_NOT_IMPLEMENTED(state_size != sizeof(Ngs2SubmixerVoiceState));
auto* submixer = reinterpret_cast<Ngs2SubmixerVoiceState*>(state);
*submixer = {};
submixer->voice_state.state_flags = Ngs2GetStateFlags(voice);
LOGF("\t state_flags = %u\n", submixer->voice_state.state_flags);
break;
}
case Ngs2RackType::CustomMastering: {
const auto configured_size =
voice->rack->option.custom_mastering.custom_rack_option.state_size;
EXIT_NOT_IMPLEMENTED(configured_size < sizeof(Ngs2CustomMasteringVoiceState));
EXIT_NOT_IMPLEMENTED(state_size != configured_size);
std::memset(state, 0, state_size);
auto* mastering = reinterpret_cast<Ngs2CustomMasteringVoiceState*>(state);
mastering->voice_state.state_flags = Ngs2GetStateFlags(voice);
LOGF("\t state_flags = %u\n", mastering->voice_state.state_flags);
break;
}
case Ngs2RackType::Sampler:
case Ngs2RackType::CustomSampler: {
if (state_size != sizeof(Ngs2SamplerVoiceState)) {
+46 -43
View File
@@ -336,18 +336,6 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr;
}
static uint32_t audioout2_context_grains(AudioOut2ContextHandle ctx) {
uint32_t samples_num = 512;
g_audioout2_context_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
samples_num = (state->num_grains == 0 ? 512u : state->num_grains);
}
g_audioout2_context_mutex.Unlock();
return samples_num;
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
@@ -455,6 +443,12 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
g_audioout2_context_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
*state = AudioOut2ContextState {};
}
g_audioout2_context_mutex.Unlock();
std::array<int, 256> audio_handles {};
size_t audio_handles_num = 0;
@@ -473,12 +467,6 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
audioout2_close_audio_handle(audio_handles[i]);
}
g_audioout2_context_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
*state = AudioOut2ContextState {};
}
g_audioout2_context_mutex.Unlock();
return OK;
}
@@ -559,30 +547,45 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(port == nullptr);
const auto next_port = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
const auto next_port = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
g_audioout2_context_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_context_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains;
g_audioout2_port_mutex.Lock();
auto* port_state = audioout2_find_port_locked(0);
if (port_state == nullptr) {
for (auto& candidate: g_audioout2_ports) {
if (!candidate.used) {
port_state = &candidate;
break;
}
AudioOut2PortStateEntry* port_state = nullptr;
for (auto& candidate: g_audioout2_ports) {
if (!candidate.used) {
port_state = &candidate;
break;
}
}
if (port_state != nullptr) {
*port_state = AudioOut2PortStateEntry {};
port_state->used = true;
port_state->handle = next_port;
port_state->context = ctx;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
port_state->sampling_freq = params->sampling_freq;
port_state->samples_num = samples_num;
port_state->audio_format = audio_format;
}
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
if (next_port > g_audioout2_ports.size() || port_state == nullptr) {
if (port_state == nullptr) {
return AUDIO_OUT2_ERROR_PORT_FULL;
}
*port = next_port;
const auto samples_num = audioout2_context_grains(ctx);
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
int audio_handle = 0;
int audio_handle = 0;
if (audio_format != AudioInternal::Format::Unknown &&
!audioout2_port_type_is_object(params->port_type)) {
@@ -591,17 +594,17 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
}
g_audioout2_port_mutex.Lock();
*port_state = AudioOut2PortStateEntry {};
port_state->used = true;
port_state->handle = *port;
port_state->context = ctx;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
port_state->sampling_freq = params->sampling_freq;
port_state->samples_num = samples_num;
port_state->audio_format = audio_format;
port_state->audio_handle = audio_handle;
const bool reserved = port_state->used && port_state->handle == next_port;
if (reserved) {
port_state->audio_handle = audio_handle;
}
g_audioout2_port_mutex.Unlock();
if (!reserved) {
audioout2_close_audio_handle(audio_handle);
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
*port = next_port;
if (next_port <= 16 || (next_port % 600) == 0) {
PRINT_NAME();
+24 -11
View File
@@ -633,6 +633,15 @@ LIB_VERSION("LibcInternalExt", 1, "LibcInternal", 1, 1);
static uint64_t g_mspace_atomic_id_mask = 0;
static uint64_t g_mstate_table[64] = {0};
using thread_atexit_destructor_t = KYTY_SYSV_ABI void (*)(void*);
struct ThreadAtexitDestructor {
thread_atexit_destructor_t destructor;
void* object;
};
static thread_local std::vector<ThreadAtexitDestructor> g_thread_atexit_destructors;
struct Info {
uint64_t size;
uint32_t unknown1;
@@ -650,25 +659,29 @@ void KYTY_SYSV_ABI LibcHeapGetTraceInfo(Info* info) {
info->mstate_table = g_mstate_table;
}
uint64_t KYTY_SYSV_ABI LibcInternalExtUnknownQBS714Jr3g(uint64_t arg0, uint64_t arg1, uint64_t arg2,
uint64_t arg3, uint64_t arg4,
uint64_t arg5) {
int KYTY_SYSV_ABI LibcInternalExtCxaThreadAtexit(thread_atexit_destructor_t destructor, void* object,
void* /*module_id*/) {
PRINT_NAME();
LOGF("\t arg0 = 0x%016" PRIx64 "\n"
"\t arg1 = 0x%016" PRIx64 "\n"
"\t arg2 = 0x%016" PRIx64 "\n"
"\t arg3 = 0x%016" PRIx64 "\n"
"\t arg4 = 0x%016" PRIx64 "\n"
"\t arg5 = 0x%016" PRIx64 "\n",
arg0, arg1, arg2, arg3, arg4, arg5);
g_thread_atexit_destructors.push_back({destructor, object});
return 0;
}
void RunThreadAtexitDestructors() {
while (!g_thread_atexit_destructors.empty()) {
auto destructor = g_thread_atexit_destructors.back();
g_thread_atexit_destructors.pop_back();
if (destructor.destructor != nullptr) {
destructor.destructor(destructor.object);
}
}
}
LIB_DEFINE(InitLibcInternalExt_1) {
LIB_FUNC("NWtTN10cJzE", LibcInternalExt::LibcHeapGetTraceInfo);
LIB_FUNC("qBS714-Jr3g", LibcInternalExt::LibcInternalExtUnknownQBS714Jr3g);
LIB_FUNC("qBS714-Jr3g", LibcInternalExt::LibcInternalExtCxaThreadAtexit);
}
} // namespace LibcInternalExt
+3
View File
@@ -159,6 +159,7 @@ LIB_DEFINE(InitGraphicsDriver_1) {
LIB_FUNC("f3dg2CSgRKY", Gen5::GraphicsCreateShader);
LIB_FUNC("dolOmWH+huQ", Gen5::GraphicsUnknownGetFusedShaderSize);
LIB_FUNC("fd5Bp5tGTgo", Gen5::GraphicsUnknownFuseShaderHalves);
LIB_FUNC("nApJjpKNBl4", Gen5::GraphicsUnknownNApJjpKNBl4);
LIB_FUNC("vcmNN+AAXnY", Gen5::GraphicsSetCxRegIndirectPatchSetAddress);
LIB_FUNC("Qrj4c+61z4A", Gen5::GraphicsSetShRegIndirectPatchSetAddress);
LIB_FUNC("6lNcCp+fxi4", Gen5::GraphicsSetUcRegIndirectPatchSetAddress);
@@ -207,6 +208,7 @@ LIB_DEFINE(InitGraphicsDriver_1) {
LIB_FUNC("ewobAQeMo5k", Gen5::GraphicsAcbAcquireMemGetSize);
LIB_FUNC("qyM2bxYFPAk", Gen5::GraphicsAcbCondExec);
LIB_FUNC("ozKzBP4aki4", Gen5::GraphicsAcbCondExecGetSize);
LIB_FUNC("b-oySn+G2tE", Gen5::GraphicsAcbJumpGetSize);
LIB_FUNC("htn36gPnBk4", Gen5::GraphicsAcbWaitRegMem);
LIB_FUNC("-RnpfpxIhec", Gen5::GraphicsAcbDmaData);
LIB_FUNC("qzMN2XKGA4k", Gen5::GraphicsAcbCopyData);
@@ -282,6 +284,7 @@ LIB_DEFINE(InitGraphicsDriver_1) {
LIB_FUNC("YWTKOju587o", Gen5::GraphicsCondExecPatchSetCommandAddress);
LIB_FUNC("k-JpyR2dYAM", Gen5::GraphicsCondExecPatchSetEnd);
LIB_FUNC("3ZWa3AoyWZQ", Gen5::GraphicsCondExecPatchSetCommandAddress);
LIB_FUNC("ziVA3whp3p4", Gen5::GraphicsRewindPatchSetRewindState);
LIB_FUNC("YUeqkyT7mEQ", Gen5::GraphicsDcbSetFlip);
}
+5
View File
@@ -97,6 +97,10 @@ int KYTY_SYSV_ABI NetShutdown(int s, int how) {
return FinishSocketCall(Net::Shutdown(s, how));
}
int KYTY_SYSV_ABI NetGetsockname(int s, void* addr, uint32_t* addrlen) {
return FinishSocketCall(Net::Getsockname(s, addr, addrlen));
}
int KYTY_SYSV_ABI NetPoolCreate(const char* name, int size, int flags) {
return NET_CALL(Net::NetPoolCreate(name, size, flags));
}
@@ -196,6 +200,7 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("v6M4txecCuo", LibNet::NetEtherNtostr);
LIB_FUNC("6Oc0bLsIYe0", LibNet::NetGetMacAddress);
LIB_FUNC("hLuXdjHnhiI", LibNet::NetGetSockInfo);
LIB_FUNC("hoOAofhhRvE", LibNet::NetGetsockname);
LIB_FUNC("SF47kB2MNTo", LibNet::NetEpollCreate);
LIB_FUNC("ZVw46bsasAk", LibNet::NetEpollControl);
LIB_FUNC("drjIbDbA7UQ", LibNet::NetEpollWait);
+7 -14
View File
@@ -4,6 +4,7 @@
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <limits>
@@ -247,20 +248,12 @@ static int KYTY_SYSV_ABI RtcGetCurrentTick(RtcTick* tick) {
return RTC_ERROR_DATETIME_UNINITIALIZED;
}
const auto now = Common::DateTime::FromSystemUTC();
const auto date = now.GetDate();
const auto tod = now.GetTime();
RtcDateTime time {};
time.year = static_cast<uint16_t>(date.Year());
time.month = static_cast<uint16_t>(date.Month());
time.day = static_cast<uint16_t>(date.Day());
time.hour = static_cast<uint16_t>(tod.Hour24());
time.minute = static_cast<uint16_t>(tod.Minute());
time.second = static_cast<uint16_t>(tod.Second());
time.microsecond = static_cast<uint32_t>(tod.Msec() * 1000);
return RtcGetTick(&time, tick);
const auto now_us =
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count());
tick->tick = RTC_UNIX_EPOCH_TICKS + now_us;
return OK;
}
static int KYTY_SYSV_ABI RtcGetCurrentNetworkTick(RtcTick* tick) {
+2 -1
View File
@@ -375,7 +375,8 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
if (Common::File::IsDirectoryExisting(root)) {
for (const auto& entry: Common::File::GetDirEntries(root)) {
if (!entry.is_file && !Common::StartsWith(entry.name, "sce_")) {
if (!entry.is_file && entry.name != "." && entry.name != ".." &&
!Common::StartsWith(entry.name, "sce_")) {
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
dir_name_match(Common::ToLower(entry.name).c_str(),
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
+30 -4
View File
@@ -38,7 +38,10 @@
#include <windows.h>
#else
#include <dlfcn.h>
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
#if defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_vm.h>
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
#include <sys/uio.h>
#include <unistd.h>
#endif
@@ -145,7 +148,7 @@ static bool PatchGuestMemory64(uint64_t vaddr, uint64_t value) {
}
static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
constexpr uint64_t thunk_size = 162;
constexpr uint64_t thunk_size = 165;
if (g_unresolved_stub_thunk_pages.empty() ||
g_unresolved_stub_thunk_offset + thunk_size > UNRESOLVED_STUB_PAGE_SIZE) {
@@ -251,6 +254,10 @@ static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
emit(0x41);
emit(0xff);
emit(0xe3); // jmp r11
// Match the integer fallback for floating-point return values.
emit(0x0f);
emit(0x57);
emit(0xc0); // xorps xmm0, xmm0
emit(0x31);
emit(0xc0); // xor eax, eax
emit(0xc3); // ret
@@ -722,7 +729,26 @@ static bool IsReadableRange(uint64_t addr, uint64_t size) {
}
current = std::min(region_end, end);
}
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
#elif defined(__APPLE__)
// Walk the Mach regions covering the range and require read permission. The fatal
// report dumps memory behind raw register values, and a fault inside the reporter
// re-enters the signal handler and wedges the reporting thread.
uint64_t current = addr;
while (current < end) {
mach_vm_address_t region_addr = current;
mach_vm_size_t region_size = 0;
vm_region_basic_info_data_64_t info {};
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
mach_port_t object_name = MACH_PORT_NULL;
if (mach_vm_region(mach_task_self(), &region_addr, &region_size, VM_REGION_BASIC_INFO_64,
reinterpret_cast<vm_region_info_t>(&info), &count,
&object_name) != KERN_SUCCESS ||
region_addr > current || (info.protection & VM_PROT_READ) == 0) {
return false;
}
current = region_addr + region_size;
}
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
const auto page_size = static_cast<uint64_t>(sysconf(_SC_PAGESIZE));
if (page_size == 0) {
return false;
@@ -752,7 +778,7 @@ static bool IsReadableRange(uint64_t addr, uint64_t size) {
}
static bool IsDumpableRange(uint64_t addr, uint64_t size) {
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
return IsReadableRange(addr, size);
#else
(void)size;
-7
View File
@@ -59,8 +59,6 @@ static void PrintUsage() {
::printf(" --printf-output-file <path> Guest printf output file.\n");
::printf(" --profiler-direction <value> None or Network.\n");
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(" --ngg-rectlist-draw <true|false> Draw rect-list auto draws using the NGG "
"4-vertex path.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(" --rd Enable RenderDoc capture.\n");
@@ -220,11 +218,6 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--ngg-rectlist-draw") {
if (!ParseBool(value, options.config.ngg_rectlist_draw_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--readback-linear-images") {
if (!ParseBool(value, options.config.readback_linear_images)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
+249
View File
@@ -0,0 +1,249 @@
#include "libs/audio.h"
#include "libs/audio_internal.h"
#include "libs/errno.h"
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <thread>
#include <vector>
namespace {
namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
void Check(bool value, const char* text) {
if (!value) {
std::fprintf(stderr, "AudioOut2PortTests: failed: %s\n", text);
std::abort();
}
}
struct PortParam {
uint16_t port_type;
uint16_t pad;
uint32_t data_format;
uint32_t sampling_freq;
uint32_t flags;
uint64_t user_handle;
uint32_t reserved[10];
};
struct ContextParam {
uint32_t max_ports;
uint32_t max_object_ports;
uint32_t guarantee_object_ports;
uint32_t queue_depth;
uint32_t num_grains;
uint32_t flags;
uint32_t reserved[10];
};
struct PortState {
uint16_t output;
uint8_t num_channels;
uint8_t pad1;
int16_t volume;
uint16_t reroute_counter;
uint32_t flags;
uint32_t pad2;
uint64_t reserved[6];
};
const auto* AsParam(const PortParam* param) {
return reinterpret_cast<const AudioOut2::AudioOut2PortParam*>(param);
}
const auto* AsParam(const ContextParam* param) {
return reinterpret_cast<const AudioOut2::AudioOut2ContextParam*>(param);
}
auto* AsState(PortState* state) {
return reinterpret_cast<AudioOut2::AudioOut2PortState*>(state);
}
PortParam MakeParam(uint32_t data_format = 0x200) {
PortParam param {};
param.data_format = data_format;
param.sampling_freq = 48000;
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext() {
ContextParam param {};
param.queue_depth = 4;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
"context create failed");
return context;
}
void BlockDeviceOpens() {
std::lock_guard lock(g_device_mutex);
g_open_waiters = 0;
g_block_opens = true;
}
void WaitForDeviceOpens(int count) {
std::unique_lock lock(g_device_mutex);
g_device_cv.wait(lock, [count]() { return g_open_waiters >= count; });
}
void ReleaseDeviceOpens() {
std::lock_guard lock(g_device_mutex);
g_block_opens = false;
g_device_cv.notify_all();
}
int LiveDeviceCount() {
std::lock_guard lock(g_device_mutex);
return static_cast<int>(g_live_devices.size());
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
for (int i = 0; i < 300; i++) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"port slot was not reusable");
Check(port != 0, "port handle is zero");
AudioOut2::AudioOut2PortDestroy(port);
}
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestFullTableRecovers() {
const auto context = CreateContext();
const auto param = MakeParam();
std::vector<AudioOut2::AudioOut2PortHandle> ports;
ports.reserve(256);
for (int i = 0; i < 256; i++) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"port table filled early");
ports.push_back(port);
}
AudioOut2::AudioOut2PortHandle overflow = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &overflow) != OK,
"full port table accepted another port");
for (auto port: ports) {
AudioOut2::AudioOut2PortDestroy(port);
}
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"port table did not recover");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestConcurrentCreates() {
constexpr int thread_count = 8;
const auto context = CreateContext();
const auto param = MakeParam(0x800);
std::vector<AudioOut2::AudioOut2PortHandle> ports(thread_count);
std::vector<int> results(thread_count);
std::vector<std::thread> threads;
BlockDeviceOpens();
for (int i = 0; i < thread_count; i++) {
threads.emplace_back([&, i]() {
results[i] = AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &ports[i]);
});
}
WaitForDeviceOpens(thread_count);
ReleaseDeviceOpens();
for (auto& thread: threads) {
thread.join();
}
for (int i = 0; i < thread_count; i++) {
Check(results[i] == OK, "concurrent port create failed");
PortState state {};
AudioOut2::AudioOut2PortGetState(ports[i], AsState(&state));
Check(state.num_channels == 8, "concurrent create lost its reserved slot");
AudioOut2::AudioOut2PortDestroy(ports[i]);
}
Check(LiveDeviceCount() == 0, "concurrent create leaked a device");
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestContextDestroyCancelsPendingCreate() {
const auto context = CreateContext();
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
int result = OK;
BlockDeviceOpens();
std::thread creator(
[&]() { result = AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port); });
WaitForDeviceOpens(1);
AudioOut2::AudioOut2ContextDestroy(context);
ReleaseDeviceOpens();
creator.join();
Check(result != OK, "destroyed context retained a pending port create");
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
}
} // namespace
namespace Libs::Audio::AudioInternal {
int AudioOutOpen(int /*type*/, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) {
std::unique_lock lock(g_device_mutex);
const int handle = g_next_device++;
g_live_devices.push_back(handle);
g_open_waiters++;
g_device_cv.notify_all();
g_device_cv.wait(lock, []() { return !g_block_opens; });
return handle;
}
void AudioOutClose(int handle) {
std::lock_guard lock(g_device_mutex);
const auto it = std::find(g_live_devices.begin(), g_live_devices.end(), handle);
if (it != g_live_devices.end()) {
g_live_devices.erase(it);
}
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool /*blocking*/) {
return 0;
}
} // namespace Libs::Audio::AudioInternal
namespace Libs::LibKernel {
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
static std::atomic_uint64_t now {0};
return now.fetch_add(1000);
}
} // namespace Libs::LibKernel
int main() {
TestSlotReuse();
TestFullTableRecovers();
TestConcurrentCreates();
TestContextDestroyCancelsPendingCreate();
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
}
+310
View File
@@ -0,0 +1,310 @@
#include "common/bitArray.h"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <utility>
namespace {
using Bits = Common::BitArray<128>;
static_assert(sizeof(Common::BitArray<1024>) == 128);
void Check(bool value, const char *message) {
if (!value) {
std::fprintf(stderr, "BitArrayTests: failed: %s\n", message);
std::abort();
}
}
void TestPointAndRangeOperations() {
Bits bits;
Check(bits.None() && !bits.Any(), "default state is not empty");
for (const auto index : {size_t{0}, size_t{63}, size_t{64}, size_t{127}}) {
bits.Set(index);
Check(bits.Get(index), "Set did not set a boundary bit");
bits.Unset(index);
Check(!bits.Get(index), "Unset did not clear a boundary bit");
}
bits.SetRange(60, 68);
for (size_t index = 0; index < 128; index++) {
Check(bits.Get(index) == (index >= 60 && index < 68),
"cross-word SetRange changed the wrong bits");
}
bits.Fill();
bits.UnsetRange(60, 68);
for (size_t index = 0; index < 128; index++) {
Check(bits.Get(index) == !(index >= 60 && index < 68),
"cross-word UnsetRange changed the wrong bits");
}
bits.Clear();
bits.SetRange(0, 128);
Check(!bits.None(), "full SetRange left the array empty");
bits.UnsetRange(0, 128);
Check(bits.None(), "full UnsetRange left set bits");
bits.Set(7);
bits.SetRange(9, 9);
bits.SetRange(0, 129);
bits.UnsetRange(9, 9);
bits.UnsetRange(0, 129);
Check(bits.Get(7), "invalid or empty range modified the array");
}
void TestMaskedConstructionAndBitwiseOperations() {
Bits source;
source.Fill();
const Bits masked(source, 31, 97);
for (size_t index = 0; index < 128; index++) {
Check(masked.Get(index) == (index >= 31 && index < 97),
"masked constructor retained a bit outside its range");
}
Check(Bits(source, 12, 12).None(), "empty masked constructor produced bits");
Check(Bits(source, 0, 129).None(),
"invalid masked constructor produced bits");
Bits left;
left.SetRange(0, 80);
Bits right;
right.SetRange(40, 120);
const auto exclusive = left ^ right;
for (size_t index = 0; index < 128; index++) {
const bool expected = (index < 80) != (index >= 40 && index < 120);
Check(exclusive.Get(index) == expected, "XOR produced the wrong bit");
}
const auto inverted = ~left;
for (size_t index = 0; index < 128; index++) {
Check(inverted.Get(index) == (index >= 80), "NOT produced the wrong bit");
}
}
void TestRangeDiscoveryAndIteration() {
Bits bits;
Check(bits.FirstRange() == Bits::Range{128, 128},
"empty FirstRange is wrong");
Check(bits.LastRange() == Bits::Range{0, 0}, "empty LastRange is wrong");
bits.SetRange(3, 8);
bits.SetRange(63, 70);
bits.Set(127);
Check(bits.FirstRange() == Bits::Range{3, 8}, "FirstRange is wrong");
Check(bits.FirstRangeFrom(5) == Bits::Range{5, 8},
"FirstRangeFrom inside a run is wrong");
Check(bits.FirstRangeFrom(8) == Bits::Range{63, 70},
"FirstRangeFrom gap is wrong");
Check(bits.LastRange() == Bits::Range{127, 128}, "LastRange is wrong");
Check(bits.LastRangeFrom(69) == Bits::Range{63, 69},
"LastRangeFrom inside a run is wrong");
Check(bits.LastRangeFrom(63) == Bits::Range{3, 8},
"LastRangeFrom gap is wrong");
constexpr std::array expected{Bits::Range{3, 8}, Bits::Range{63, 70},
Bits::Range{127, 128}};
size_t range_index = 0;
for (const auto range : bits) {
Check(range_index < expected.size() && range == expected[range_index],
"range iterator produced the wrong run");
range_index++;
}
Check(range_index == expected.size(), "range iterator omitted a run");
}
void TestRandomizedDifferential() {
Bits bits;
std::array<bool, 128> reference{};
uint64_t random = 0x53a9'7f11'ced4'29b5ull;
const auto next_random = [&random] {
random ^= random << 13;
random ^= random >> 7;
random ^= random << 17;
return random;
};
for (size_t operation = 0; operation < 10000; operation++) {
const auto first = static_cast<size_t>(next_random() % 128);
const auto last =
first + 1 + static_cast<size_t>(next_random() % (128 - first));
if ((next_random() & 1) != 0) {
bits.SetRange(first, last);
for (auto index = first; index < last; index++) {
reference[index] = true;
}
} else {
bits.UnsetRange(first, last);
for (auto index = first; index < last; index++) {
reference[index] = false;
}
}
bool any = false;
for (size_t index = 0; index < reference.size(); index++) {
Check(bits.Get(index) == reference[index],
"randomized bit state diverged");
any |= reference[index];
}
Check(bits.Any() == any && bits.None() == !any,
"randomized Any/None diverged");
const auto range_start = static_cast<size_t>(next_random() % 129);
auto expected_first_begin = range_start;
while (expected_first_begin < reference.size() &&
!reference[expected_first_begin]) {
expected_first_begin++;
}
if (expected_first_begin == reference.size()) {
Check(bits.FirstRangeFrom(range_start) == Bits::Range{128, 128},
"randomized FirstRangeFrom empty suffix diverged");
} else {
auto expected_first_end = expected_first_begin;
while (expected_first_end < reference.size() &&
reference[expected_first_end]) {
expected_first_end++;
}
Check(bits.FirstRangeFrom(range_start) ==
Bits::Range{expected_first_begin, expected_first_end},
"randomized FirstRangeFrom diverged");
}
const auto range_end = static_cast<size_t>(next_random() % 129);
auto expected_last_end = range_end;
while (expected_last_end != 0 && !reference[expected_last_end - 1]) {
expected_last_end--;
}
if (expected_last_end == 0) {
Check(bits.LastRangeFrom(range_end) == Bits::Range{0, 0},
"randomized LastRangeFrom empty prefix diverged");
} else {
auto expected_last_begin = expected_last_end;
while (expected_last_begin != 0 && reference[expected_last_begin - 1]) {
expected_last_begin--;
}
Check(bits.LastRangeFrom(range_end) ==
Bits::Range{expected_last_begin, expected_last_end},
"randomized LastRangeFrom diverged");
}
const auto masked_start = static_cast<size_t>(next_random() % 129);
const auto masked_end =
masked_start +
static_cast<size_t>(next_random() % (129 - masked_start));
const Bits masked(bits, masked_start, masked_end);
for (size_t index = 0; index < reference.size(); index++) {
Check(masked.Get(index) == (index >= masked_start && index < masked_end &&
reference[index]),
"randomized masked constructor diverged");
}
size_t first_begin = 0;
while (first_begin < reference.size() && !reference[first_begin]) {
first_begin++;
}
if (first_begin == reference.size()) {
Check(bits.FirstRange() == Bits::Range{128, 128},
"randomized empty FirstRange diverged");
Check(bits.LastRange() == Bits::Range{0, 0},
"randomized empty LastRange diverged");
} else {
auto first_end = first_begin;
while (first_end < reference.size() && reference[first_end]) {
first_end++;
}
Check(bits.FirstRange() == Bits::Range{first_begin, first_end},
"randomized FirstRange diverged");
auto last_end = reference.size();
while (!reference[last_end - 1]) {
last_end--;
}
auto last_begin = last_end;
while (last_begin != 0 && reference[last_begin - 1]) {
last_begin--;
}
Check(bits.LastRange() == Bits::Range{last_begin, last_end},
"randomized LastRange diverged");
}
size_t expected_begin = 0;
for (const auto [begin, end] : bits) {
while (expected_begin < reference.size() && !reference[expected_begin]) {
expected_begin++;
}
Check(begin == expected_begin, "randomized iterator run start diverged");
while (expected_begin < reference.size() && reference[expected_begin]) {
expected_begin++;
}
Check(end == expected_begin, "randomized iterator run end diverged");
}
while (expected_begin < reference.size() && !reference[expected_begin]) {
expected_begin++;
}
Check(expected_begin == reference.size(),
"randomized iterator omitted a run");
}
}
void TestTrackerSizedRandomizedDifferential() {
using TrackerBits = Common::BitArray<1024>;
TrackerBits bits;
std::array<bool, 1024> reference{};
uint64_t random = 0x9e37'79b9'7f4a'7c15ull;
const auto next_random = [&random] {
random ^= random << 13;
random ^= random >> 7;
random ^= random << 17;
return random;
};
for (size_t operation = 0; operation < 4096; operation++) {
const auto first = static_cast<size_t>(next_random() % reference.size());
const auto last =
first + 1 +
static_cast<size_t>(next_random() % (reference.size() - first));
const bool set = (next_random() & 1) != 0;
if (set) {
bits.SetRange(first, last);
} else {
bits.UnsetRange(first, last);
}
for (auto index = first; index < last; index++) {
reference[index] = set;
}
for (size_t index = 0; index < reference.size(); index++) {
Check(bits.Get(index) == reference[index],
"tracker-sized randomized bit state diverged");
}
size_t expected = 0;
for (const auto [begin, end] : bits) {
while (expected < reference.size() && !reference[expected]) {
expected++;
}
Check(begin == expected, "tracker-sized randomized range start diverged");
while (expected < reference.size() && reference[expected]) {
expected++;
}
Check(end == expected, "tracker-sized randomized range end diverged");
}
while (expected < reference.size() && !reference[expected]) {
expected++;
}
Check(expected == reference.size(),
"tracker-sized randomized iterator omitted a run");
}
}
} // namespace
int main() {
TestPointAndRangeOperations();
TestMaskedConstructionAndBitwiseOperations();
TestRangeDiscoveryAndIteration();
TestRandomizedDifferential();
TestTrackerSizedRandomizedDifferential();
std::puts("BitArrayTests: all cases passed");
return 0;
}
+19 -1
View File
@@ -117,12 +117,15 @@ void TestStrictByteFilteringAndPredicate() {
OwnerIndex index;
Check(index.Register(31, {{0x300100, 0x100}}), "first byte-disjoint owner registers");
Check(index.Register(32, {{0x300800, 0x100}}), "second byte-disjoint owner registers");
Check(index.Register(33, {{0x30f000, 0x100}}), "coarse-only owner registers");
Check(index.TrackingMembershipCount(0x300) == 2,
"byte-disjoint owners share one tracking page");
Check(index.CoarseMembershipCount(3) == 3,
"all owners share one coarse candidate bucket");
Check(index.Query(0x300400, 0x40).empty(), "page hit without byte overlap is filtered out");
const auto page_candidates = index.QueryCandidates(0x300400, 0x40);
Check(page_candidates.size() == 2,
"fault candidate query retains byte-disjoint owners on the touched page");
"fault candidate query retains touched-page owners and rejects coarse-only owners");
const auto first = index.Query(0x300180, 0x10);
Check(first.size() == 1 && first.front() == 31,
"strict byte overlap selects only the matching owner");
@@ -132,6 +135,20 @@ void TestStrictByteFilteringAndPredicate() {
"supplied predicate filters query owners");
}
void TestOwnerIndexAddressSpaceBoundary() {
OwnerIndex index;
constexpr uint64_t last_byte = OwnerIndex::CoarseTable::kAddressSpaceSize - 1;
Check(index.Register(41, {{last_byte, 1}}), "final guest byte registers");
const auto exact = index.Query(last_byte, 1);
Check(exact.size() == 1 && exact.front() == 41,
"strict query finds an exact overlap at the final guest byte");
Check(index.Query(last_byte - 1, 1).empty(),
"strict query preserves half-open overlap boundaries");
const auto page_candidates = index.QueryCandidates(last_byte - 1, 1);
Check(page_candidates.size() == 1 && page_candidates.front() == 41,
"page candidate query retains a byte-disjoint owner on the final tracking page");
}
} // namespace
int main() {
@@ -142,6 +159,7 @@ int main() {
TestMultiRangeRegistrationDeduplicatesPages();
TestSharedPageUnregisterLifecycle();
TestStrictByteFilteringAndPredicate();
TestOwnerIndexAddressSpaceBoundary();
std::printf("ImagePageTableTests: all cases passed\n");
return 0;
}
+549 -1096
View File
File diff suppressed because it is too large Load Diff
+519 -658
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -870,6 +870,7 @@ void TestMaterializationSharesReadConstEvaluation() {
for (uint32_t i = 0; i < memory.words.size(); i++) {
memory.words[i] = 0x100 + i;
}
memory.words[1] |= Prospero::GpuEnumValue(Prospero::BufferFormat::k8UNorm) << 20u;
memory.words[3] |= Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) << 28u;
std::array<uint32_t, 32> user_data {};
user_data[16] = static_cast<uint32_t>(memory.base);
@@ -910,6 +911,7 @@ void TestInvalidImagesMaterializeAsNull() {
std::string error;
for (uint32_t type = 0; type < 8; type++) {
auto descriptor = stale;
descriptor[1] |= Prospero::GpuEnumValue(Prospero::BufferFormat::k8UNorm) << 20u;
descriptor[3] = (descriptor[3] & 0x0fffffffu) | (type << 28u);
ResourceSnapshot snapshot;
Check(MaterializeResources(sampled, {descriptor}, snapshot, &error) &&
@@ -966,7 +968,18 @@ void TestInvalidImagesMaterializeAsNull() {
}
}
std::array<uint32_t, 8> invalid_format {};
invalid_format[0] = 1;
invalid_format[3] = Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) << 28u;
ResourceSnapshot invalid_format_snapshot;
Check(MaterializeResources(sampled, {invalid_format}, invalid_format_snapshot, &error) &&
std::all_of(invalid_format_snapshot.images[0].dwords.begin(),
invalid_format_snapshot.images[0].dwords.end(),
[](uint32_t word) { return word == 0; }),
"invalid-format image descriptor was not normalized to null");
auto valid = stale;
valid[1] |= Prospero::GpuEnumValue(Prospero::BufferFormat::k8UNorm) << 20u;
valid[3] =
(valid[3] & 0x0fffffffu) | (Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) << 28u);
ResourceSnapshot valid_snapshot;
File diff suppressed because it is too large Load Diff
+131
View File
@@ -1356,6 +1356,135 @@ void TestLargeDirectMapAliasesAcrossChunks() {
std::printf("[host] %-48s ok\n", test);
}
void TestHintlessDirectMapUsesCanonicalGuestBase() {
// Mirrors the allocation Sony's libc.prx makes for its internal heap: 4 MiB of
// direct memory, 2 MiB aligned, mapped with no address hint. The PS5 kernel never
// places hint-less user mappings below 0x200000000 and guest code relies on that
// (libc fails its mspace setup for a lower heap address, and the first malloc then
// dereferences a null mspace). Writes through the mapping must also stick.
const char* test = "HintlessDirectMapUsesCanonicalGuestBase";
constexpr uint64_t Len = 0x400000;
constexpr uint64_t Align = 0x200000;
int64_t phys_addr = 0;
CheckOk(test,
Libs::LibKernel::Memory::KernelAllocateDirectMemory(0, 0x260000000ull, Len, Align, 12,
&phys_addr),
"KernelAllocateDirectMemory");
void* address = nullptr;
CheckOk(test,
Libs::LibKernel::Memory::KernelMapNamedDirectMemory(&address, Len, SceKernelProtCpuRw,
0, phys_addr, Align, "libc_heap"),
"KernelMapNamedDirectMemory");
const auto base = reinterpret_cast<uint64_t>(address);
{
char message[128] = {};
std::snprintf(message, sizeof(message),
"hint-less direct map landed below the PS5 base: 0x%016" PRIx64, base);
Check(test, base >= 0x200000000ull, message);
}
auto* header = reinterpret_cast<uint64_t*>(base);
header[0] = 0x4d53504143453030ull; // "MSPACE00"
header[7] = 0x58585858ull; // magic at +0x38, like the libc mspace
*reinterpret_cast<uint64_t*>(base + Len - 8) = 0x454e444d41524bull;
Check(test, header[0] == 0x4d53504143453030ull, "immediate readback of header[0] failed");
Check(test, header[7] == 0x58585858ull, "immediate readback of header[7] failed");
Check(test, *reinterpret_cast<const uint64_t*>(base + Len - 8) == 0x454e444d41524bull,
"immediate readback of tail failed");
uint64_t backing = 0;
Check(test, Libs::LibKernel::Memory::TryReadBacking(base + 0x38, &backing, sizeof(backing)),
"TryReadBacking(header+0x38)");
Check(test, backing == 0x58585858ull, "backing store does not see the guest write at +0x38");
CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(base, Len), "KernelMunmap");
CheckOk(test, Libs::LibKernel::Memory::KernelReleaseDirectMemory(phys_addr, Len),
"KernelReleaseDirectMemory");
std::printf("[host] %-48s ok\n", test);
}
void TestDirectMemoryContentPersistsAcrossRemap() {
const char* test = "DirectMemoryContentPersistsAcrossRemap";
constexpr uint64_t MapSize = SceKernelPageSize * 4;
int64_t phys_addr = 0;
CheckOk(test,
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
SceKernelDirectMemoryStart, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
MapSize, SceKernelPageSize, SceKernelMtypeC, &phys_addr),
"KernelAllocateDirectMemory");
// Direct memory is physical: contents must survive unmapping and remapping, including
// a remap of a sub-range at a nonzero physical offset.
void* address = nullptr;
CheckOk(test,
Libs::LibKernel::Memory::KernelMapNamedDirectMemory(&address, MapSize,
SceKernelProtCpuRw, 0, phys_addr,
SceKernelPageSize, "persist_a"),
"KernelMapNamedDirectMemory(first)");
const auto base = reinterpret_cast<uint64_t>(address);
for (uint64_t offset = 0; offset < MapSize; offset += sizeof(uint64_t)) {
*reinterpret_cast<uint64_t*>(base + offset) = offset ^ 0x4b5954595045525aull; // "KYTYPERZ"
}
CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(base, MapSize), "KernelMunmap(first)");
void* remap = nullptr;
CheckOk(test,
Libs::LibKernel::Memory::KernelMapNamedDirectMemory(&remap, MapSize,
SceKernelProtCpuRw, 0, phys_addr,
SceKernelPageSize, "persist_b"),
"KernelMapNamedDirectMemory(remap)");
const auto remap_base = reinterpret_cast<uint64_t>(remap);
for (uint64_t offset = 0; offset < MapSize; offset += sizeof(uint64_t)) {
const auto expected = offset ^ 0x4b5954595045525aull;
const auto actual = *reinterpret_cast<const uint64_t*>(remap_base + offset);
if (actual != expected) {
char message[160] = {};
std::snprintf(message, sizeof(message),
"content lost across remap at offset 0x%" PRIx64 ": expected 0x%016" PRIx64
", read 0x%016" PRIx64,
offset, expected, actual);
Fail(test, message);
}
}
CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(remap_base, MapSize), "KernelMunmap(remap)");
// Sub-range remap at a nonzero physical offset: page 2 of the original allocation.
void* partial = nullptr;
CheckOk(test,
Libs::LibKernel::Memory::KernelMapNamedDirectMemory(
&partial, SceKernelPageSize, SceKernelProtCpuRw, 0,
phys_addr + static_cast<int64_t>(SceKernelPageSize * 2), SceKernelPageSize,
"persist_c"),
"KernelMapNamedDirectMemory(partial)");
const auto partial_base = reinterpret_cast<uint64_t>(partial);
for (uint64_t offset = 0; offset < SceKernelPageSize; offset += sizeof(uint64_t)) {
const auto expected = (SceKernelPageSize * 2 + offset) ^ 0x4b5954595045525aull;
const auto actual = *reinterpret_cast<const uint64_t*>(partial_base + offset);
if (actual != expected) {
char message[160] = {};
std::snprintf(message, sizeof(message),
"content lost in partial remap at offset 0x%" PRIx64
": expected 0x%016" PRIx64 ", read 0x%016" PRIx64,
offset, expected, actual);
Fail(test, message);
}
}
CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(partial_base, SceKernelPageSize),
"KernelMunmap(partial)");
CheckOk(test, Libs::LibKernel::Memory::KernelReleaseDirectMemory(phys_addr, MapSize),
"KernelReleaseDirectMemory");
std::printf("[host] %-48s ok\n", test);
}
void TestDirectMapUnmapReusesHostAddress() {
const char* test = "DirectMapUnmapReusesHostAddress";
@@ -2150,6 +2279,8 @@ int main() {
RunTest(TestDirectAlignmentStaysWithinSearchRange);
RunTest(TestDefaultDirectMapUsesSystemAddressRange);
RunTest(TestLargeDirectMapAliasesAcrossChunks);
RunTest(TestHintlessDirectMapUsesCanonicalGuestBase);
RunTest(TestDirectMemoryContentPersistsAcrossRemap);
RunTest(TestDirectMapUnmapReusesHostAddress);
RunTest(TestFixedReserveReplacesPartialDirectMapping);
RunTest(TestFixedReserveRollbackConsumesRestoredPlaceholder);
+413 -12
View File
@@ -5394,7 +5394,224 @@ void TestNewShaderRecompilerCfgSharedOuterAndLoopMerge() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgLoopSharedContinueSelectionMerges() {
void TestNewShaderRecompilerCfgLoopEarlyBreakNoSelection() {
const uint32_t shader[] = {
EncodeSopc(0x0a, 0, 129), // loop: s_cmp_lt_u32 s0, 1
EncodeSopp(0x04, 4), // loop exit -> end
EncodeSopc(0x06, 1, 1), // s_cmp_eq_u32 s1, s1
EncodeSopp(0x04, 2), // early break -> same loop end
EncodeSop2(0x00, 0, 0, 129), // s_add_u32 s0, s0, 1
EncodeSopp(0x02, 0xfffau), // backedge -> loop header
0xbf810000u,
};
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"loop early-break CFG did not stay on structured path");
Check(SpirvInstructionOpcodeCount(result.spirv, 246) != 0,
"loop early-break SPIR-V lacks OpLoopMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0,
"loop early-break SPIR-V unexpectedly used OpSelectionMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"loop early-break CFG unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgNestedLoopNonlocalExitDispatcher() {
const uint32_t shader[] = {
EncodeSopc(0x0a, 0, 129), // outer loop: s_cmp_lt_u32 s0, 1
EncodeSopp(0x04, 9), // outer exit -> end
EncodeSopc(0x0a, 1, 129), // inner loop: s_cmp_lt_u32 s1, 1
EncodeSopp(0x04, 5), // inner exit -> outer continue
EncodeSopc(0x06, 2, 2), // s_cmp_eq_u32 s2, s2
EncodeSopp(0x05, 5), // nonlocal exit -> outer end
EncodeSMovB32(3, 129), // inner work
EncodeSop2(0x00, 1, 1, 129), // s_add_u32 s1, s1, 1
EncodeSopp(0x02, 0xfff9u), // inner backedge
EncodeSop2(0x00, 0, 0, 129), // outer continue: s_add_u32 s0, s0, 1
EncodeSopp(0x02, 0xfff5u), // outer backedge
0xbf810000u,
};
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=dispatcher"),
"nested-loop nonlocal exit did not select dispatcher fallback");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) != 0,
"nested-loop nonlocal exit dispatcher SPIR-V lacks OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgNestedLoopLocalExitNoSelection() {
const uint32_t shader[] = {
EncodeSopc(0x0a, 0, 129), // outer loop: s_cmp_lt_u32 s0, 1
EncodeSopp(0x04, 6), // outer exit -> end
EncodeSopc(0x0a, 1, 129), // inner loop: s_cmp_lt_u32 s1, 1
EncodeSopp(0x04, 2), // inner exit -> outer continue
EncodeSMovB32(2, 129), // inner work
EncodeSopp(0x02, 0xfffcu), // inner backedge
EncodeSop2(0x00, 0, 0, 129), // outer continue: s_add_u32 s0, s0, 1
EncodeSopp(0x02, 0xfff8u), // outer backedge
0xbf810000u,
};
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"nested local loop exit did not stay on structured path");
Check(SpirvInstructionOpcodeCount(result.spirv, 246) >= 2,
"nested local loop exit SPIR-V lacks both OpLoopMerge instructions");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0,
"nested local loop exit SPIR-V unexpectedly used OpSelectionMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"nested local loop exit unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgNestedLoopExitTailMergeSplit() {
const uint32_t shader[] = {
EncodeSopc(0x0a, 0, 129), // outer loop: s_cmp_lt_u32 s0, 1
EncodeSopp(0x04, 11), // outer exit -> end
EncodeSopc(0x06, 1, 1), // inner loop first exit condition
EncodeSopp(0x05, 3), // first inner exit -> tail A
EncodeSopc(0x06, 2, 2), // inner loop second exit condition
EncodeSopp(0x05, 3), // second inner exit -> tail B
EncodeSopp(0x02, 0xfffbu), // inner backedge
EncodeSMovB32(3, 129), // tail A
EncodeSopp(0x02, 2), // tail A -> outer continue
EncodeSMovB32(4, 129), // tail B
EncodeSopp(0x02, 0), // tail B -> outer continue
EncodeSop2(0x00, 0, 0, 129), // outer continue: s_add_u32 s0, s0, 1
EncodeSopp(0x02, 0xfff3u), // outer backedge
0xbf810000u,
};
ShaderRecompiler::Decoder::Program program;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(std::span {shader}, program, &error),
error.c_str());
ShaderRecompiler::CFG::Graph graph;
Check(ShaderRecompiler::CFG::BuildGraph(program, graph, &error), error.c_str());
const auto original_block_count = graph.blocks.size();
Check(ShaderRecompiler::CFG::Structurize(graph, &error), error.c_str());
Check(graph.blocks.size() > original_block_count,
"nested loop exit tails did not create a private inner merge");
const auto* outer_header = graph.FindBlockByPc(0);
const auto* inner_header = graph.FindBlockByPc(8);
Check(outer_header != nullptr && inner_header != nullptr &&
outer_header->terminator.loop_header && inner_header->terminator.loop_header,
"nested loop exit-tail fixture did not retain both loop headers");
Check(inner_header->terminator.merge_block != outer_header->terminator.continue_block,
"inner loop merge still aliases the outer continue target");
const auto* inner_merge = graph.FindBlock(inner_header->terminator.merge_block);
Check(inner_merge != nullptr && inner_merge->inst_begin == inner_merge->inst_end &&
inner_merge->terminator.kind == ShaderRecompiler::CFG::TerminatorKind::Branch &&
inner_merge->terminator.true_block == outer_header->terminator.continue_block,
"private inner merge does not forward to the outer continue target");
}
void TestNewShaderRecompilerCfgMixedContinueNonmergeExitDispatcher() {
const uint32_t shader[] = {
EncodeSopc(0x06, 7, 7), // entry branch bypasses loop -> exit X
EncodeSopp(0x05, 5), // entry -> X
EncodeSopc(0x0a, 0, 129), // loop: s_cmp_lt_u32 s0, 1
EncodeSopp(0x04, 5), // loop exit -> Y
EncodeSopc(0x06, 1, 1), // inner condition
EncodeSopp(0x05, 1), // nonmerge exit -> X, else continue
EncodeSopp(0x02, 0xfffbu), // loop backedge
EncodeSMovB32(2, 129), // X
EncodeSopp(0x02, 2), // X -> end
EncodeSMovB32(3, 129), // Y
EncodeSopp(0x02, 0), // Y -> end
0xbf810000u,
};
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=dispatcher"),
"mixed continue/nonmerge exit did not select dispatcher fallback");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) != 0,
"mixed continue/nonmerge exit dispatcher SPIR-V lacks OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgConditionalLatchNoSelection() {
const uint32_t shader[] = {
EncodeSopp(0x02, 0), // loop header -> conditional block
EncodeSopc(0x06, 0, 0), // s_cmp_eq_u32 s0, s0
EncodeSopp(0x05, 1), // loop exit -> end
EncodeSopp(0x02, 0xfffcu), // separate latch -> loop header
0xbf810000u,
};
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"conditional latch did not stay on structured path");
Check(SpirvInstructionOpcodeCount(result.spirv, 246) != 0,
"conditional latch SPIR-V lacks OpLoopMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0,
"conditional latch SPIR-V unexpectedly used OpSelectionMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"conditional latch unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgDirectConditionalLatchNoSelection() {
const uint32_t shader[] = {
EncodeSopp(0x02, 0), // loop header -> conditional latch
EncodeSopc(0x06, 0, 0), // s_cmp_eq_u32 s0, s0
EncodeSopp(0x05, 0xfffdu), // direct latch backedge -> loop header
0xbf810000u,
};
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"direct conditional latch did not stay on structured path");
Check(SpirvInstructionOpcodeCount(result.spirv, 246) != 0,
"direct conditional latch SPIR-V lacks OpLoopMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0,
"direct conditional latch SPIR-V unexpectedly used OpSelectionMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"direct conditional latch unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgLoopEarlyContinuesNoSelection() {
const uint32_t shader[] = {
EncodeSMovB32(0, 128), // s0 = 0
EncodeSopc(0x0a, 0, 130), // loop: s_cmp_lt_u32 s0, 2
@@ -5419,15 +5636,111 @@ void TestNewShaderRecompilerCfgLoopSharedContinueSelectionMerges() {
std::string error;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"shared loop continue selections should stay on structured path");
Check(!Common::ContainsStr(result.ir_dump, "duplicate structured merge block"),
"shared loop continue selections were not split before structurization");
Check(SpirvContainsOpcode(result.spirv, 246),
"shared loop continue selections SPIR-V lacks OpLoopMerge");
Check(SpirvContainsOpcode(result.spirv, 247),
"shared loop continue selections SPIR-V lacks OpSelectionMerge");
Check(!SpirvContainsOpcode(result.spirv, 251),
"shared loop continue selections unexpectedly used dispatcher OpSwitch");
"loop early continues should stay on structured path");
Check(SpirvInstructionOpcodeCount(result.spirv, 246) != 0,
"loop early continues SPIR-V lacks OpLoopMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0,
"loop early continues SPIR-V unexpectedly used OpSelectionMerge");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0,
"loop early continues unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgConditionalLoopHeaderSelection() {
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 0), // loop body selection condition
EncodeSopp(0x05, 2), // select path B
EncodeSMovB32(1, 129), // path A
EncodeSopp(0x02, 1), // path A -> join
EncodeSMovB32(2, 129), // path B
EncodeSMovB32(3, 129), // join
EncodeSopc(0x06, 4, 4), // repeat condition
EncodeSopp(0x05, 0xfff8u), // repeat -> guest header
0xbf810000u,
};
ShaderRecompiler::Decoder::Program decoded;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(std::span {shader}, decoded, &error),
error.c_str());
ShaderRecompiler::CFG::Graph graph;
Check(ShaderRecompiler::CFG::BuildGraph(decoded, graph, &error), error.c_str());
const auto original_block_count = graph.blocks.size();
Check(ShaderRecompiler::CFG::Structurize(graph, &error), error.c_str());
Check(graph.blocks.size() > original_block_count,
"conditional guest loop header did not create a synthetic header");
uint32_t loop_headers = 0;
uint32_t selection_headers = 0;
for (const auto& block: graph.blocks) {
if (block.terminator.loop_header) {
loop_headers++;
Check(block.inst_begin == block.inst_end &&
block.terminator.kind == ShaderRecompiler::CFG::TerminatorKind::Branch,
"canonical loop header is not an empty unconditional block");
} else if (block.terminator.kind ==
ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch &&
block.terminator.merge_block != UINT32_MAX) {
selection_headers++;
}
}
Check(loop_headers == 1u && selection_headers == 1u,
"guest conditional was not separated from the loop header");
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
ShaderRecompiler::CompileResult result;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(SpirvInstructionOpcodeCount(result.spirv, 246) == 1u,
"conditional loop-header SPIR-V has the wrong loop-merge count");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 1u,
"conditional loop-header SPIR-V has the wrong selection-merge count");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"conditional loop-header unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgMultipleLoopLatches() {
const uint32_t shader[] = {
EncodeSopc(0x0a, 0, 129), // loop condition
EncodeSopp(0x04, 5), // loop exit -> end
EncodeSopc(0x06, 1, 1), // early repeat condition
EncodeSopp(0x05, 0xfffcu), // early repeat -> header
EncodeSMovB32(2, 129), // body
EncodeSMovB32(3, 129), // body tail
EncodeSopp(0x02, 0xfff9u), // ordinary latch -> header
0xbf810000u,
};
ShaderRecompiler::Decoder::Program decoded;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(std::span {shader}, decoded, &error),
error.c_str());
ShaderRecompiler::CFG::Graph graph;
Check(ShaderRecompiler::CFG::BuildGraph(decoded, graph, &error), error.c_str());
const auto original_block_count = graph.blocks.size();
Check(graph.back_edges.size() == 2u, "multiple-latch fixture lacks two native backedges");
Check(ShaderRecompiler::CFG::Structurize(graph, &error), error.c_str());
Check(graph.blocks.size() == original_block_count + 1u,
"multiple native latches did not create one synthetic continue");
Check(graph.back_edges.size() == 1u && graph.natural_loops.size() == 1u,
"multiple native latches were not coalesced to one SPIR-V backedge");
const auto& loop = graph.natural_loops.front();
const auto* continue_block = graph.FindBlock(loop.continue_block);
Check(continue_block != nullptr && continue_block->inst_begin == continue_block->inst_end &&
continue_block->predecessors.size() == 2u,
"canonical continue does not join both native latches");
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
ShaderRecompiler::CompileResult result;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(SpirvInstructionOpcodeCount(result.spirv, 246) == 1u,
"multiple-latch SPIR-V has the wrong loop-merge count");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) == 0u,
"multiple-latch SPIR-V unexpectedly used a selection merge");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"multiple-latch SPIR-V unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -5457,6 +5770,85 @@ void TestNewShaderRecompilerCfgDuplicateMergeStructuredSplit() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgOverlappingEarlyExitLadder() {
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 0), // block 0
EncodeSopp(0x04, 2), // block 0 -> 2 or 1
EncodeSopc(0x06, 1, 1), // block 1
EncodeSopp(0x04, 6), // block 1 -> 5 or 2
EncodeSopc(0x06, 2, 2), // block 2
EncodeSopp(0x04, 4), // block 2 -> 5 or 3
EncodeSopc(0x06, 3, 3), // block 3
EncodeSopp(0x04, 2), // block 3 -> 5 or 4
EncodeSMovB32(4, 129), // block 4
0xbf810000u, // block 4 -> 6
EncodeSMovB32(5, 129), // block 5
0xbf810000u, // block 5 -> 6
};
ShaderRecompiler::Decoder::Program decoded;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(std::span {shader}, decoded, &error),
error.c_str());
ShaderRecompiler::CFG::Graph graph;
Check(ShaderRecompiler::CFG::BuildGraph(decoded, graph, &error), error.c_str());
Check(graph.blocks.size() == 7u && graph.blocks[0].successors == std::vector<uint32_t>({1, 2}) &&
graph.blocks[0].terminator.true_block == 2u &&
graph.blocks[0].terminator.false_block == 1u &&
graph.blocks[1].successors == std::vector<uint32_t>({2, 5}) &&
graph.blocks[1].terminator.true_block == 5u &&
graph.blocks[1].terminator.false_block == 2u &&
graph.blocks[2].successors == std::vector<uint32_t>({3, 5}) &&
graph.blocks[2].terminator.true_block == 5u &&
graph.blocks[2].terminator.false_block == 3u &&
graph.blocks[3].successors == std::vector<uint32_t>({4, 5}) &&
graph.blocks[3].terminator.true_block == 5u &&
graph.blocks[3].terminator.false_block == 4u &&
graph.blocks[4].successors == std::vector<uint32_t>({6}) &&
graph.blocks[5].successors == std::vector<uint32_t>({6}),
"overlapping early-exit fixture does not match the observed shader CFG");
Check(ShaderRecompiler::CFG::Structurize(graph, &error), error.c_str());
std::vector<bool> reachable(graph.blocks.size());
std::vector<uint32_t> pending = {graph.entry_block};
while (!pending.empty()) {
const auto block_id = pending.back();
pending.pop_back();
if (reachable[block_id]) {
continue;
}
reachable[block_id] = true;
pending.insert(pending.end(), graph.blocks[block_id].successors.begin(),
graph.blocks[block_id].successors.end());
}
Check(std::all_of(reachable.begin(), reachable.end(), [](bool value) { return value; }),
"overlapping early-exit structurization left unreachable blocks");
std::vector<uint32_t> merges;
for (const auto& block: graph.blocks) {
if (block.terminator.kind == ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch) {
Check(block.terminator.merge_block != UINT32_MAX &&
std::find(merges.begin(), merges.end(), block.terminator.merge_block) ==
merges.end(),
"overlapping early-exit structurization retained a shared merge");
merges.push_back(block.terminator.merge_block);
}
}
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Pixel;
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.ir_dump, "mode=structured"),
"overlapping early-exit ladder did not stay on the structured path");
Check(!Common::ContainsStr(result.ir_dump, "duplicate structured merge block"),
"overlapping early-exit ladder retained a shared merge");
Check(SpirvInstructionOpcodeCount(result.spirv, 247) >= 4u,
"overlapping early-exit ladder lost its selections");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"overlapping early-exit ladder used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgIrreducibleDispatcher() {
const uint32_t shader[] = {
EncodeSopp(0x05, 2), // entry -> B, fallthrough A
@@ -7014,7 +7406,6 @@ int main() {
using namespace Libs::Graphics;
EnsureConfigInitialized();
TestResourceDescriptorClassification();
TestNativeShaderResourceDependencies();
TestNormalizedImageContracts();
@@ -7086,8 +7477,18 @@ int main() {
TestNewShaderRecompilerCfgLoopHeaderBufferLoadDispatcher();
TestNewShaderRecompilerCfgLoopHeaderDsAppendConsumeDispatcher();
TestNewShaderRecompilerCfgSharedOuterAndLoopMerge();
TestNewShaderRecompilerCfgLoopSharedContinueSelectionMerges();
TestNewShaderRecompilerCfgLoopEarlyBreakNoSelection();
TestNewShaderRecompilerCfgNestedLoopNonlocalExitDispatcher();
TestNewShaderRecompilerCfgNestedLoopLocalExitNoSelection();
TestNewShaderRecompilerCfgNestedLoopExitTailMergeSplit();
TestNewShaderRecompilerCfgMixedContinueNonmergeExitDispatcher();
TestNewShaderRecompilerCfgConditionalLatchNoSelection();
TestNewShaderRecompilerCfgDirectConditionalLatchNoSelection();
TestNewShaderRecompilerCfgLoopEarlyContinuesNoSelection();
TestNewShaderRecompilerCfgConditionalLoopHeaderSelection();
TestNewShaderRecompilerCfgMultipleLoopLatches();
TestNewShaderRecompilerCfgDuplicateMergeStructuredSplit();
TestNewShaderRecompilerCfgOverlappingEarlyExitLadder();
TestNewShaderRecompilerCfgIrreducibleDispatcher();
TestNewShaderRecompilerExecMaskHelpers();
TestComputeShaderInputWaveSize();